mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 13:12:17 +08:00
kritorをmasterブランチに設定する
kritorをmasterブランチに設定する
This commit is contained in:
parent
7782feb6ac
commit
680317da13
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "kritor"]
|
||||||
|
path = kritor
|
||||||
|
url = https://github.com/KarinJS/kritor
|
20
README.md
20
README.md
@ -28,24 +28,8 @@
|
|||||||
## 兼容|迁移|替代 说明
|
## 兼容|迁移|替代 说明
|
||||||
|
|
||||||
- 一键移植:本项目基于 go-cqhttp 的文档进行开发实现。
|
- 一键移植:本项目基于 go-cqhttp 的文档进行开发实现。
|
||||||
- 平行部署:可多平台部署,未来将会支持 Docker 部署的教程。
|
- 平行部署:可多平台部署,未来将会支持 Docker 部署的教程。
|
||||||
|
- 替代方案:[Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core)
|
||||||
## 相关项目
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td><a href="https://github.com/LagrangeDev/Lagrange.Core">Lagrange.Core</a></td>
|
|
||||||
<td>NTQQ 的协议实现</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><a href="https://github.com/whitechi73/OpenShamrock">OpenShamrock</a></td>
|
|
||||||
<td>基于 Xposed 实现 OneBot 标准的机器人框架(👈你在这里</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><a href="https://github.com/chrononeko/chronocat">Chronocat</a></td>
|
|
||||||
<td>基于 Electron 的、模块化的 Satori 框架</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
## 权限声明
|
## 权限声明
|
||||||
|
|
||||||
|
@ -9,6 +9,6 @@ java {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(DEPENDENCY_PROTOBUF)
|
//implementation(DEPENDENCY_PROTOBUF)
|
||||||
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
implementation(kotlinx("serialization-protobuf", "1.6.2"))
|
||||||
}
|
}
|
11
annotations/src/main/java/kritor/service/Grpc.kt
Normal file
11
annotations/src/main/java/kritor/service/Grpc.kt
Normal file
@ -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,
|
||||||
|
|
||||||
|
)
|
@ -5,6 +5,8 @@ import kotlinx.serialization.protobuf.ProtoBuf
|
|||||||
|
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
val EMPTY_BYTE_ARRAY = ByteArray(0)
|
||||||
|
|
||||||
interface Protobuf<T: Protobuf<T>>
|
interface Protobuf<T: Protobuf<T>>
|
||||||
|
|
||||||
inline fun <reified T: Protobuf<T>> ByteArray.decodeProtobuf(to: KClass<T>? = null): T {
|
inline fun <reified T: Protobuf<T>> ByteArray.decodeProtobuf(to: KClass<T>? = null): T {
|
||||||
|
@ -17,7 +17,7 @@ android {
|
|||||||
minSdk = 27
|
minSdk = 27
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = getVersionCode()
|
versionCode = getVersionCode()
|
||||||
versionName = "1.0.9" + ".r${getGitCommitCount()}." + getVersionName()
|
versionName = "1.1.0" + ".r${getGitCommitCount()}." + getVersionName()
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@ -201,14 +201,8 @@ dependencies {
|
|||||||
implementation("io.coil-kt:coil-compose:2.4.0")
|
implementation("io.coil-kt:coil-compose:2.4.0")
|
||||||
|
|
||||||
implementation(kotlinx("io-jvm", "0.1.16"))
|
implementation(kotlinx("io-jvm", "0.1.16"))
|
||||||
implementation(ktor("server", "core"))
|
|
||||||
implementation(ktor("server", "host-common"))
|
|
||||||
implementation(ktor("server", "status-pages"))
|
|
||||||
implementation(ktor("server", "netty"))
|
|
||||||
implementation(ktor("server", "content-negotiation"))
|
|
||||||
implementation(ktor("client", "core"))
|
implementation(ktor("client", "core"))
|
||||||
implementation(ktor("client", "content-negotiation"))
|
implementation(ktor("client", "okhttp"))
|
||||||
implementation(ktor("client", "cio"))
|
|
||||||
implementation(ktor("serialization", "kotlinx-json"))
|
implementation(ktor("serialization", "kotlinx-json"))
|
||||||
|
|
||||||
implementation(project(":xposed"))
|
implementation(project(":xposed"))
|
||||||
|
@ -37,7 +37,6 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
|||||||
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
||||||
${SRC_DIR}
|
${SRC_DIR}
|
||||||
md5.cpp
|
md5.cpp
|
||||||
cqcode.cpp
|
|
||||||
silk.cpp
|
silk.cpp
|
||||||
message.cpp
|
message.cpp
|
||||||
shamrock.cpp)
|
shamrock.cpp)
|
||||||
|
@ -1,138 +0,0 @@
|
|||||||
#include <stdexcept>
|
|
||||||
#include "cqcode.h"
|
|
||||||
|
|
||||||
inline void replace_string(std::string& str, const std::string& from, const std::string& to) {
|
|
||||||
size_t startPos = 0;
|
|
||||||
while ((startPos = str.find(from, startPos)) != std::string::npos) {
|
|
||||||
str.replace(startPos, from.length(), to);
|
|
||||||
startPos += to.length();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline int utf8_next_len(const std::string& str, size_t offset)
|
|
||||||
{
|
|
||||||
uint8_t c = (uint8_t)str[offset];
|
|
||||||
if (c >= 0xFC)
|
|
||||||
return 6;
|
|
||||||
else if (c >= 0xF8)
|
|
||||||
return 5;
|
|
||||||
else if (c >= 0xF0)
|
|
||||||
return 4;
|
|
||||||
else if (c >= 0xE0)
|
|
||||||
return 3;
|
|
||||||
else if (c >= 0xC0)
|
|
||||||
return 2;
|
|
||||||
else if (c > 0x00)
|
|
||||||
return 1;
|
|
||||||
else
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::string, std::string>>& dest) {
|
|
||||||
std::string cache;
|
|
||||||
bool is_start = false;
|
|
||||||
std::string key_tmp;
|
|
||||||
std::unordered_map<std::string, std::string> kv;
|
|
||||||
for(size_t i = 0; i < code.size(); i++) {
|
|
||||||
int utf8_char_len = utf8_next_len(code, i);
|
|
||||||
if(utf8_char_len == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
std::string_view c(&code[i],utf8_char_len);
|
|
||||||
if (c == "[") {
|
|
||||||
if (is_start) {
|
|
||||||
throw illegal_code();
|
|
||||||
} else {
|
|
||||||
if (!cache.empty()) {
|
|
||||||
std::unordered_map<std::string, std::string> kv;
|
|
||||||
replace_string(cache, "[", "[");
|
|
||||||
replace_string(cache, "]", "]");
|
|
||||||
replace_string(cache, "&", "&");
|
|
||||||
kv.emplace("_type", "text");
|
|
||||||
kv.emplace("text", cache);
|
|
||||||
dest.push_back(kv);
|
|
||||||
cache.clear();
|
|
||||||
}
|
|
||||||
std::string_view cq_flag(&code[i],4);
|
|
||||||
if(cq_flag == "[CQ:"){
|
|
||||||
is_start = true;
|
|
||||||
i += 3;
|
|
||||||
}else{
|
|
||||||
cache += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (c == "=") {
|
|
||||||
if (is_start) {
|
|
||||||
if (cache.empty()) {
|
|
||||||
throw illegal_code();
|
|
||||||
} else {
|
|
||||||
if (key_tmp.empty()) {
|
|
||||||
key_tmp.append(cache);
|
|
||||||
cache.clear();
|
|
||||||
} else {
|
|
||||||
cache += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cache += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (c == ",") {
|
|
||||||
if (is_start) {
|
|
||||||
if (kv.count("_type") == 0 && !cache.empty()) {
|
|
||||||
kv.emplace("_type", cache);
|
|
||||||
cache.clear();
|
|
||||||
} else {
|
|
||||||
if (!key_tmp.empty()) {
|
|
||||||
replace_string(cache, "[", "[");
|
|
||||||
replace_string(cache, "]", "]");
|
|
||||||
replace_string(cache, ",", ",");
|
|
||||||
replace_string(cache, "&", "&");
|
|
||||||
kv.emplace(key_tmp, cache);
|
|
||||||
cache.clear();
|
|
||||||
key_tmp.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cache += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (c == "]") {
|
|
||||||
if (is_start) {
|
|
||||||
if (!cache.empty()) {
|
|
||||||
if (!key_tmp.empty()) {
|
|
||||||
replace_string(cache, "[", "[");
|
|
||||||
replace_string(cache, "]", "]");
|
|
||||||
replace_string(cache, ",", ",");
|
|
||||||
replace_string(cache, "&", "&");
|
|
||||||
kv.emplace(key_tmp, cache);
|
|
||||||
} else {
|
|
||||||
kv.emplace("_type", cache);
|
|
||||||
}
|
|
||||||
dest.push_back(kv);
|
|
||||||
kv.clear();
|
|
||||||
key_tmp.clear();
|
|
||||||
cache.clear();
|
|
||||||
is_start = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cache += c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
cache += c;
|
|
||||||
i += (utf8_char_len - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!cache.empty()) {
|
|
||||||
std::unordered_map<std::string, std::string> kv;
|
|
||||||
replace_string(cache, "[", "[");
|
|
||||||
replace_string(cache, "]", "]");
|
|
||||||
replace_string(cache, "&", "&");
|
|
||||||
kv.emplace("_type", "text");
|
|
||||||
kv.emplace("text", cache);
|
|
||||||
dest.push_back(kv);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,87 +0,0 @@
|
|||||||
#include "jni.h"
|
|
||||||
#include <vector>
|
|
||||||
#include <string>
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
struct Honor {
|
|
||||||
int id;
|
|
||||||
std::string name;
|
|
||||||
std::string icon_url;
|
|
||||||
int priority;
|
|
||||||
};
|
|
||||||
|
|
||||||
int calc_honor_flag(int honor_id, char honor_flag);
|
|
||||||
|
|
||||||
jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor);
|
|
||||||
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jobject JNICALL
|
|
||||||
Java_moe_fuqiuluo_shamrock_remote_action_handlers_GetTroopHonor_nativeDecodeHonor(JNIEnv *env, jobject thiz,
|
|
||||||
jstring user_id,
|
|
||||||
jint honor_id,
|
|
||||||
jbyte honor_flag) {
|
|
||||||
static std::vector<Honor> honor_list = {
|
|
||||||
Honor{1, "龙王", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150116_n4PxCiurbm.png", 1},
|
|
||||||
Honor{2, "群聊之火", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190136_92JEGFKC5k.png", 3},
|
|
||||||
Honor{3, "群聊炽焰", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190204_zgCTeSrMq1.png", 4},
|
|
||||||
Honor{5, "冒尖小春笋", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150335_tUJCAtoKVP.png", 5},
|
|
||||||
Honor{6, "快乐源泉", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150434_3tDmsJExCP.png", 7},
|
|
||||||
Honor{7, "学术新星", "https://sola.gtimg.cn/aoi/sola/20200515140645_j0X6gbuHNP.png", 8},
|
|
||||||
Honor{8, "顶尖学霸", "https://sola.gtimg.cn/aoi/sola/20200515140639_0CtWOpfVzK.png", 9},
|
|
||||||
Honor{9, "至尊学神", "https://sola.gtimg.cn/aoi/sola/20200515140628_P8UEYBjMBT.png", 10},
|
|
||||||
Honor{10, "一笔当先", "https://sola.gtimg.cn/aoi/sola/20200515140654_4r94tSCdaB.png", 11},
|
|
||||||
Honor{11, "奋进小翠竹", "https://sola.gtimg.cn/aoi/sola/20200812151819_wbj6z2NGoB.png", 6},
|
|
||||||
Honor{12, "氛围魔杖", "https://sola.gtimg.cn/aoi/sola/20200812151831_4ZJgQCaD1H.png", 2},
|
|
||||||
Honor{13, "壕礼皇冠", "https://sola.gtimg.cn/aoi/sola/20200930154050_juZOAMg7pt.png", 12},
|
|
||||||
};
|
|
||||||
int flag = calc_honor_flag(honor_id, honor_flag);
|
|
||||||
if ((honor_id != 1 && honor_id != 2 && honor_id != 3) || flag != 1) {
|
|
||||||
auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) {
|
|
||||||
return honor.id == honor_id;
|
|
||||||
});
|
|
||||||
return make_honor_object(env, user_id, honor);
|
|
||||||
} else {
|
|
||||||
auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) {
|
|
||||||
return honor.id == honor_id;
|
|
||||||
});
|
|
||||||
std::string url = "https://static-res.qq.com/static-res/groupInteract/vas/a/" + std::to_string(honor_id) + "_1.png";
|
|
||||||
honor = Honor{honor_id, honor.name, url, honor.priority};
|
|
||||||
return make_honor_object(env, user_id, honor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int calc_honor_flag(int honor_id, char honor_flag) {
|
|
||||||
int flag;
|
|
||||||
if (honor_flag == 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (honor_id == 1) {
|
|
||||||
flag = honor_flag;
|
|
||||||
} else if (honor_id == 2 || honor_id == 3) {
|
|
||||||
flag = honor_flag >> 2;
|
|
||||||
} else if (honor_id != 4) {
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
flag = honor_flag >> 4;
|
|
||||||
}
|
|
||||||
return flag & 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor) {
|
|
||||||
jclass GroupMemberHonor = env->FindClass("moe/fuqiuluo/shamrock/remote/service/data/GroupMemberHonor");
|
|
||||||
jmethodID GroupMemberHonor_init = env->GetMethodID(GroupMemberHonor, "<init>",
|
|
||||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;)V");
|
|
||||||
auto user_id_str = (jstring) user_id;
|
|
||||||
jstring honor_desc = env->NewStringUTF(honor.name.c_str());
|
|
||||||
jstring uin_name = env->NewStringUTF("");
|
|
||||||
jstring honor_icon_url = env->NewStringUTF(honor.icon_url.c_str());
|
|
||||||
jobject ret = env->NewObject(GroupMemberHonor, GroupMemberHonor_init, user_id_str, uin_name, honor_icon_url, 0, honor.id, honor_desc);
|
|
||||||
|
|
||||||
env->DeleteLocalRef(GroupMemberHonor);
|
|
||||||
env->DeleteLocalRef(user_id_str);
|
|
||||||
env->DeleteLocalRef(honor_desc);
|
|
||||||
env->DeleteLocalRef(honor_icon_url);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
#ifndef UNTITLED_CQCODE_H
|
|
||||||
#define UNTITLED_CQCODE_H
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <unordered_map>
|
|
||||||
#include <vector>
|
|
||||||
#include <exception>
|
|
||||||
|
|
||||||
class illegal_code: std::exception {
|
|
||||||
public:
|
|
||||||
[[nodiscard]] const char * what() const noexcept override {
|
|
||||||
return "Error cq code.";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::string, std::string>>& dest);
|
|
||||||
|
|
||||||
void encode_cqcode(const std::vector<std::unordered_map<std::string, std::string>>& segment, std::string& dest);
|
|
||||||
|
|
||||||
#endif //UNTITLED_CQCODE_H
|
|
@ -1,5 +1,4 @@
|
|||||||
#include "jni.h"
|
#include "jni.h"
|
||||||
#include "cqcode.h"
|
|
||||||
#include <random>
|
#include <random>
|
||||||
|
|
||||||
inline void replace_string(std::string& str, const std::string& from, const std::string& to) {
|
inline void replace_string(std::string& str, const std::string& from, const std::string& to) {
|
||||||
@ -12,7 +11,7 @@ inline void replace_string(std::string& str, const std::string& from, const std:
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jlong JNICALL
|
JNIEXPORT jlong JNICALL
|
||||||
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz,
|
Java_qq_service_msg_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz,
|
||||||
jint chat_type,
|
jint chat_type,
|
||||||
jlong time) {
|
jlong time) {
|
||||||
static std::random_device rd;
|
static std::random_device rd;
|
||||||
@ -32,123 +31,6 @@ Java_moe_fuqiuluo_shamrock_helper_MessageHelper_getChatType(JNIEnv *env, jobject
|
|||||||
return (int32_t) ((int64_t) msg_id & 0xffL);
|
return (int32_t) ((int64_t) msg_id & 0xffL);
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jobject JNICALL
|
|
||||||
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeDecodeCQCode(JNIEnv *env, jobject thiz,
|
|
||||||
jstring code) {
|
|
||||||
jclass ArrayList = env->FindClass("java/util/ArrayList");
|
|
||||||
jmethodID NewArrayList = env->GetMethodID(ArrayList, "<init>", "()V");
|
|
||||||
jmethodID ArrayListAdd = env->GetMethodID(ArrayList, "add", "(Ljava/lang/Object;)Z");
|
|
||||||
jobject arrayList = env->NewObject(ArrayList, NewArrayList);
|
|
||||||
|
|
||||||
const char* cCode = env->GetStringUTFChars(code, nullptr);
|
|
||||||
std::string cppCode = cCode;
|
|
||||||
std::vector<std::unordered_map<std::string, std::string>> dest;
|
|
||||||
try {
|
|
||||||
decode_cqcode(cppCode, dest);
|
|
||||||
} catch (illegal_code& code) {
|
|
||||||
return arrayList;
|
|
||||||
}
|
|
||||||
|
|
||||||
jclass HashMap = env->FindClass("java/util/HashMap");
|
|
||||||
jmethodID NewHashMap = env->GetMethodID(HashMap, "<init>", "()V");
|
|
||||||
jclass String = env->FindClass("java/lang/String");
|
|
||||||
jmethodID NewString = env->GetMethodID(String, "<init>", "([BLjava/lang/String;)V");
|
|
||||||
jstring charset = env->NewStringUTF("UTF-8");
|
|
||||||
jmethodID put = env->GetMethodID(HashMap, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
|
||||||
for (auto& map : dest) {
|
|
||||||
jobject hashMap = env->NewObject(HashMap, NewHashMap);
|
|
||||||
for (const auto& pair : map) {
|
|
||||||
jbyteArray keyArray = env->NewByteArray((int) pair.first.size());
|
|
||||||
jbyteArray valueArray = env->NewByteArray((int) pair.second.size());
|
|
||||||
env->SetByteArrayRegion(keyArray, 0, (int) pair.first.size(), (jbyte*)pair.first.c_str());
|
|
||||||
env->SetByteArrayRegion(valueArray, 0, (int) pair.second.size(), (jbyte*)pair.second.c_str());
|
|
||||||
auto key = (jstring) env->NewObject(String, NewString, keyArray, charset);
|
|
||||||
auto value = (jstring) env->NewObject(String, NewString, valueArray, charset);
|
|
||||||
env->CallObjectMethod(hashMap, put, key, value);
|
|
||||||
}
|
|
||||||
env->CallBooleanMethod(arrayList, ArrayListAdd, hashMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
env->DeleteLocalRef(ArrayList);
|
|
||||||
env->DeleteLocalRef(HashMap);
|
|
||||||
env->DeleteLocalRef(String);
|
|
||||||
env->DeleteLocalRef(charset);
|
|
||||||
env->ReleaseStringUTFChars(code, cCode);
|
|
||||||
|
|
||||||
return arrayList;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jstring JNICALL
|
|
||||||
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeEncodeCQCode(JNIEnv *env, jobject thiz,
|
|
||||||
jobject segment_list) {
|
|
||||||
jclass List = env->FindClass("java/util/List");
|
|
||||||
jmethodID ListSize = env->GetMethodID(List, "size", "()I");
|
|
||||||
jmethodID ListGet = env->GetMethodID(List, "get", "(I)Ljava/lang/Object;");
|
|
||||||
jclass Map = env->FindClass("java/util/Map");
|
|
||||||
jmethodID MapGet = env->GetMethodID(Map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
|
|
||||||
jmethodID entrySetMethod = env->GetMethodID(Map, "entrySet", "()Ljava/util/Set;");
|
|
||||||
jclass setClass = env->FindClass("java/util/Set");
|
|
||||||
jmethodID iteratorMethod = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;");
|
|
||||||
jclass entryClass = env->FindClass("java/util/Map$Entry");
|
|
||||||
jmethodID getKeyMethod = env->GetMethodID(entryClass, "getKey", "()Ljava/lang/Object;");
|
|
||||||
jmethodID getValueMethod = env->GetMethodID(entryClass, "getValue", "()Ljava/lang/Object;");
|
|
||||||
|
|
||||||
std::string result;
|
|
||||||
jint size = env->CallIntMethod(segment_list, ListSize);
|
|
||||||
for (int i = 0; i < size; i++ ) {
|
|
||||||
jobject segment = env->CallObjectMethod(segment_list, ListGet, i);
|
|
||||||
jobject entrySet = env->CallObjectMethod(segment, entrySetMethod);
|
|
||||||
jobject iterator = env->CallObjectMethod(entrySet, iteratorMethod);
|
|
||||||
auto type = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("_type"));
|
|
||||||
auto typeString = env->GetStringUTFChars(type, nullptr);
|
|
||||||
if (strcmp(typeString, "text") == 0) {
|
|
||||||
auto text = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("text"));
|
|
||||||
auto textString = env->GetStringUTFChars(text, nullptr);
|
|
||||||
std::string tmpValue = textString;
|
|
||||||
replace_string(tmpValue, "&", "&");
|
|
||||||
replace_string(tmpValue, "[", "[");
|
|
||||||
replace_string(tmpValue, "]", "]");
|
|
||||||
replace_string(tmpValue, ",", ",");
|
|
||||||
result.append(tmpValue);
|
|
||||||
env->ReleaseStringUTFChars(text, textString);
|
|
||||||
} else {
|
|
||||||
result.append("[CQ:");
|
|
||||||
result.append(typeString);
|
|
||||||
while (env->CallBooleanMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "hasNext", "()Z"))) {
|
|
||||||
jobject entry = env->CallObjectMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "next", "()Ljava/lang/Object;"));
|
|
||||||
auto key = (jstring) env->CallObjectMethod(entry, getKeyMethod);
|
|
||||||
auto value = (jstring) env->CallObjectMethod(entry, getValueMethod);
|
|
||||||
auto keyString = env->GetStringUTFChars(key, nullptr);
|
|
||||||
auto valueString = env->GetStringUTFChars(value, nullptr);
|
|
||||||
if (strcmp(keyString, "_type") != 0) {
|
|
||||||
std::string tmpValue = valueString;
|
|
||||||
replace_string(tmpValue, "&", "&");
|
|
||||||
replace_string(tmpValue, "[", "[");
|
|
||||||
replace_string(tmpValue, "]", "]");
|
|
||||||
replace_string(tmpValue, ",", ",");
|
|
||||||
result.append(",").append(keyString).append("=").append(tmpValue);
|
|
||||||
}
|
|
||||||
env->ReleaseStringUTFChars(key, keyString);
|
|
||||||
env->ReleaseStringUTFChars(value, valueString);
|
|
||||||
env->DeleteLocalRef(entry);
|
|
||||||
env->DeleteLocalRef(key);
|
|
||||||
env->DeleteLocalRef(value);
|
|
||||||
}
|
|
||||||
result.append("]");
|
|
||||||
}
|
|
||||||
env->ReleaseStringUTFChars(type, typeString);
|
|
||||||
}
|
|
||||||
|
|
||||||
env->DeleteLocalRef(List);
|
|
||||||
env->DeleteLocalRef(Map);
|
|
||||||
env->DeleteLocalRef(setClass);
|
|
||||||
env->DeleteLocalRef(entryClass);
|
|
||||||
return env->NewStringUTF(result.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jlong JNICALL
|
JNIEXPORT jlong JNICALL
|
||||||
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz,
|
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz,
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jstring JNICALL
|
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库成功~");
|
return env->NewStringUTF("加载Shamrock库成功~");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ package moe.fuqiuluo.shamrock
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@ -64,7 +65,9 @@ import androidx.compose.ui.unit.sp
|
|||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import moe.fuqiuluo.shamrock.tools.GlobalUi
|
||||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
||||||
import moe.fuqiuluo.shamrock.ui.app.Logger
|
import moe.fuqiuluo.shamrock.ui.app.Logger
|
||||||
import moe.fuqiuluo.shamrock.ui.app.RuntimeState
|
import moe.fuqiuluo.shamrock.ui.app.RuntimeState
|
||||||
@ -85,8 +88,16 @@ import moe.fuqiuluo.shamrock.ui.tools.getShamrockVersion
|
|||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while (true) {
|
||||||
|
delay(5_000) // Delay in milliseconds
|
||||||
|
broadcastToModule {
|
||||||
|
putExtra("__cmd", "switch_status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalIndication provides NoIndication
|
LocalIndication provides NoIndication
|
||||||
) {
|
) {
|
||||||
@ -96,8 +107,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
isAppearanceLightStatusBars = true
|
isAppearanceLightStatusBars = true
|
||||||
}
|
}
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||||
broadcastToModule { putExtra("__cmd", "fetchPort") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GlobalUi = Handler(mainLooper)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +165,7 @@ private fun AppMainView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val ctx = LocalContext.current
|
val ctx = LocalContext.current
|
||||||
LaunchedEffect(isFined.value) {
|
LaunchedEffect(isFined) {
|
||||||
if (isFined.value) {
|
if (isFined.value) {
|
||||||
AppRuntime.log(LocalString.logCentralLoadSuccessfully)
|
AppRuntime.log(LocalString.logCentralLoadSuccessfully)
|
||||||
Toast.makeText(ctx, LocalString.frameworkYes, Toast.LENGTH_SHORT).show()
|
Toast.makeText(ctx, LocalString.frameworkYes, Toast.LENGTH_SHORT).show()
|
||||||
@ -284,58 +296,11 @@ private fun AnimatedTab(
|
|||||||
val lastSelectedState = remember {
|
val lastSelectedState = remember {
|
||||||
mutableIntStateOf(0)
|
mutableIntStateOf(0)
|
||||||
}
|
}
|
||||||
val enter = remember {
|
|
||||||
scaleIn(animationSpec = TweenSpec(150, easing = FastOutLinearInEasing))
|
|
||||||
}
|
|
||||||
val exit = remember {
|
|
||||||
scaleOut(animationSpec = TweenSpec(150, easing = FastOutSlowInEasing))
|
|
||||||
}
|
|
||||||
|
|
||||||
val defaultConst = SELECTED_TABLE[index * 2]
|
val defaultConst = SELECTED_TABLE[index * 2]
|
||||||
val selectedConst = SELECTED_TABLE[(index * 2) + 1]
|
val selectedConst = SELECTED_TABLE[(index * 2) + 1]
|
||||||
val isFirst: Boolean = (lastSelectedState.value and defaultConst) != defaultConst
|
val isFirst: Boolean = (lastSelectedState.value and defaultConst) != defaultConst
|
||||||
|
|
||||||
var icon: @Composable (() -> Unit)? = null
|
|
||||||
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(
|
ShamrockTab(
|
||||||
selected = curSelected,
|
selected = curSelected,
|
||||||
onClick = {
|
onClick = {
|
||||||
@ -343,11 +308,13 @@ private fun AnimatedTab(
|
|||||||
state.scrollToPage(index, 0f)
|
state.scrollToPage(index, 0f)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
text = text,
|
|
||||||
icon = icon,
|
|
||||||
selectedContentColor = Color.Transparent,
|
selectedContentColor = Color.Transparent,
|
||||||
unselectedContentColor = Color.Transparent,
|
unselectedContentColor = Color.Transparent,
|
||||||
indication = null
|
indication = null,
|
||||||
|
titleWithIcon = titleWithIcon,
|
||||||
|
visibleState = MutableTransitionState(false).also {
|
||||||
|
it.targetState = isFirst || lastSelectedState.value and selectedConst == selectedConst
|
||||||
|
}
|
||||||
)
|
)
|
||||||
lastSelectedState.value.let {
|
lastSelectedState.value.let {
|
||||||
var tmp = it
|
var tmp = it
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.app.config
|
||||||
|
|
@ -0,0 +1,33 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.app.config
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import moe.fuqiuluo.shamrock.config.ConfigKey
|
||||||
|
import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule
|
||||||
|
|
||||||
|
object ShamrockConfig {
|
||||||
|
internal fun getConfigPref(ctx: Context) = ctx.getSharedPreferences("config", 0)
|
||||||
|
|
||||||
|
internal inline operator fun <reified Type> get(ctx: Context, key: ConfigKey<Type>): Type {
|
||||||
|
val preferences = getConfigPref(ctx)
|
||||||
|
return when(Type::class) {
|
||||||
|
Int::class -> preferences.getInt(key.name(), key.default() as Int) as Type
|
||||||
|
Long::class -> preferences.getLong(key.name(), key.default() as Long) as Type
|
||||||
|
String::class -> preferences.getString(key.name(), key.default() as String) as Type
|
||||||
|
Boolean::class -> preferences.getBoolean(key.name(), key.default() as Boolean) as Type
|
||||||
|
else -> throw IllegalArgumentException("Unsupported type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal inline operator fun <reified Type> set(ctx: Context, key: ConfigKey<Type>, value: Type) {
|
||||||
|
val preferences = getConfigPref(ctx)
|
||||||
|
val editor = preferences.edit()
|
||||||
|
when(Type::class) {
|
||||||
|
Int::class -> editor.putInt(key.name(), value as Int)
|
||||||
|
Long::class -> editor.putLong(key.name(), value as Long)
|
||||||
|
String::class -> editor.putString(key.name(), value as String)
|
||||||
|
Boolean::class -> editor.putBoolean(key.name(), value as Boolean)
|
||||||
|
else -> throw IllegalArgumentException("Unsupported type")
|
||||||
|
}
|
||||||
|
editor.apply()
|
||||||
|
}
|
||||||
|
}
|
@ -1,395 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.ui.app
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule
|
|
||||||
|
|
||||||
object ShamrockConfig {
|
|
||||||
fun getSSLKeyPath(ctx: Context): String {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getString("key_store", "")!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSSLKeyPath(ctx: Context, path: String) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putString("key_store", path).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSSLPort(ctx: Context): Int {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getInt("ssl_port", 5701)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSSLPort(ctx: Context, port: Int) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putInt("ssl_port", port).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSSLAlias(ctx: Context): String {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getString("ssl_alias", "")!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSSLAlias(ctx: Context, alias: String) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putString("ssl_alias", alias).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSSLPwd(ctx: Context): String {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getString("ssl_pwd", "")!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSSLPwd(ctx: Context, alias: String) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putString("ssl_pwd", alias).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getSSLPrivatePwd(ctx: Context): String {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getString("ssl_private_pwd", "")!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSSLPrivatePwd(ctx: Context, alias: String) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putString("ssl_private_pwd", alias).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHttpAddr(ctx: Context): String {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getString("http_addr", "")!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setHttpAddr(ctx: Context, v: String) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putString("http_addr", v).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isPro(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("pro_api", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setPro(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("pro_api", v).apply()
|
|
||||||
ctx.broadcastToModule {
|
|
||||||
putExtra("type", "restart")
|
|
||||||
putExtra("__cmd", "change_port")
|
|
||||||
}
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getToken(ctx: Context): String {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getString("token", null) ?: ""
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setToken(ctx: Context, v: String?) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putString("token", v).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isWs(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("ws", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setWs(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("ws", v).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isWsClient(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("ws_client", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setWsClient(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("ws_client", v).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isTablet(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("tablet", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTablet(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("tablet", v).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isUseCQCode(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("use_cqcode", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setUseCQCode(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("use_cqcode", v).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isWebhook(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("webhook", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setWebhook(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("webhook", v).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getWsAddr(ctx: Context): String {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getString("ws_addr", "")!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setWsAddr(ctx: Context, v: String) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putString("ws_addr", v).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUploadResourceGroup(ctx: Context): String {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getString("up_res_group", "100000000")!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setUploadResourceGroup(ctx: Context, v: String) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putString("up_res_group", v).apply()
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHttpPort(ctx: Context): Int {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getInt("port", 5700)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setHttpPort(ctx: Context, v: Int) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putInt("port", v).apply()
|
|
||||||
ctx.broadcastToModule {
|
|
||||||
putExtra("type", "port")
|
|
||||||
putExtra("port", v)
|
|
||||||
putExtra("__cmd", "change_port")
|
|
||||||
}
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getWsPort(ctx: Context): Int {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getInt("ws_port", 5800)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setWsPort(ctx: Context, v: Int) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putInt("ws_port", v).apply()
|
|
||||||
ctx.broadcastToModule {
|
|
||||||
putExtra("type", "ws_port")
|
|
||||||
putExtra("port", v)
|
|
||||||
putExtra("__cmd", "change_port")
|
|
||||||
}
|
|
||||||
pushUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun is2B(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("2B", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun set2B(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("2B", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAutoClean(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("auto_clear", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isAutoClean(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("auto_clear", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isDebug(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("debug", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDebug(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("debug", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isAntiTrace(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("anti_qq_trace", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isForbidUselessProcess(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("forbid_useless_process", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setForbidUselessProcess(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("forbid_useless_process", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAntiTrace(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("anti_qq_trace", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isInjectPacket(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("inject_packet", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setInjectPacket(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("inject_packet", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun enableAutoStart(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("enable_auto_start", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun disableAutoSyncSetting(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("disable_auto_sync_setting", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun enableAliveReply(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("alive_reply", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun allowShell(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("shell", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAutoStart(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("enable_auto_start", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDisableAutoSyncSetting(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("disable_auto_sync_setting", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAliveReply(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("alive_reply", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setShellStatus(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("shell", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun enableSelfMsg(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("enable_self_msg", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun enableOldBDH(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("enable_old_bdh", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setEnableOldBDH(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("enable_old_bdh", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun enableSyncMsgAsSentMsg(ctx: Context): Boolean {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return preferences.getBoolean("enable_sync_msg_as_sent_msg", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setEnableSelfMsg(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("enable_self_msg", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setEnableSyncMsgAsSentMsg(ctx: Context, v: Boolean) {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
preferences.edit().putBoolean("enable_sync_msg_as_sent_msg", v).apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getConfigMap(ctx: Context): Map<String, Any?> {
|
|
||||||
val preferences = ctx.getSharedPreferences("config", 0)
|
|
||||||
return mapOf(
|
|
||||||
"tablet" to preferences.getBoolean("tablet", false),
|
|
||||||
"port" to preferences.getInt("port", 5700),
|
|
||||||
"ws" to preferences.getBoolean("ws", false),
|
|
||||||
"ws_port" to preferences.getInt("ws_port", 5800),
|
|
||||||
"ssl_port" to preferences.getInt("ssl_port", 5701),
|
|
||||||
"http" to preferences.getBoolean("webhook", false),
|
|
||||||
"http_addr" to preferences.getString("http_addr", ""),
|
|
||||||
"ws_client" to preferences.getBoolean("ws_client", false),
|
|
||||||
"use_cqcode" to preferences.getBoolean("use_cqcode", false),
|
|
||||||
"ws_addr" to preferences.getString("ws_addr", ""),
|
|
||||||
"ssl_alias" to preferences.getString("ssl_alias", ""),
|
|
||||||
"pro_api" to preferences.getBoolean("pro_api", false),
|
|
||||||
"token" to preferences.getString("token", null),
|
|
||||||
"ssl_pwd" to preferences.getString("ssl_pwd", ""),
|
|
||||||
"inject_packet" to preferences.getBoolean("inject_packet", false),
|
|
||||||
"debug" to preferences.getBoolean("debug", false),
|
|
||||||
"anti_qq_trace" to preferences.getBoolean("anti_qq_trace", true),
|
|
||||||
"ssl_private_pwd" to preferences.getString("ssl_private_pwd", ""),
|
|
||||||
"key_store" to preferences.getString("key_store", ""),
|
|
||||||
"enable_self_msg" to preferences.getBoolean("enable_self_msg", false),
|
|
||||||
"echo_number" to preferences.getBoolean("echo_number", false),
|
|
||||||
"shell" to preferences.getBoolean("shell", false),
|
|
||||||
"alive_reply" to preferences.getBoolean("alive_reply", false),
|
|
||||||
"enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false),
|
|
||||||
"disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false),
|
|
||||||
"forbid_useless_process" to preferences.getBoolean("forbid_useless_process", false),
|
|
||||||
"enable_old_bdh" to preferences.getBoolean("enable_old_bdh", false),
|
|
||||||
"up_res_group" to preferences.getString("up_res_group", ""),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun pushUpdate(ctx: Context) {
|
|
||||||
ctx.broadcastToModule {
|
|
||||||
getConfigMap(ctx).forEach { (key, value) ->
|
|
||||||
if (value == null) {
|
|
||||||
val v: String? = null
|
|
||||||
this.putExtra(key, v)
|
|
||||||
} else {
|
|
||||||
when (value) {
|
|
||||||
is Int -> this.putExtra(key, value)
|
|
||||||
is Long -> this.putExtra(key, value)
|
|
||||||
is Short -> this.putExtra(key, value)
|
|
||||||
is Byte -> this.putExtra(key, value)
|
|
||||||
is String -> this.putExtra(key, value)
|
|
||||||
is ByteArray -> this.putExtra(key, value)
|
|
||||||
is Boolean -> this.putExtra(key, value)
|
|
||||||
is Float -> this.putExtra(key, value)
|
|
||||||
is Double -> this.putExtra(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
putExtra("__cmd", "push_config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,7 +5,6 @@ import android.content.ClipData
|
|||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
@ -25,6 +24,7 @@ import androidx.compose.material3.Divider
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -45,10 +45,12 @@ import coil.compose.rememberAsyncImagePainter
|
|||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import moe.fuqiuluo.shamrock.R
|
import moe.fuqiuluo.shamrock.R
|
||||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
||||||
import moe.fuqiuluo.shamrock.ui.app.Level
|
import moe.fuqiuluo.shamrock.ui.app.Level
|
||||||
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig
|
import moe.fuqiuluo.shamrock.app.config.ShamrockConfig
|
||||||
|
import moe.fuqiuluo.shamrock.config.*
|
||||||
import moe.fuqiuluo.shamrock.ui.theme.GlobalColor
|
import moe.fuqiuluo.shamrock.ui.theme.GlobalColor
|
||||||
import moe.fuqiuluo.shamrock.ui.theme.LocalString
|
import moe.fuqiuluo.shamrock.ui.theme.LocalString
|
||||||
import moe.fuqiuluo.shamrock.ui.theme.ThemeColor
|
import moe.fuqiuluo.shamrock.ui.theme.ThemeColor
|
||||||
@ -72,110 +74,6 @@ fun DashboardFragment(
|
|||||||
InformationCard(ctx)
|
InformationCard(ctx)
|
||||||
APIInfoCard(ctx)
|
APIInfoCard(ctx)
|
||||||
FunctionCard(scope, ctx, LocalString.functionSetting)
|
FunctionCard(scope, ctx, LocalString.functionSetting)
|
||||||
SSLCard(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SSLCard(ctx: Context) {
|
|
||||||
ActionBox(
|
|
||||||
modifier = Modifier.padding(top = 12.dp),
|
|
||||||
painter = painterResource(id = R.drawable.baseline_security_24),
|
|
||||||
title = LocalString.sslSetting
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
Divider(
|
|
||||||
modifier = Modifier,
|
|
||||||
color = GlobalColor.Divider,
|
|
||||||
thickness = 0.2.dp
|
|
||||||
)
|
|
||||||
|
|
||||||
val sslPort = remember { mutableStateOf(ShamrockConfig.getSSLPort(ctx).toString()) }
|
|
||||||
TextItem(
|
|
||||||
title = "SSL端口",
|
|
||||||
desc = "端口范围在0~65565,并确保可用。",
|
|
||||||
text = sslPort,
|
|
||||||
hint = "请输入端口号",
|
|
||||||
error = "端口范围应在0~65565",
|
|
||||||
checker = {
|
|
||||||
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false)
|
|
||||||
},
|
|
||||||
confirm = {
|
|
||||||
val newPort = sslPort.value.toInt()
|
|
||||||
ShamrockConfig.setSSLPort(ctx, newPort)
|
|
||||||
AppRuntime.log("设置SSL(HTTP)端口为$newPort,立即生效尝试中。")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val keyStore = remember { mutableStateOf(ShamrockConfig.getSSLKeyPath(ctx)) }
|
|
||||||
TextItem(
|
|
||||||
title = "SSL证书",
|
|
||||||
desc = "BKS签名的证书。",
|
|
||||||
text = keyStore,
|
|
||||||
hint = "输入证书路径",
|
|
||||||
error = "证书路径不合法或不存在",
|
|
||||||
checker = {
|
|
||||||
it.isNotBlank()
|
|
||||||
},
|
|
||||||
confirm = {
|
|
||||||
val new = keyStore.value
|
|
||||||
ShamrockConfig.setSSLKeyPath(ctx, new)
|
|
||||||
AppRuntime.log("设置SSL证书为[$new]。")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val alias = remember { mutableStateOf(ShamrockConfig.getSSLAlias(ctx)) }
|
|
||||||
TextItem(
|
|
||||||
title = "SSL别名",
|
|
||||||
desc = "BKS签名的别名,确保大小写区分正确。",
|
|
||||||
text = alias,
|
|
||||||
hint = "输入签名别名",
|
|
||||||
error = "别名不合法",
|
|
||||||
checker = {
|
|
||||||
it.isNotBlank()
|
|
||||||
},
|
|
||||||
confirm = {
|
|
||||||
val new = alias.value
|
|
||||||
ShamrockConfig.setSSLAlias(ctx, new)
|
|
||||||
AppRuntime.log("设置SSL别名为[$new]。")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val sslPwd = remember { mutableStateOf(ShamrockConfig.getSSLPwd(ctx)) }
|
|
||||||
TextItem(
|
|
||||||
title = "SSL密码",
|
|
||||||
desc = "BKS签名的密码。",
|
|
||||||
text = sslPwd,
|
|
||||||
hint = "输入签名密码",
|
|
||||||
error = "密码不合法",
|
|
||||||
checker = {
|
|
||||||
it.isNotBlank()
|
|
||||||
},
|
|
||||||
confirm = {
|
|
||||||
val new = sslPwd.value
|
|
||||||
ShamrockConfig.setSSLPwd(ctx, new)
|
|
||||||
AppRuntime.log("设置SSL密码为[$new]。")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val sslPrivatePwd = remember { mutableStateOf(ShamrockConfig.getSSLPrivatePwd(ctx)) }
|
|
||||||
TextItem(
|
|
||||||
title = "SSL Private密码",
|
|
||||||
desc = "BKS签名的Private密码。",
|
|
||||||
text = sslPrivatePwd,
|
|
||||||
hint = "输入Private密码",
|
|
||||||
error = "密码不合法",
|
|
||||||
checker = {
|
|
||||||
it.isNotBlank()
|
|
||||||
},
|
|
||||||
confirm = {
|
|
||||||
val new = sslPrivatePwd.value
|
|
||||||
ShamrockConfig.setSSLPrivatePwd(ctx, new)
|
|
||||||
AppRuntime.log("设置SSL Private密码为[$new]。")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,93 +93,35 @@ private fun APIInfoCard(
|
|||||||
thickness = 0.2.dp
|
thickness = 0.2.dp
|
||||||
)
|
)
|
||||||
|
|
||||||
val wsPort = remember { mutableStateOf(ShamrockConfig.getWsPort(ctx).toString()) }
|
val rpcPort = remember { mutableStateOf(ShamrockConfig[ctx, RPCPort].toString()) }
|
||||||
val port = remember { mutableStateOf(ShamrockConfig.getHttpPort(ctx).toString()) }
|
|
||||||
TextItem(
|
TextItem(
|
||||||
title = "主动HTTP端口",
|
title = "RPC服务端口",
|
||||||
desc = "端口范围在0~65565,并确保可用。",
|
desc = "端口范围在0~65565,并确保可用。",
|
||||||
text = port,
|
text = rpcPort,
|
||||||
hint = "请输入端口号",
|
hint = "请输入端口号",
|
||||||
error = "端口范围应在0~65565",
|
error = "端口范围应在0~65565",
|
||||||
checker = {
|
checker = {
|
||||||
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false) && wsPort.value != it
|
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }
|
||||||
|
.getOrDefault(false) && rpcPort.value != it
|
||||||
},
|
},
|
||||||
confirm = {
|
confirm = {
|
||||||
val newPort = port.value.toInt()
|
val newPort = rpcPort.value.toInt()
|
||||||
ShamrockConfig.setHttpPort(ctx, newPort)
|
ShamrockConfig[ctx, RPCPort] = newPort
|
||||||
AppRuntime.log("设置主动HTTP监听端口为$newPort,立即生效尝试中。")
|
AppRuntime.log("设置主动HTTP监听端口为$newPort,立即生效尝试中。")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val rpcAddress = remember { mutableStateOf(ShamrockConfig[ctx, RPCAddress]) }
|
||||||
TextItem(
|
TextItem(
|
||||||
title = "主动WebSocket端口",
|
title = "回调RPC地址",
|
||||||
desc = "端口范围在0~65565,并确保可用。",
|
desc = "例如:kritor.support:8081",
|
||||||
text = wsPort,
|
text = rpcAddress,
|
||||||
hint = "请输入端口号",
|
|
||||||
error = "端口范围应在0~65565",
|
|
||||||
checker = {
|
|
||||||
it.isNotBlank() && kotlin.runCatching { it.toInt() in 0..65565 }.getOrDefault(false) && it != port.value
|
|
||||||
},
|
|
||||||
confirm = {
|
|
||||||
val newPort = wsPort.value.toInt()
|
|
||||||
ShamrockConfig.setWsPort(ctx, newPort)
|
|
||||||
AppRuntime.log("设置主动WebSocket监听端口为$newPort。")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val webHookAddress = remember { mutableStateOf(ShamrockConfig.getHttpAddr(ctx)) }
|
|
||||||
TextItem(
|
|
||||||
title = "回调HTTP地址",
|
|
||||||
desc = "例如:http://shamrock.moe:80。",
|
|
||||||
text = webHookAddress,
|
|
||||||
hint = "请输入回调地址",
|
hint = "请输入回调地址",
|
||||||
error = "输入的地址不合法",
|
error = "输入的地址不合法",
|
||||||
checker = {
|
|
||||||
it.isNotBlank()
|
|
||||||
},
|
|
||||||
confirm = {
|
|
||||||
if (it.startsWith("http://") || it.startsWith("https://")) {
|
|
||||||
ShamrockConfig.setHttpAddr(ctx, webHookAddress.value)
|
|
||||||
AppRuntime.log("设置回调HTTP地址为[${webHookAddress.value}]。")
|
|
||||||
} else {
|
|
||||||
Toast.makeText(ctx, "回调地址不合法", Toast.LENGTH_SHORT).show()
|
|
||||||
webHookAddress.value = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val wsAddress = remember { mutableStateOf(ShamrockConfig.getWsAddr(ctx)) }
|
|
||||||
TextItem(
|
|
||||||
title = "被动WebSocket地址",
|
|
||||||
desc = "例如:ws://shamrock.moe:81,多个使用逗号分隔。",
|
|
||||||
text = wsAddress,
|
|
||||||
hint = "请输入被动地址",
|
|
||||||
error = "输入的地址不合法",
|
|
||||||
checker = {
|
|
||||||
true
|
|
||||||
},
|
|
||||||
confirm = {
|
|
||||||
if (it.startsWith("ws://") || it.startsWith("wss://") || it.isBlank()) {
|
|
||||||
ShamrockConfig.setWsAddr(ctx, wsAddress.value)
|
|
||||||
AppRuntime.log("设置被动WebSocket地址为[${wsAddress.value}]。")
|
|
||||||
} else {
|
|
||||||
Toast.makeText(ctx, "被动WebSocket地址不合法", Toast.LENGTH_SHORT).show()
|
|
||||||
wsAddress.value = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val authToken = remember { mutableStateOf(ShamrockConfig.getToken(ctx)) }
|
|
||||||
TextItem(
|
|
||||||
title = "鉴权Token",
|
|
||||||
desc = "用于鉴权的Token。",
|
|
||||||
text = authToken,
|
|
||||||
hint = "请填写鉴权token",
|
|
||||||
error = "输入的参数不合法",
|
|
||||||
checker = { true },
|
checker = { true },
|
||||||
confirm = {
|
confirm = {
|
||||||
ShamrockConfig.setToken(ctx, authToken.value)
|
ShamrockConfig[ctx, RPCAddress] = rpcAddress.value
|
||||||
AppRuntime.log("设置鉴权Token为[${authToken.value}]。")
|
AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -314,50 +154,32 @@ private fun FunctionCard(
|
|||||||
Function(
|
Function(
|
||||||
title = "强制平板模式",
|
title = "强制平板模式",
|
||||||
desc = "强制QQ使用平板模式,实现共存登录。",
|
desc = "强制QQ使用平板模式,实现共存登录。",
|
||||||
isSwitch = ShamrockConfig.isTablet(ctx)
|
isSwitch = ShamrockConfig[ctx, ForceTablet]
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.setTablet(ctx, it)
|
ShamrockConfig[ctx, ForceTablet] = it
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
Function(
|
Function(
|
||||||
title = "HTTP回调",
|
title = "主动RPC",
|
||||||
desc = "OneBot标准的HTTPAPI回调,Shamrock作为Client。",
|
desc = "Kritor协议实现RPC",
|
||||||
isSwitch = ShamrockConfig.isWebhook(ctx)
|
isSwitch = ShamrockConfig[ctx, ActiveRPC]
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.setWebhook(ctx, it)
|
ShamrockConfig[ctx, ActiveRPC] = it
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
Function(
|
Function(
|
||||||
title = "消息格式为CQ码",
|
title = "被动RPC",
|
||||||
desc = "HTTPAPI回调的消息格式,关闭则为消息段。",
|
desc = "Kritor协议实现RPC",
|
||||||
isSwitch = ShamrockConfig.isUseCQCode(ctx)
|
isSwitch = ShamrockConfig[ctx, PassiveRPC]
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.setUseCQCode(ctx, it)
|
ShamrockConfig[ctx, PassiveRPC] = it
|
||||||
return@Function true
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
|
||||||
title = "主动WebSocket",
|
|
||||||
desc = "OneBot标准WebSocket,Shamrock作为Server。",
|
|
||||||
isSwitch = ShamrockConfig.isWs(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setWs(ctx, it)
|
|
||||||
return@Function true
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
|
||||||
title = "被动WebSocket",
|
|
||||||
desc = "OneBot标准WebSocket,Shamrock作为Client。",
|
|
||||||
isSwitch = ShamrockConfig.isWsClient(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setWsClient(ctx, it)
|
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
run {
|
run {
|
||||||
val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig.getUploadResourceGroup(ctx)) }
|
val uploadResourceGroup = remember { mutableStateOf(ShamrockConfig[ctx, ResourceGroup]) }
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.absolutePadding(left = 8.dp, right = 8.dp, top = 12.dp, bottom = 0.dp)
|
.absolutePadding(left = 8.dp, right = 8.dp, top = 12.dp, bottom = 0.dp)
|
||||||
@ -380,23 +202,11 @@ private fun FunctionCard(
|
|||||||
},
|
},
|
||||||
confirm = {
|
confirm = {
|
||||||
val groupId = uploadResourceGroup.value
|
val groupId = uploadResourceGroup.value
|
||||||
ShamrockConfig.setUploadResourceGroup(ctx, groupId)
|
ShamrockConfig[ctx, ResourceGroup] = groupId
|
||||||
AppRuntime.log("设置接受资源群聊为[$groupId]。")
|
AppRuntime.log("设置接受资源群聊为[$groupId]。")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Function(
|
|
||||||
title = "专业级接口",
|
|
||||||
desc = "如果你不知道你在做什么,请不要开启本功能。",
|
|
||||||
descColor = Color.Red,
|
|
||||||
isSwitch = ShamrockConfig.isPro(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setPro(ctx, it)
|
|
||||||
AppRuntime.log("专业级API = $it", Level.WARN)
|
|
||||||
return@Function true
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,14 @@ import androidx.compose.ui.unit.sp
|
|||||||
import moe.fuqiuluo.shamrock.R
|
import moe.fuqiuluo.shamrock.R
|
||||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
||||||
import moe.fuqiuluo.shamrock.ui.app.Level
|
import moe.fuqiuluo.shamrock.ui.app.Level
|
||||||
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig
|
import moe.fuqiuluo.shamrock.app.config.ShamrockConfig
|
||||||
|
import moe.fuqiuluo.shamrock.config.AliveReply
|
||||||
|
import moe.fuqiuluo.shamrock.config.AntiJvmTrace
|
||||||
|
import moe.fuqiuluo.shamrock.config.B2Mode
|
||||||
|
import moe.fuqiuluo.shamrock.config.DebugMode
|
||||||
|
import moe.fuqiuluo.shamrock.config.EnableOldBDH
|
||||||
|
import moe.fuqiuluo.shamrock.config.EnableSelfMessage
|
||||||
|
import moe.fuqiuluo.shamrock.ui.service.handlers.InitHandler
|
||||||
import moe.fuqiuluo.shamrock.ui.theme.GlobalColor
|
import moe.fuqiuluo.shamrock.ui.theme.GlobalColor
|
||||||
import moe.fuqiuluo.shamrock.ui.theme.LocalString
|
import moe.fuqiuluo.shamrock.ui.theme.LocalString
|
||||||
import moe.fuqiuluo.shamrock.ui.tools.NoticeTextDialog
|
import moe.fuqiuluo.shamrock.ui.tools.NoticeTextDialog
|
||||||
@ -68,9 +75,9 @@ fun LabFragment() {
|
|||||||
title = LocalString.b2Mode,
|
title = LocalString.b2Mode,
|
||||||
desc = LocalString.b2ModeDesc,
|
desc = LocalString.b2ModeDesc,
|
||||||
descColor = it,
|
descColor = it,
|
||||||
isSwitch = ShamrockConfig.is2B(ctx)
|
isSwitch = ShamrockConfig[ctx, B2Mode]
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.set2B(ctx, it)
|
ShamrockConfig[ctx, B2Mode] = it
|
||||||
scope.toast(ctx, LocalString.restartToast)
|
scope.toast(ctx, LocalString.restartToast)
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
@ -79,10 +86,10 @@ fun LabFragment() {
|
|||||||
title = LocalString.showDebugLog,
|
title = LocalString.showDebugLog,
|
||||||
desc = LocalString.showDebugLogDesc,
|
desc = LocalString.showDebugLogDesc,
|
||||||
descColor = it,
|
descColor = it,
|
||||||
isSwitch = ShamrockConfig.isDebug(ctx)
|
isSwitch = ShamrockConfig[ctx, DebugMode]
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.setDebug(ctx, it)
|
ShamrockConfig[ctx, DebugMode] = it
|
||||||
ShamrockConfig.pushUpdate(ctx)
|
InitHandler.update(ctx)
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -100,54 +107,13 @@ fun LabFragment() {
|
|||||||
thickness = 0.2.dp
|
thickness = 0.2.dp
|
||||||
)
|
)
|
||||||
|
|
||||||
Function(
|
|
||||||
title = "禁止无用进程",
|
|
||||||
desc = "禁止QQ生成无用进程浪费内存,可能造成部分功能闪退。",
|
|
||||||
descColor = color,
|
|
||||||
isSwitch = ShamrockConfig.isForbidUselessProcess(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setForbidUselessProcess(ctx, it)
|
|
||||||
ShamrockConfig.pushUpdate(ctx)
|
|
||||||
return@Function true
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
Function(
|
||||||
title = "自回复测试",
|
title = "自回复测试",
|
||||||
desc = "发送[ping],机器人发送一个具有调试信息的返回。",
|
desc = "发送[ping],机器人发送一个具有调试信息的返回。",
|
||||||
descColor = color,
|
descColor = color,
|
||||||
isSwitch = ShamrockConfig.enableAliveReply(ctx)
|
isSwitch = ShamrockConfig[ctx, AliveReply]
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.setAliveReply(ctx, it)
|
ShamrockConfig[ctx, AliveReply] = it
|
||||||
return@Function true
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
|
||||||
title = "开启Shell接口",
|
|
||||||
desc = "可能导致设备被入侵,请勿随意开启。",
|
|
||||||
descColor = color,
|
|
||||||
isSwitch = ShamrockConfig.allowShell(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setShellStatus(ctx, it)
|
|
||||||
return@Function true
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
|
||||||
title = "自动唤醒QQ",
|
|
||||||
desc = "QQ进程死亡时重新打开QQ进程,前提本进程存活。",
|
|
||||||
descColor = color,
|
|
||||||
isSwitch = ShamrockConfig.enableAutoStart(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setAutoStart(ctx, it)
|
|
||||||
return@Function true
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
|
||||||
title = "禁止Shamrock同步设置",
|
|
||||||
desc = "禁止Shamrock同步设置,防止恢复手动修改后的配置文件。",
|
|
||||||
descColor = color,
|
|
||||||
isSwitch = ShamrockConfig.disableAutoSyncSetting(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setDisableAutoSyncSetting(ctx, it)
|
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -194,25 +160,14 @@ fun LabFragment() {
|
|||||||
thickness = 0.2.dp
|
thickness = 0.2.dp
|
||||||
)
|
)
|
||||||
|
|
||||||
Function(
|
|
||||||
title = LocalString.injectPacket,
|
|
||||||
desc = LocalString.injectPacketDesc,
|
|
||||||
descColor = color,
|
|
||||||
isSwitch = ShamrockConfig.isInjectPacket(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setInjectPacket(ctx, it)
|
|
||||||
ShamrockConfig.pushUpdate(ctx)
|
|
||||||
return@Function true
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
Function(
|
||||||
title = LocalString.antiTrace,
|
title = LocalString.antiTrace,
|
||||||
desc = LocalString.antiTraceDesc,
|
desc = LocalString.antiTraceDesc,
|
||||||
descColor = color,
|
descColor = color,
|
||||||
isSwitch = ShamrockConfig.isAntiTrace(ctx)
|
isSwitch = ShamrockConfig[ctx, AntiJvmTrace]
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.setAntiTrace(ctx, it)
|
ShamrockConfig[ctx, AntiJvmTrace] = it
|
||||||
ShamrockConfig.pushUpdate(ctx)
|
scope.toast(ctx, LocalString.restartToast)
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,21 +232,10 @@ fun LabFragment() {
|
|||||||
title = "自发消息推送",
|
title = "自发消息推送",
|
||||||
desc = "推送Bot发送的消息,未做特殊处理请勿打开。",
|
desc = "推送Bot发送的消息,未做特殊处理请勿打开。",
|
||||||
descColor = it,
|
descColor = it,
|
||||||
isSwitch = ShamrockConfig.enableSelfMsg(ctx)
|
isSwitch = ShamrockConfig[ctx, EnableSelfMessage]
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.setEnableSelfMsg(ctx, it)
|
ShamrockConfig[ctx, EnableSelfMessage] = it
|
||||||
ShamrockConfig.pushUpdate(ctx)
|
InitHandler.update(ctx)
|
||||||
return@Function true
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
|
||||||
title = "同步消息推送类型异换",
|
|
||||||
desc = "推送来自同号异设备消息,将同步消息作为自发消息推送。",
|
|
||||||
descColor = it,
|
|
||||||
isSwitch = ShamrockConfig.enableSyncMsgAsSentMsg(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setEnableSyncMsgAsSentMsg(ctx, it)
|
|
||||||
ShamrockConfig.pushUpdate(ctx)
|
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,10 +243,10 @@ fun LabFragment() {
|
|||||||
title = "启用旧版资源上传系统",
|
title = "启用旧版资源上传系统",
|
||||||
desc = "如果NT内核无法上传资源,请打开本开关。",
|
desc = "如果NT内核无法上传资源,请打开本开关。",
|
||||||
descColor = it,
|
descColor = it,
|
||||||
isSwitch = ShamrockConfig.enableOldBDH(ctx)
|
isSwitch = ShamrockConfig[ctx, EnableOldBDH]
|
||||||
) {
|
) {
|
||||||
ShamrockConfig.setEnableOldBDH(ctx, it)
|
ShamrockConfig[ctx, EnableOldBDH] = it
|
||||||
ShamrockConfig.pushUpdate(ctx)
|
InitHandler.update(ctx)
|
||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,108 +0,0 @@
|
|||||||
@file:OptIn(DelicateCoroutinesApi::class)
|
|
||||||
package moe.fuqiuluo.shamrock.ui.service
|
|
||||||
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
|
||||||
import io.ktor.client.request.get
|
|
||||||
import io.ktor.client.request.parameter
|
|
||||||
import io.ktor.client.request.url
|
|
||||||
import io.ktor.client.statement.bodyAsText
|
|
||||||
import io.ktor.http.HttpStatusCode
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.CommonResult
|
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.CurrentAccount
|
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.Status
|
|
||||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
|
||||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime.AccountInfo
|
|
||||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime.log
|
|
||||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime.state
|
|
||||||
import moe.fuqiuluo.shamrock.ui.app.Level
|
|
||||||
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig
|
|
||||||
import moe.fuqiuluo.shamrock.ui.service.internal.broadcastToModule
|
|
||||||
import java.net.ConnectException
|
|
||||||
import java.util.Timer
|
|
||||||
import kotlin.concurrent.timer
|
|
||||||
|
|
||||||
object DashboardInitializer {
|
|
||||||
private var servicePort: Int = 0
|
|
||||||
private lateinit var heartbeatTimer: Timer
|
|
||||||
|
|
||||||
operator fun invoke(context: Context, port: Int) {
|
|
||||||
servicePort = port
|
|
||||||
initHeartbeat(true, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initHeartbeat(reload: Boolean, context: Context) {
|
|
||||||
if (::heartbeatTimer.isInitialized && !reload) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (::heartbeatTimer.isInitialized) {
|
|
||||||
heartbeatTimer.cancel()
|
|
||||||
}
|
|
||||||
heartbeatTimer = timer("heartbeat", false, 0, 1000L * 30) {
|
|
||||||
checkService(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkService(context: Context) {
|
|
||||||
GlobalScope.launch {
|
|
||||||
try {
|
|
||||||
GlobalClient.get {
|
|
||||||
url("http://127.0.0.1:$servicePort/get_account_info")
|
|
||||||
val token = ShamrockConfig.getToken(context)
|
|
||||||
if (token.isNotBlank()) {
|
|
||||||
//header("Authorization", "Bearer $token")
|
|
||||||
parameter("access_token", token)
|
|
||||||
}
|
|
||||||
}.let {
|
|
||||||
if (it.status == HttpStatusCode.OK) {
|
|
||||||
val result: CommonResult<CurrentAccount> = Json.decodeFromString(it.bodyAsText())
|
|
||||||
state.isFined.value = result.retcode == 0
|
|
||||||
if (result.retcode == Status.InternalHandlerError.code) {
|
|
||||||
log("账号未登录。", Level.WARN)
|
|
||||||
} else if (result.retcode != 0) {
|
|
||||||
log("尝试从接口获取账号信息失败,未知错误。", Level.ERROR)
|
|
||||||
} else {
|
|
||||||
AccountInfo.let { account ->
|
|
||||||
account.uin.value = result.data.uin.toString()
|
|
||||||
account.nick.value = result.data.nick
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
state.isFined.value = false
|
|
||||||
log("尝试从接口获取账号信息失败,服务运行异常。", Level.ERROR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: ConnectException) {
|
|
||||||
state.isFined.value = false
|
|
||||||
context.broadcastToModule {
|
|
||||||
putExtra("__cmd", "checkAndStartService")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ShamrockConfig.enableAutoStart(context)) {
|
|
||||||
log("检测到Service死亡,正在尝试重新启动!")
|
|
||||||
GlobalScope.launch(Dispatchers.Main) {
|
|
||||||
val packageName = "com.tencent.mobileqq"
|
|
||||||
val className = "com.tencent.mobileqq.activity.SplashActivity"
|
|
||||||
|
|
||||||
val intent = Intent()
|
|
||||||
intent.component = ComponentName(packageName, className)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
intent.putExtra("from", "shamrock")
|
|
||||||
startActivity(context, intent, Bundle.EMPTY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
state.isFined.value = false
|
|
||||||
log(e.stackTraceToString(), Level.ERROR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.ui.service.handlers
|
|
||||||
|
|
||||||
import android.content.ContentValues
|
|
||||||
import android.content.Context
|
|
||||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
|
||||||
import moe.fuqiuluo.shamrock.ui.service.DashboardInitializer
|
|
||||||
|
|
||||||
object FetchPortHandler: ModuleHandler() {
|
|
||||||
override val cmd: String = "success"
|
|
||||||
|
|
||||||
override fun onReceive(callbackId: Int, values: ContentValues, context: Context) {
|
|
||||||
AppRuntime.state.supportVoice.value = values.getAsBoolean("voice")
|
|
||||||
DashboardInitializer(context, values.getAsInteger("port"))
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,13 +3,37 @@ package moe.fuqiuluo.shamrock.ui.service.handlers
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
||||||
import moe.fuqiuluo.shamrock.ui.app.ShamrockConfig
|
import moe.fuqiuluo.shamrock.app.config.ShamrockConfig
|
||||||
|
import moe.fuqiuluo.shamrock.config.*
|
||||||
|
|
||||||
internal object InitHandler: ModuleHandler() {
|
internal object InitHandler: ModuleHandler() {
|
||||||
override val cmd: String = "init"
|
override val cmd: String = "init"
|
||||||
|
|
||||||
override fun onReceive(callbackId: Int, values: ContentValues, context: Context) {
|
override fun onReceive(callbackId: Int, values: ContentValues, context: Context) {
|
||||||
|
update(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(context: Context) {
|
||||||
AppRuntime.log("推送QQ进程初始化设置数据包成功...")
|
AppRuntime.log("推送QQ进程初始化设置数据包成功...")
|
||||||
callback(context, callbackId, ShamrockConfig.getConfigMap(context))
|
|
||||||
|
val maps = hashMapOf<String, Any?>()
|
||||||
|
|
||||||
|
ActiveRPC.update(context, maps)
|
||||||
|
AliveReply.update(context, maps)
|
||||||
|
AntiJvmTrace.update(context, maps)
|
||||||
|
DebugMode.update(context, maps)
|
||||||
|
EnableOldBDH.update(context, maps)
|
||||||
|
EnableSelfMessage.update(context, maps)
|
||||||
|
ForceTablet.update(context, maps)
|
||||||
|
PassiveRPC.update(context, maps)
|
||||||
|
ResourceGroup.update(context, maps)
|
||||||
|
RPCAddress.update(context, maps)
|
||||||
|
RPCPort.update(context, maps)
|
||||||
|
|
||||||
|
callback(context, 1, maps)
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> ConfigKey<T>.update(context: Context, map: HashMap<String, Any?>) {
|
||||||
|
map[name()] = ShamrockConfig[context, this]
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -29,6 +29,7 @@ abstract class ModuleHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
putExtra("__cmd", cmd)
|
||||||
putExtra("__hash", callbackId)
|
putExtra("__hash", callbackId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
package moe.fuqiuluo.shamrock.ui.service.handlers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.widget.Toast
|
||||||
|
import moe.fuqiuluo.shamrock.tools.GlobalUi
|
||||||
|
import moe.fuqiuluo.shamrock.ui.app.AppRuntime
|
||||||
|
import java.util.Timer
|
||||||
|
import kotlin.concurrent.timer
|
||||||
|
import kotlin.concurrent.timerTask
|
||||||
|
|
||||||
|
object SwitchStatus: ModuleHandler() {
|
||||||
|
override val cmd: String
|
||||||
|
get() = "switch_status"
|
||||||
|
|
||||||
|
private var lastActiveTime = 0L
|
||||||
|
private var timer: Timer? = null
|
||||||
|
|
||||||
|
override fun onReceive(callbackId: Int, values: ContentValues, context: Context) {
|
||||||
|
val voiceSwitch = values.getAsBoolean("voice")
|
||||||
|
val nickname = values.getAsString("nickname")
|
||||||
|
val account = values.getAsString("account")
|
||||||
|
if (lastActiveTime == 0L) GlobalUi.post {
|
||||||
|
Toast.makeText(context, "激活成功", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
AppRuntime.state.apply {
|
||||||
|
isFined.value = true
|
||||||
|
coreVersion.value = values.getAsString("core_version")
|
||||||
|
coreName.value = "LSPosed"
|
||||||
|
supportVoice.value = voiceSwitch
|
||||||
|
}
|
||||||
|
AppRuntime.AccountInfo.apply {
|
||||||
|
uin.value = account
|
||||||
|
nick.value = nickname
|
||||||
|
}
|
||||||
|
lastActiveTime = System.currentTimeMillis()
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTimer() {
|
||||||
|
timer?.cancel()
|
||||||
|
timer = timer("SwitchStatus", true, 0, 5_000) {
|
||||||
|
if (lastActiveTime != 0L && System.currentTimeMillis() - lastActiveTime > 30 * 1000) {
|
||||||
|
AppRuntime.state.isFined.value = false
|
||||||
|
lastActiveTime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,9 +5,9 @@ import android.content.ContentValues
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
|
||||||
import moe.fuqiuluo.shamrock.ui.service.ModuleTalker
|
import moe.fuqiuluo.shamrock.ui.service.ModuleTalker
|
||||||
import moe.fuqiuluo.shamrock.ui.service.handlers.*
|
import moe.fuqiuluo.shamrock.ui.service.handlers.*
|
||||||
|
import android.net.Uri
|
||||||
|
|
||||||
class MultifunctionalProvider: ContentProvider() {
|
class MultifunctionalProvider: ContentProvider() {
|
||||||
override fun insert(uri: Uri, content: ContentValues?): Uri {
|
override fun insert(uri: Uri, content: ContentValues?): Uri {
|
||||||
@ -28,8 +28,8 @@ class MultifunctionalProvider: ContentProvider() {
|
|||||||
|
|
||||||
override fun onCreate(): Boolean {
|
override fun onCreate(): Boolean {
|
||||||
ModuleTalker.register(InitHandler)
|
ModuleTalker.register(InitHandler)
|
||||||
ModuleTalker.register(FetchPortHandler)
|
|
||||||
ModuleTalker.register(LogHandler)
|
ModuleTalker.register(LogHandler)
|
||||||
|
ModuleTalker.register(SwitchStatus)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +58,7 @@ class MultifunctionalProvider: ContentProvider() {
|
|||||||
|
|
||||||
inline fun Context.broadcastToModule(intentBuilder: Intent.() -> Unit) {
|
inline fun Context.broadcastToModule(intentBuilder: Intent.() -> Unit) {
|
||||||
val intent = Intent()
|
val intent = Intent()
|
||||||
intent.action = "moe.fuqiuluo.xqbot.dynamic"
|
intent.action = "moe.fuqiuluo.kritor.dynamic"
|
||||||
intent.intentBuilder()
|
intent.intentBuilder()
|
||||||
sendBroadcast(intent)
|
sendBroadcast(intent)
|
||||||
}
|
}
|
@ -1,23 +1,35 @@
|
|||||||
package moe.fuqiuluo.shamrock.ui.tools
|
package moe.fuqiuluo.shamrock.ui.tools
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColor
|
import androidx.compose.animation.animateColor
|
||||||
|
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
|
import androidx.compose.animation.core.TweenSpec
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.core.updateTransition
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.animation.scaleIn
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.foundation.Indication
|
import androidx.compose.foundation.Indication
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.indication
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ProvideTextStyle
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
@ -31,8 +43,10 @@ import androidx.compose.ui.layout.LastBaseline
|
|||||||
import androidx.compose.ui.layout.Layout
|
import androidx.compose.ui.layout.Layout
|
||||||
import androidx.compose.ui.layout.Placeable
|
import androidx.compose.ui.layout.Placeable
|
||||||
import androidx.compose.ui.layout.layoutId
|
import androidx.compose.ui.layout.layoutId
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Density
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@ -135,20 +149,18 @@ private fun TabBaselineLayout(
|
|||||||
text: @Composable (() -> Unit)?,
|
text: @Composable (() -> Unit)?,
|
||||||
icon: @Composable (() -> Unit)?
|
icon: @Composable (() -> Unit)?
|
||||||
) {
|
) {
|
||||||
Layout(
|
Layout({
|
||||||
{
|
if (text != null) {
|
||||||
if (text != null) {
|
Box(
|
||||||
Box(
|
Modifier
|
||||||
Modifier
|
.layoutId("text")
|
||||||
.layoutId("text")
|
.padding(horizontal = HorizontalTextPadding)
|
||||||
.padding(horizontal = HorizontalTextPadding)
|
) { text() }
|
||||||
) { text() }
|
|
||||||
}
|
|
||||||
if (icon != null) {
|
|
||||||
Box(Modifier.layoutId("icon")) { icon() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
) { measurables, constraints ->
|
if (icon != null) {
|
||||||
|
Box(Modifier.layoutId("icon")) { icon() }
|
||||||
|
}
|
||||||
|
}) { measurables, constraints ->
|
||||||
val textPlaceable = text?.let {
|
val textPlaceable = text?.let {
|
||||||
measurables.first { it.layoutId == "text" }.measure(
|
measurables.first { it.layoutId == "text" }.measure(
|
||||||
// Measure with loose constraints for height as we don't want the text to take up more
|
// Measure with loose constraints for height as we don't want the text to take up more
|
||||||
@ -247,21 +259,66 @@ fun ShamrockTab(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
text: @Composable (() -> Unit)? = null,
|
|
||||||
icon: @Composable (() -> Unit)? = null,
|
|
||||||
selectedContentColor: Color = GlobalColor.TabSelected,
|
selectedContentColor: Color = GlobalColor.TabSelected,
|
||||||
unselectedContentColor: Color = selectedContentColor,
|
unselectedContentColor: Color = selectedContentColor,
|
||||||
indication: Indication? = rememberRipple(bounded = true, color = selectedContentColor),
|
indication: Indication? = rememberRipple(bounded = true, color = selectedContentColor),
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
titleWithIcon: Pair<String, Int>,
|
||||||
|
visibleState: MutableTransitionState<Boolean>
|
||||||
) {
|
) {
|
||||||
val styledText: @Composable (() -> Unit)? = text?.let {
|
var text: @Composable (() -> Unit)? = null
|
||||||
@Composable {
|
var icon: @Composable (() -> Unit)? = null
|
||||||
val style =
|
|
||||||
MaterialTheme.typography.fromToken(PrimaryNavigationTabTokens.LabelTextFont)
|
if (!selected) {
|
||||||
.copy(textAlign = TextAlign.Center)
|
icon = {
|
||||||
ProvideTextStyle(style, content = text)
|
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(
|
ShamrockTab(
|
||||||
selected,
|
selected,
|
||||||
onClick,
|
onClick,
|
||||||
@ -272,7 +329,10 @@ fun ShamrockTab(
|
|||||||
interactionSource,
|
interactionSource,
|
||||||
indication
|
indication
|
||||||
) {
|
) {
|
||||||
TabBaselineLayout(icon = icon, text = styledText)
|
TabBaselineLayout(
|
||||||
|
icon = icon,
|
||||||
|
text = text
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application") version "8.2.1" apply false
|
id("com.android.application") version "8.2.0" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
id("org.jetbrains.kotlin.android") version "1.9.22" apply false
|
||||||
id("com.android.library") version "8.2.1" apply false
|
id("com.android.library") version "8.2.0" apply false
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,6 @@ val DEPENDENCY_ANDROIDX = arrayOf(
|
|||||||
"androidx.activity:activity-compose:1.7.2",
|
"androidx.activity:activity-compose:1.7.2",
|
||||||
)
|
)
|
||||||
|
|
||||||
const val DEPENDENCY_JSON5K = "io.github.xn32:json5k:0.3.0"
|
|
||||||
const val DEPENDENCY_PROTOBUF = "com.google.protobuf:protobuf-java:3.24.0"
|
|
||||||
const val DEPENDENCY_JAVA_WEBSOCKET = "org.java-websocket:Java-WebSocket:1.5.4"
|
|
||||||
|
|
||||||
fun room(name: String) = "androidx.room:room-$name:${Versions.roomVersion}"
|
fun room(name: String) = "androidx.room:room-$name:${Versions.roomVersion}"
|
||||||
|
|
||||||
fun kotlinx(name: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$name:$version"
|
fun kotlinx(name: String, version: String) = "org.jetbrains.kotlinx:kotlinx-$name:$version"
|
||||||
|
@ -1,85 +0,0 @@
|
|||||||
@file:OptIn(KspExperimental::class)
|
|
||||||
@file:Suppress("LocalVariableName", "UNCHECKED_CAST")
|
|
||||||
|
|
||||||
package moe.fuqiuluo.ksp.impl
|
|
||||||
|
|
||||||
import com.google.devtools.ksp.KspExperimental
|
|
||||||
import com.google.devtools.ksp.getAnnotationsByType
|
|
||||||
import com.google.devtools.ksp.getClassDeclarationByName
|
|
||||||
import com.google.devtools.ksp.getKotlinClassByName
|
|
||||||
import com.google.devtools.ksp.processing.CodeGenerator
|
|
||||||
import com.google.devtools.ksp.processing.Dependencies
|
|
||||||
import com.google.devtools.ksp.processing.KSPLogger
|
|
||||||
import com.google.devtools.ksp.processing.Resolver
|
|
||||||
import com.google.devtools.ksp.processing.SymbolProcessor
|
|
||||||
import com.google.devtools.ksp.symbol.ClassKind
|
|
||||||
import com.google.devtools.ksp.symbol.KSAnnotated
|
|
||||||
import com.google.devtools.ksp.symbol.KSClassDeclaration
|
|
||||||
import com.google.devtools.ksp.symbol.KSVisitorVoid
|
|
||||||
import com.google.devtools.ksp.validate
|
|
||||||
import com.squareup.kotlinpoet.FileSpec
|
|
||||||
import com.squareup.kotlinpoet.FunSpec
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
class OneBotHandlerProcessor(
|
|
||||||
private val codeGenerator: CodeGenerator,
|
|
||||||
private val logger: KSPLogger
|
|
||||||
): SymbolProcessor {
|
|
||||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
|
||||||
val ActionManagerNode = resolver.getClassDeclarationByName("moe.fuqiuluo.shamrock.remote.action.ActionManager")
|
|
||||||
?: resolver.getKotlinClassByName("moe.fuqiuluo.shamrock.remote.action.ActionManager")
|
|
||||||
?: resolver.getClassDeclarationByName("ActionManager")
|
|
||||||
val symbols = resolver.getSymbolsWithAnnotation(OneBotHandler::class.qualifiedName!!)
|
|
||||||
val unableToProcess = symbols.filterNot { it.validate() }
|
|
||||||
if (ActionManagerNode != null) {
|
|
||||||
val oneBotHandlers = (symbols.filter {
|
|
||||||
it is KSClassDeclaration && it.validate() && it.classKind == ClassKind.OBJECT
|
|
||||||
} as Sequence<KSClassDeclaration>).toList()
|
|
||||||
|
|
||||||
if (oneBotHandlers.isNotEmpty()) {
|
|
||||||
ActionManagerNode.accept(ActionManagerVisitor(oneBotHandlers), Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return unableToProcess.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ActionManagerVisitor(
|
|
||||||
private val actionHandlers: List<KSClassDeclaration>
|
|
||||||
): KSVisitorVoid() {
|
|
||||||
override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
|
|
||||||
val packageName = classDeclaration.packageName.asString()
|
|
||||||
|
|
||||||
// generate kotlin `init { }`
|
|
||||||
val fileSpec = FileSpec.builder(packageName, classDeclaration.qualifiedName?.asString() ?: run {
|
|
||||||
throw IllegalStateException("ActionManagerVisitor: classDeclaration.qualifiedName is null")
|
|
||||||
}).addFunction(FunSpec.builder("initManager").apply {
|
|
||||||
actionHandlers.forEach { handler ->
|
|
||||||
// fetch the params of the annotation
|
|
||||||
val annotation = handler.getAnnotationsByType(OneBotHandler::class).first()
|
|
||||||
val actionName = annotation.actionName
|
|
||||||
val alias = annotation.alias
|
|
||||||
alias.forEach { name ->
|
|
||||||
addStatement("actionMap[\"$name\"] = ${handler.simpleName.asString()}")
|
|
||||||
}
|
|
||||||
addStatement("actionMap[\"$actionName\"] = ${handler.simpleName.asString()}")
|
|
||||||
}
|
|
||||||
}.build()).apply {
|
|
||||||
addImport("moe.fuqiuluo.shamrock.remote.action.ActionManager", "actionMap")
|
|
||||||
actionHandlers.forEach {
|
|
||||||
addImport(it.packageName.asString(), it.simpleName.asString())
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
codeGenerator.createNewFile(
|
|
||||||
dependencies = Dependencies(aggregating = false),
|
|
||||||
packageName = packageName,
|
|
||||||
fileName = "Auto" + classDeclaration.simpleName.asString()
|
|
||||||
).use { outputStream ->
|
|
||||||
outputStream.writer().use {
|
|
||||||
fileSpec.writeTo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
// IByteData.aidl
|
|
||||||
package moe.fuqiuluo.shamrock.xposed.ipc.bytedata;
|
|
||||||
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.ipc.bytedata.IByteDataSign;
|
|
||||||
|
|
||||||
interface IByteData {
|
|
||||||
IByteDataSign sign(String uin, String data, in byte[] salt);
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
// IByteDataSign.aidl
|
|
||||||
package moe.fuqiuluo.shamrock.xposed.ipc.bytedata;
|
|
||||||
|
|
||||||
parcelable IByteDataSign;
|
|
@ -1,4 +0,0 @@
|
|||||||
// IQSign.aidl
|
|
||||||
package moe.fuqiuluo.shamrock.xposed.ipc.qsign;
|
|
||||||
|
|
||||||
parcelable IQSign;
|
|
@ -1,14 +0,0 @@
|
|||||||
// IQSigner.aidl
|
|
||||||
package moe.fuqiuluo.shamrock.xposed.ipc.qsign;
|
|
||||||
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.ipc.qsign.IQSign;
|
|
||||||
|
|
||||||
interface IQSigner {
|
|
||||||
IQSign sign(String cmd, int seq, String uin, in byte[] buffer);
|
|
||||||
|
|
||||||
byte[] energy(String module, in byte[] salt);
|
|
||||||
|
|
||||||
byte[] xwDebugId(String uin, String start, String end);
|
|
||||||
|
|
||||||
List<String> getCmdWhiteList();
|
|
||||||
}
|
|
@ -1,201 +0,0 @@
|
|||||||
@file:OptIn(DelicateCoroutinesApi::class)
|
|
||||||
|
|
||||||
package moe.fuqiuluo.qqinterface.servlet
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.tencent.mobileqq.app.QQAppInterface
|
|
||||||
import com.tencent.mobileqq.msf.core.MsfCore
|
|
||||||
import com.tencent.mobileqq.pb.ByteStringMicro
|
|
||||||
import com.tencent.qphone.base.remote.ToServiceMsg
|
|
||||||
import io.ktor.utils.io.core.BytePacketBuilder
|
|
||||||
import io.ktor.utils.io.core.readBytes
|
|
||||||
import io.ktor.utils.io.core.writeFully
|
|
||||||
import io.ktor.utils.io.core.writeInt
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import kotlinx.serialization.encodeToByteArray
|
|
||||||
|
|
||||||
import moe.fuqiuluo.shamrock.tools.slice
|
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
|
||||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest
|
|
||||||
import protobuf.oidb.TrpcOidb
|
|
||||||
import mqq.app.MobileQQ
|
|
||||||
import protobuf.auto.toByteArray
|
|
||||||
import tencent.im.oidb.oidb_sso
|
|
||||||
import kotlin.coroutines.CoroutineContext
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
internal abstract class BaseSvc {
|
|
||||||
companion object Default: CoroutineScope {
|
|
||||||
val currentUin: String
|
|
||||||
get() = app.currentAccountUin
|
|
||||||
|
|
||||||
val app: QQAppInterface
|
|
||||||
get() = AppRuntimeFetcher.appRuntime as QQAppInterface
|
|
||||||
|
|
||||||
fun createToServiceMsg(cmd: String): ToServiceMsg {
|
|
||||||
return ToServiceMsg("mobileqq.service", app.currentAccountUin, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendOidbAW(cmd: String, cmdId: Int, serviceId: Int, data: ByteArray, trpc: Boolean = false, timeout: Long = 5000L): ByteArray? {
|
|
||||||
val seq = MsfCore.getNextSeq()
|
|
||||||
val buffer = withTimeoutOrNull(timeout) {
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
|
||||||
launch(Dispatchers.Default) {
|
|
||||||
DynamicReceiver.register(IPCRequest(cmd, seq) {
|
|
||||||
val buffer = it.getByteArrayExtra("buffer")!!
|
|
||||||
continuation.resume(buffer)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (trpc) sendTrpcOidb(cmd, cmdId, serviceId, data, seq)
|
|
||||||
else sendOidb(cmd, cmdId, serviceId, data, seq)
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
if (it == null)
|
|
||||||
DynamicReceiver.unregister(seq)
|
|
||||||
}?.copyOf()
|
|
||||||
try {
|
|
||||||
if (buffer != null && buffer.size >= 5 && buffer[4] == 120.toByte()) {
|
|
||||||
val builder = BytePacketBuilder()
|
|
||||||
val deBuffer = DeflateTools.uncompress(buffer.slice(4))
|
|
||||||
builder.writeInt(deBuffer.size)
|
|
||||||
builder.writeFully(deBuffer)
|
|
||||||
return builder.build().readBytes()
|
|
||||||
}
|
|
||||||
} catch (_: Exception) { }
|
|
||||||
return buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendBufferAW(cmd: String, isPb: Boolean, data: ByteArray, timeout: Long = 5000L): ByteArray? {
|
|
||||||
val seq = MsfCore.getNextSeq()
|
|
||||||
val buffer = withTimeoutOrNull<ByteArray?>(timeout) {
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
|
||||||
launch(Dispatchers.Default) {
|
|
||||||
DynamicReceiver.register(IPCRequest(cmd, seq) {
|
|
||||||
val buffer = it.getByteArrayExtra("buffer")!!
|
|
||||||
continuation.resume(buffer)
|
|
||||||
})
|
|
||||||
sendBuffer(cmd, isPb, data, seq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
if (it == null)
|
|
||||||
DynamicReceiver.unregister(seq)
|
|
||||||
}?.copyOf()
|
|
||||||
try {
|
|
||||||
if (buffer != null && buffer.size >= 5 && buffer[4] == 120.toByte()) {
|
|
||||||
val builder = BytePacketBuilder()
|
|
||||||
val deBuffer = DeflateTools.uncompress(buffer.slice(4))
|
|
||||||
builder.writeInt(deBuffer.size)
|
|
||||||
builder.writeFully(deBuffer)
|
|
||||||
return builder.build().readBytes()
|
|
||||||
}
|
|
||||||
} catch (_: Exception) { }
|
|
||||||
return buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendOidb(cmd: String, cmdId: Int, serviceId: Int, buffer: ByteArray, seq: Int = -1, trpc: Boolean = false) {
|
|
||||||
if (trpc) {
|
|
||||||
sendTrpcOidb(cmd, cmdId, serviceId, buffer, seq)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val to = createToServiceMsg(cmd)
|
|
||||||
val oidb = oidb_sso.OIDBSSOPkg()
|
|
||||||
oidb.uint32_command.set(cmdId)
|
|
||||||
oidb.uint32_service_type.set(serviceId)
|
|
||||||
oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(buffer))
|
|
||||||
oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext()))
|
|
||||||
to.putWupBuffer(oidb.toByteArray())
|
|
||||||
to.addAttribute("req_pb_protocol_flag", true)
|
|
||||||
if (seq != -1) {
|
|
||||||
to.addAttribute("shamrock_seq", seq)
|
|
||||||
}
|
|
||||||
app.sendToService(to)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendTrpcOidb(cmd: String, cmdId: Int, serviceId: Int, buffer: ByteArray, seq: Int = -1) {
|
|
||||||
val to = createToServiceMsg(cmd)
|
|
||||||
|
|
||||||
val oidb = TrpcOidb(
|
|
||||||
cmd = cmdId,
|
|
||||||
service = serviceId,
|
|
||||||
buffer = buffer,
|
|
||||||
flag = 1
|
|
||||||
)
|
|
||||||
to.putWupBuffer(oidb.toByteArray())
|
|
||||||
|
|
||||||
to.addAttribute("req_pb_protocol_flag", true)
|
|
||||||
if (seq != -1) {
|
|
||||||
to.addAttribute("shamrock_seq", seq)
|
|
||||||
}
|
|
||||||
app.sendToService(to)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendBuffer(cmd: String, isPb: Boolean, buffer: ByteArray, seq: Int = MsfCore.getNextSeq()) {
|
|
||||||
val toServiceMsg = ToServiceMsg("mobileqq.service", app.currentUin, cmd)
|
|
||||||
toServiceMsg.putWupBuffer(buffer)
|
|
||||||
toServiceMsg.addAttribute("req_pb_protocol_flag", isPb)
|
|
||||||
toServiceMsg.addAttribute("shamrock_seq", seq)
|
|
||||||
app.sendToService(toServiceMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
override val coroutineContext: CoroutineContext by lazy {
|
|
||||||
Dispatchers.IO.limitedParallelism(12)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun send(toServiceMsg: ToServiceMsg) {
|
|
||||||
app.sendToService(toServiceMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected suspend fun sendAW(toServiceMsg: ToServiceMsg, timeout: Long = 5000L): ByteArray? {
|
|
||||||
val seq = MsfCore.getNextSeq()
|
|
||||||
val buffer = withTimeoutOrNull<ByteArray?>(timeout) {
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
|
||||||
launch(Dispatchers.Default) {
|
|
||||||
DynamicReceiver.register(IPCRequest(toServiceMsg.serviceCmd, seq) {
|
|
||||||
val buffer = it.getByteArrayExtra("buffer")!!
|
|
||||||
continuation.resume(buffer)
|
|
||||||
})
|
|
||||||
toServiceMsg.addAttribute("shamrock_seq", seq)
|
|
||||||
send(toServiceMsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.also {
|
|
||||||
if (it == null) DynamicReceiver.unregister(seq)
|
|
||||||
}?.copyOf()
|
|
||||||
try {
|
|
||||||
if (buffer != null && buffer.size >= 5 && buffer[4] == 120.toByte()) {
|
|
||||||
val builder = BytePacketBuilder()
|
|
||||||
val deBuffer = DeflateTools.uncompress(buffer.slice(4))
|
|
||||||
builder.writeInt(deBuffer.size)
|
|
||||||
builder.writeFully(deBuffer)
|
|
||||||
return builder.build().readBytes()
|
|
||||||
}
|
|
||||||
} catch (_: Exception) { }
|
|
||||||
return buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun sendExtra(cmd: String, builder: (Bundle) -> Unit) {
|
|
||||||
val toServiceMsg = createToServiceMsg(cmd)
|
|
||||||
builder(toServiceMsg.extraData)
|
|
||||||
app.sendToService(toServiceMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun sendPb(cmd: String, buffer: ByteArray, seq: Int) {
|
|
||||||
val toServiceMsg = createToServiceMsg(cmd)
|
|
||||||
toServiceMsg.putWupBuffer(buffer)
|
|
||||||
toServiceMsg.addAttribute("req_pb_protocol_flag", true)
|
|
||||||
toServiceMsg.addAttribute("shamrock_seq", seq)
|
|
||||||
app.sendToService(toServiceMsg)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet
|
|
||||||
|
|
||||||
import VIP.GetCustomOnlineStatusReq
|
|
||||||
import VIP.GetCustomOnlineStatusRsp
|
|
||||||
import com.qq.jce.wup.UniPacket
|
|
||||||
import com.tencent.mobileqq.data.Card
|
|
||||||
import com.tencent.mobileqq.profilecard.api.IProfileDataService
|
|
||||||
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
|
|
||||||
import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver
|
|
||||||
import io.ktor.client.request.header
|
|
||||||
import io.ktor.client.request.post
|
|
||||||
import io.ktor.client.request.setBody
|
|
||||||
import io.ktor.client.statement.bodyAsText
|
|
||||||
import io.ktor.http.ContentType.Application.Json
|
|
||||||
import io.ktor.http.contentType
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
|
||||||
import moe.fuqiuluo.shamrock.tools.json
|
|
||||||
import moe.fuqiuluo.shamrock.tools.slice
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import mqq.app.Packet
|
|
||||||
import tencent.im.oidb.cmd0x11b2.oidb_0x11b2
|
|
||||||
import tencent.im.oidb.oidb_sso
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
internal object CardSvc: BaseSvc() {
|
|
||||||
private val GetModelShowLock by lazy {
|
|
||||||
Mutex()
|
|
||||||
}
|
|
||||||
private val refreshCardLock by lazy {
|
|
||||||
Mutex()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getModelShow(uin: Long = app.longAccountUin): String {
|
|
||||||
return GetModelShowLock.withLock {
|
|
||||||
val uniPacket = UniPacket()
|
|
||||||
uniPacket.servantName = "VIP.CustomOnlineStatusServer.CustomOnlineStatusObj"
|
|
||||||
uniPacket.funcName = "GetCustomOnlineStatus"
|
|
||||||
val getCustomOnlineStatusReq = GetCustomOnlineStatusReq()
|
|
||||||
getCustomOnlineStatusReq.lUin = uin
|
|
||||||
getCustomOnlineStatusReq.sIMei = ""
|
|
||||||
uniPacket.put("req", getCustomOnlineStatusReq)
|
|
||||||
|
|
||||||
val resp = sendBufferAW("VipCustom.GetCustomOnlineStatus", false, uniPacket.encode())
|
|
||||||
?: error("unable to fetch contact model_show")
|
|
||||||
Packet.decodePacket(resp, "rsp", GetCustomOnlineStatusRsp()).sBuffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setModelShow(model: String, manu: String, modelShow: String, imei: String, show: Boolean) {
|
|
||||||
val cookie = TicketSvc.getCookie("vip.qq.com")
|
|
||||||
val csrf = TicketSvc.getCSRF(TicketSvc.getUin(), "vip.qq.com")
|
|
||||||
val p4token = TicketSvc.getPt4Token(TicketSvc.getUin(), "vip.qq.com") ?: ""
|
|
||||||
GlobalClient.post("https://club.vip.qq.com/srf-cgi-node?srfname=VIP.CustomOnlineStatusServer.CustomOnlineStatusObj.SetCustomOnlineStatus&ts=${System.currentTimeMillis()}&daid=18&g_tk=$csrf&pt4_token=$p4token") {
|
|
||||||
header("Cookie", cookie)
|
|
||||||
contentType(Json)
|
|
||||||
setBody(mapOf(
|
|
||||||
"servicesName" to "VIP.CustomOnlineStatusServer.CustomOnlineStatusObj",
|
|
||||||
"cmd" to "SetCustomOnlineStatus",
|
|
||||||
"args" to listOf(mapOf(
|
|
||||||
"sIMei" to imei,
|
|
||||||
"sModel" to model,
|
|
||||||
"sManu" to manu,
|
|
||||||
"lUin" to app.currentUin.toLong(),
|
|
||||||
"bShowInfo" to show,
|
|
||||||
"sModelShow" to modelShow
|
|
||||||
))
|
|
||||||
).json.toString())
|
|
||||||
}.bodyAsText().let {
|
|
||||||
LogCenter.log({ "setModelShow() => $it" }, Level.DEBUG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getSharePrivateArkMsg(peerId: Long): String {
|
|
||||||
val reqBody = oidb_0x11b2.BusinessCardV3Req()
|
|
||||||
reqBody.uin.set(peerId)
|
|
||||||
reqBody.jump_url.set("mqqapi://card/show_pslcard?src_type=internal&source=sharecard&version=1&uin=$peerId")
|
|
||||||
|
|
||||||
val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11ca_0", 4790, 0, reqBody.toByteArray())
|
|
||||||
?: error("unable to fetch contact ark_json_text")
|
|
||||||
val body = oidb_sso.OIDBSSOPkg()
|
|
||||||
body.mergeFrom(buffer.slice(4))
|
|
||||||
val rsp = oidb_0x11b2.BusinessCardV3Rsp()
|
|
||||||
rsp.mergeFrom(body.bytes_bodybuffer.get().toByteArray())
|
|
||||||
return rsp.signed_ark_msg.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getProfileCard(uin: Long): Result<Card> {
|
|
||||||
return getProfileCardFromCache(uin).onFailure {
|
|
||||||
return refreshAndGetProfileCard(uin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getProfileCardFromCache(uin: Long): Result<Card> {
|
|
||||||
val profileDataService = app
|
|
||||||
.getRuntimeService(IProfileDataService::class.java, "all")
|
|
||||||
val card = profileDataService.getProfileCard(uin.toString(), true)
|
|
||||||
return if (card == null || card.strNick.isNullOrEmpty()) {
|
|
||||||
Result.failure(Exception("unable to fetch profile card"))
|
|
||||||
} else {
|
|
||||||
Result.success(card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun refreshAndGetProfileCard(uin: Long): Result<Card> {
|
|
||||||
val dataService = app
|
|
||||||
.getRuntimeService(IProfileDataService::class.java, "all")
|
|
||||||
val card = refreshCardLock.withLock {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
app.addObserver(object: ProfileCardObserver() {
|
|
||||||
override fun onGetProfileCard(success: Boolean, obj: Any) {
|
|
||||||
app.removeObserver(this)
|
|
||||||
if (!success || obj !is Card) {
|
|
||||||
it.resume(null)
|
|
||||||
} else {
|
|
||||||
dataService.saveProfileCard(obj)
|
|
||||||
it.resume(obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
app.getRuntimeService(IProfileProtocolService::class.java, "all")
|
|
||||||
.requestProfileCard(app.currentUin, uin.toString(), 12, 0L, 0.toByte(), 0L, 0L, null, "", 0L, 10004, null, 0.toByte())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (card == null || card.strNick.isNullOrEmpty()) {
|
|
||||||
Result.failure(Exception("unable to fetch profile card"))
|
|
||||||
} else {
|
|
||||||
Result.success(card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet
|
|
||||||
|
|
||||||
import kotlinx.serialization.encodeToByteArray
|
|
||||||
import protobuf.auto.toByteArray
|
|
||||||
|
|
||||||
import protobuf.oidb.cmd0x9082.Oidb0x9082
|
|
||||||
|
|
||||||
internal object ChatSvc: BaseSvc() {
|
|
||||||
fun setGroupMessageCommentFace(peer: Long, msgSeq: ULong, faceIndex: String, isSet: Boolean) {
|
|
||||||
val serviceId = if (isSet) 1 else 2
|
|
||||||
sendOidb("OidbSvcTrpcTcp.0x9082_$serviceId", 36994, serviceId, Oidb0x9082(
|
|
||||||
peer = peer.toULong(),
|
|
||||||
msgSeq = msgSeq,
|
|
||||||
faceIndex = faceIndex,
|
|
||||||
flag = 1u,
|
|
||||||
u1 = 0u,
|
|
||||||
u2 = 0u
|
|
||||||
).toByteArray())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,248 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet
|
|
||||||
|
|
||||||
import com.tencent.mobileqq.pb.ByteStringMicro
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.structures.*
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
|
|
||||||
import moe.fuqiuluo.shamrock.tools.slice
|
|
||||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
|
||||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
|
||||||
import protobuf.oidb.cmd0x6d7.CreateFolderReq
|
|
||||||
import protobuf.oidb.cmd0x6d7.DeleteFolderReq
|
|
||||||
import protobuf.oidb.cmd0x6d7.MoveFolderReq
|
|
||||||
import protobuf.oidb.cmd0x6d7.Oidb0x6d7ReqBody
|
|
||||||
import protobuf.oidb.cmd0x6d7.Oidb0x6d7RespBody
|
|
||||||
import protobuf.oidb.cmd0x6d7.RenameFolderReq
|
|
||||||
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
|
|
||||||
import tencent.im.oidb.cmd0x6d8.oidb_0x6d8
|
|
||||||
import tencent.im.oidb.oidb_sso
|
|
||||||
import protobuf.group_file_common.FolderInfo as GroupFileCommonFolderInfo
|
|
||||||
import protobuf.auto.toByteArray
|
|
||||||
|
|
||||||
internal object FileSvc: BaseSvc() {
|
|
||||||
suspend fun createFileFolder(groupId: Long, folderName: String, parentFolderId: String = "/"): Result<GroupFileCommonFolderInfo> {
|
|
||||||
val data = Oidb0x6d7ReqBody(
|
|
||||||
createFolder = CreateFolderReq(
|
|
||||||
groupCode = groupId.toULong(),
|
|
||||||
appId = 3u,
|
|
||||||
parentFolderId = parentFolderId,
|
|
||||||
folderName = folderName
|
|
||||||
)
|
|
||||||
).toByteArray()
|
|
||||||
val resultBuffer = sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data)
|
|
||||||
?: return Result.failure(Exception("unable to fetch result"))
|
|
||||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
|
||||||
oidbPkg.mergeFrom(resultBuffer.slice(4))
|
|
||||||
val rsp = oidbPkg.bytes_bodybuffer.get()
|
|
||||||
.toByteArray()
|
|
||||||
.decodeProtobuf<Oidb0x6d7RespBody>()
|
|
||||||
if (rsp.createFolder?.retCode != 0) {
|
|
||||||
return Result.failure(Exception("unable to create folder: ${rsp.createFolder?.retCode}"))
|
|
||||||
}
|
|
||||||
return Result.success(rsp.createFolder!!.folderInfo!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun deleteGroupFolder(groupId: Long, folderUid: String): Boolean {
|
|
||||||
val buffer = sendOidbAW("OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody(
|
|
||||||
deleteFolder = DeleteFolderReq(
|
|
||||||
groupCode = groupId.toULong(),
|
|
||||||
appId = 3u,
|
|
||||||
folderId = folderUid
|
|
||||||
)
|
|
||||||
).toByteArray()) ?: return false
|
|
||||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
|
||||||
oidbPkg.mergeFrom(buffer.slice(4))
|
|
||||||
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
|
|
||||||
return rsp.deleteFolder?.retCode == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun moveGroupFolder(groupId: Long, folderUid: String, newParentFolderUid: String): Boolean {
|
|
||||||
val buffer = sendOidbAW("OidbSvc.0x6d7_2", 1751, 2, Oidb0x6d7ReqBody(
|
|
||||||
moveFolder = MoveFolderReq(
|
|
||||||
groupCode = groupId.toULong(),
|
|
||||||
appId = 3u,
|
|
||||||
folderId = folderUid,
|
|
||||||
parentFolderId = "/"
|
|
||||||
)
|
|
||||||
).toByteArray()) ?: return false
|
|
||||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
|
||||||
oidbPkg.mergeFrom(buffer.slice(4))
|
|
||||||
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
|
|
||||||
return rsp.moveFolder?.retCode == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun renameFolder(groupId: Long, folderUid: String, name: String): Boolean {
|
|
||||||
val buffer = sendOidbAW("OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody(
|
|
||||||
renameFolder = RenameFolderReq(
|
|
||||||
groupCode = groupId.toULong(),
|
|
||||||
appId = 3u,
|
|
||||||
folderId = folderUid,
|
|
||||||
folderName = name
|
|
||||||
)
|
|
||||||
).toByteArray()) ?: return false
|
|
||||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
|
||||||
oidbPkg.mergeFrom(buffer.slice(4))
|
|
||||||
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
|
|
||||||
return rsp.renameFolder?.retCode == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun deleteGroupFile(groupId: Long, bizId: Int, fileUid: String): Boolean {
|
|
||||||
val oidb0x6d6ReqBody = oidb_0x6d6.ReqBody().apply {
|
|
||||||
delete_file_req.set(oidb_0x6d6.DeleteFileReqBody().apply {
|
|
||||||
uint64_group_code.set(groupId)
|
|
||||||
uint32_app_id.set(3)
|
|
||||||
uint32_bus_id.set(bizId)
|
|
||||||
str_parent_folder_id.set("/")
|
|
||||||
str_file_id.set(fileUid)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
val result = sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray())
|
|
||||||
?: return false
|
|
||||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
|
||||||
oidbPkg.mergeFrom(result.slice(4))
|
|
||||||
val rsp = oidb_0x6d6.RspBody().apply {
|
|
||||||
mergeFrom(oidbPkg.bytes_bodybuffer.get().toByteArray())
|
|
||||||
}
|
|
||||||
return rsp.delete_file_rsp.int32_ret_code.get() == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupFileSystemInfo(groupId: Long): FileSystemInfo {
|
|
||||||
val rspGetFileCntBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 2, oidb_0x6d8.ReqBody().also {
|
|
||||||
it.group_file_cnt_req.set(oidb_0x6d8.GetFileCountReqBody().also {
|
|
||||||
it.uint64_group_code.set(groupId)
|
|
||||||
it.uint32_app_id.set(3)
|
|
||||||
it.uint32_bus_id.set(0)
|
|
||||||
})
|
|
||||||
}.toByteArray())
|
|
||||||
val fileCnt: Int
|
|
||||||
val limitCnt: Int
|
|
||||||
if (rspGetFileCntBuffer != null) {
|
|
||||||
oidb_0x6d8.RspBody().mergeFrom(oidb_sso.OIDBSSOPkg()
|
|
||||||
.mergeFrom(rspGetFileCntBuffer.slice(4))
|
|
||||||
.bytes_bodybuffer.get()
|
|
||||||
.toByteArray()
|
|
||||||
).group_file_cnt_rsp.apply {
|
|
||||||
fileCnt = uint32_all_file_count.get()
|
|
||||||
limitCnt = uint32_limit_count.get()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw RuntimeException("获取群文件数量失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
val rspGetFileSpaceBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 3, oidb_0x6d8.ReqBody().also {
|
|
||||||
it.group_space_req.set(oidb_0x6d8.GetSpaceReqBody().apply {
|
|
||||||
uint64_group_code.set(groupId)
|
|
||||||
uint32_app_id.set(3)
|
|
||||||
})
|
|
||||||
}.toByteArray())
|
|
||||||
val totalSpace: Long
|
|
||||||
val usedSpace: Long
|
|
||||||
if (rspGetFileSpaceBuffer != null) {
|
|
||||||
oidb_0x6d8.RspBody().mergeFrom(oidb_sso.OIDBSSOPkg()
|
|
||||||
.mergeFrom(rspGetFileSpaceBuffer.slice(4))
|
|
||||||
.bytes_bodybuffer.get()
|
|
||||||
.toByteArray()).group_space_rsp.apply {
|
|
||||||
totalSpace = uint64_total_space.get()
|
|
||||||
usedSpace = uint64_used_space.get()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw RuntimeException("获取群文件空间失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
return FileSystemInfo(
|
|
||||||
fileCnt, limitCnt, usedSpace, totalSpace
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupRootFiles(groupId: Long): Result<GroupFileList> {
|
|
||||||
return getGroupFiles(groupId, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupFileInfo(groupId: Long, fileId: String, busid: Int): FileUrl {
|
|
||||||
return FileUrl(RichProtoSvc.getGroupFileDownUrl(groupId, fileId, busid))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupFiles(groupId: Long, folderId: String): Result<GroupFileList> {
|
|
||||||
val fileSystemInfo = getGroupFileSystemInfo(groupId)
|
|
||||||
val rspGetFileListBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 1, oidb_0x6d8.ReqBody().also {
|
|
||||||
it.file_list_info_req.set(oidb_0x6d8.GetFileListReqBody().apply {
|
|
||||||
uint64_group_code.set(groupId)
|
|
||||||
uint32_app_id.set(3)
|
|
||||||
str_folder_id.set(folderId)
|
|
||||||
|
|
||||||
uint32_file_count.set(fileSystemInfo.fileCount)
|
|
||||||
uint32_all_file_count.set(0)
|
|
||||||
uint32_req_from.set(3)
|
|
||||||
uint32_sort_by.set(oidb_0x6d8.GetFileListReqBody.SORT_BY_TIMESTAMP)
|
|
||||||
|
|
||||||
uint32_filter_code.set(0)
|
|
||||||
uint64_uin.set(0)
|
|
||||||
|
|
||||||
uint32_start_index.set(0)
|
|
||||||
|
|
||||||
bytes_context.set(ByteStringMicro.copyFrom(EMPTY_BYTE_ARRAY))
|
|
||||||
|
|
||||||
uint32_show_onlinedoc_folder.set(0)
|
|
||||||
})
|
|
||||||
}.toByteArray(), timeout = 15_000L)
|
|
||||||
|
|
||||||
return kotlin.runCatching {
|
|
||||||
val files = arrayListOf<FileInfo>()
|
|
||||||
val dirs = arrayListOf<FolderInfo>()
|
|
||||||
if (rspGetFileListBuffer != null) {
|
|
||||||
val oidb = oidb_sso.OIDBSSOPkg().mergeFrom(rspGetFileListBuffer.slice(4).let {
|
|
||||||
if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it
|
|
||||||
})
|
|
||||||
|
|
||||||
oidb_0x6d8.RspBody().mergeFrom(oidb.bytes_bodybuffer.get().toByteArray())
|
|
||||||
.file_list_info_rsp.apply {
|
|
||||||
rpt_item_list.get().forEach { file ->
|
|
||||||
if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FILE) {
|
|
||||||
val fileInfo = file.file_info
|
|
||||||
files.add(FileInfo(
|
|
||||||
groupId = groupId,
|
|
||||||
fileId = fileInfo.str_file_id.get(),
|
|
||||||
fileName = fileInfo.str_file_name.get(),
|
|
||||||
fileSize = fileInfo.uint64_file_size.get(),
|
|
||||||
busid = fileInfo.uint32_bus_id.get(),
|
|
||||||
uploadTime = fileInfo.uint32_upload_time.get(),
|
|
||||||
deadTime = fileInfo.uint32_dead_time.get(),
|
|
||||||
modifyTime = fileInfo.uint32_modify_time.get(),
|
|
||||||
downloadTimes = fileInfo.uint32_download_times.get(),
|
|
||||||
uploadUin = fileInfo.uint64_uploader_uin.get(),
|
|
||||||
uploadNick = fileInfo.str_uploader_name.get(),
|
|
||||||
md5 = fileInfo.bytes_md5.get().toByteArray().toHexString(),
|
|
||||||
sha = fileInfo.bytes_sha.get().toByteArray().toHexString(),
|
|
||||||
// 根本没有
|
|
||||||
sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) {
|
|
||||||
val folderInfo = file.folder_info
|
|
||||||
dirs.add(FolderInfo(
|
|
||||||
groupId = groupId,
|
|
||||||
folderId = folderInfo.str_folder_id.get(),
|
|
||||||
folderName = folderInfo.str_folder_name.get(),
|
|
||||||
totalFileCount = folderInfo.uint32_total_file_count.get(),
|
|
||||||
createTime = folderInfo.uint32_create_time.get(),
|
|
||||||
creator = folderInfo.uint64_create_uin.get(),
|
|
||||||
creatorNick = folderInfo.str_creator_name.get()
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
LogCenter.log("未知文件类型: ${file.uint32_type.get()}", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw RuntimeException("获取群文件列表失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupFileList(files, dirs)
|
|
||||||
}.onFailure {
|
|
||||||
LogCenter.log(it.message + ", buffer: ${rspGetFileListBuffer.toHexString()}", Level.ERROR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,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<List<Friends>> {
|
|
||||||
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<structmsg.StructMsg>? {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Oidb0xf57MetaInfo> {
|
|
||||||
val respBuffer = sendOidbAW("OidbSvcTrpcTcp.0xf57_9", 0xf57, 9, Oidb0xf57Req(
|
|
||||||
filter = Oidb0xf57Filter(
|
|
||||||
u1 = Oidb0xf57U1(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u),
|
|
||||||
u2 = Oidb0xf57U2(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u)
|
|
||||||
),
|
|
||||||
guildInfo = Oidb0xf57GuildInfo(guildId = guildId)
|
|
||||||
).toByteArray())
|
|
||||||
val body = oidb_sso.OIDBSSOPkg()
|
|
||||||
if (respBuffer == null) {
|
|
||||||
return Result.failure(Exception("unable to send packet"))
|
|
||||||
}
|
|
||||||
body.mergeFrom(respBuffer.slice(4))
|
|
||||||
return runCatching {
|
|
||||||
body.bytes_bodybuffer.get()
|
|
||||||
.toByteArray()
|
|
||||||
.decodeProtobuf<Oidb0xf57Rsp>().metaInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGuildFeeds(guildId: ULong, channelId: ULong, startIndex: Int): Result<GetGuildFeedsRsp> {
|
|
||||||
val buffer = sendBufferAW("QChannelSvr.trpc.qchannel.commreader.ComReader.GetGuildFeeds", true, QWebReq(
|
|
||||||
seq = 10,
|
|
||||||
qua = PlatformUtils.getQUA(),
|
|
||||||
deviceInfo = 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<QWebRsp>()
|
|
||||||
if(webRsp.buffer == null) return Result.failure(Exception("server error"))
|
|
||||||
val wupBuffer = webRsp.buffer!!
|
|
||||||
val feeds = wupBuffer.decodeProtobuf<GetGuildFeedsRsp>()
|
|
||||||
return Result.success(feeds)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getChannelList(guildId: ULong, refresh: Boolean = false): Result<ArrayList<GProChannelInfo>> {
|
|
||||||
if (refresh) {
|
|
||||||
refreshGuildInfo(guildId)
|
|
||||||
}
|
|
||||||
val result = arrayListOf<GProChannelInfo>()
|
|
||||||
app.getRuntimeService(IGPSService::class.java, "all").getChannelList(guildId.toString()).forEach {
|
|
||||||
result.add(GProChannelInfo(
|
|
||||||
ownerGuildId = guildId,
|
|
||||||
guildId = it.guildId,
|
|
||||||
channelId = it.channelUin.toLong(),
|
|
||||||
channelUin = it.channelUin.toLong(),
|
|
||||||
channelName = it.channelName ?: "",
|
|
||||||
channelType = it.type,
|
|
||||||
createTime = it.createTime,
|
|
||||||
creatorTinyId = it.creatorId.toLong(),
|
|
||||||
talkPermission = it.talkPermission,
|
|
||||||
visibleType = it.visibleType,
|
|
||||||
currentSlowMode = it.slowModeKey,
|
|
||||||
slowModes = it.gProSlowModeInfoList.map {
|
|
||||||
SlowModeInfo(it.slowModeKey, it.slowModeText, it.speakFrequency, it.slowModeCircle)
|
|
||||||
},
|
|
||||||
appIconUrl = it.iconUrl,
|
|
||||||
jumpType = it.appChannelJumpType,
|
|
||||||
jumpSwitch = it.jumpSwitch,
|
|
||||||
jumpUrl = it.appChannelJumpUrl,
|
|
||||||
categoryId = it.categoryId,
|
|
||||||
myTalkPermission = it.myTalkPermissionType,
|
|
||||||
maxMemberCount = it.channelMemberMax
|
|
||||||
))
|
|
||||||
}
|
|
||||||
return Result.success(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshGuildInfo(guildId: ULong) {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
kernelGProService.refreshGuildInfo(guildId.toLong(), true, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGuildMemberList(
|
|
||||||
guildId: ULong,
|
|
||||||
startIndex: Long = 0,
|
|
||||||
roleIndex: Long = 1,
|
|
||||||
count: Int = 50,
|
|
||||||
fetchAll: Boolean = false,
|
|
||||||
result: ArrayList<GProRoleMemberList> = arrayListOf()
|
|
||||||
): Result<Pair<GetGuildMemberListNextToken, ArrayList<GProRoleMemberList>>> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
|
|
||||||
val fetchGuildMemberListResult: Pair<GetGuildMemberListNextToken, ArrayList<GProRoleMemberList>> = (withTimeoutOrNull(5000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
kernelGProService.fetchMemberListWithRole(guildId.toLong(), 0, startIndex, roleIndex, count, 0) { code, reason, finish, nextIndex, nextRoleIdIndex, _, seq, roleList ->
|
|
||||||
if (code == 0) {
|
|
||||||
it.resume(GetGuildMemberListNextToken(nextIndex, nextRoleIdIndex, seq, finish) to roleList)
|
|
||||||
} else {
|
|
||||||
LogCenter.log("fetchMemberListWithRole failed: $code($reason)", Level.WARN)
|
|
||||||
it.resume(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) ?: return Result.failure(Exception("unable to fetch guild member list"))
|
|
||||||
|
|
||||||
val nextToken = fetchGuildMemberListResult.first
|
|
||||||
val roleList = fetchGuildMemberListResult.second
|
|
||||||
result.addAll(roleList)
|
|
||||||
return if (fetchAll) {
|
|
||||||
if (!fetchGuildMemberListResult.first.finish) {
|
|
||||||
getGuildMemberList(guildId, nextToken.startIndex, nextToken.roleIndex, count, true, result)
|
|
||||||
} else {
|
|
||||||
Result.success(nextToken.copy(finish = true) to result)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Result.success(nextToken to result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getSelfGuildInfo(): Result<GProUserInfo> {
|
|
||||||
val selfTinyId = getSelfTinyId()
|
|
||||||
return getUserGuildInfo(0u, selfTinyId)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getUserGuildInfo(
|
|
||||||
guildId: ULong,
|
|
||||||
memberTinyId: ULong
|
|
||||||
): Result<GProUserInfo> {
|
|
||||||
val respBuffer = sendOidbAW("OidbSvcTrpcTcp.0xf88_1", 0xf88, 1, Oidb0xf88Req(
|
|
||||||
filter = GProFilter(1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u, 1u),
|
|
||||||
memberId = 0uL,
|
|
||||||
tinyId = memberTinyId,
|
|
||||||
guildId = guildId
|
|
||||||
).toByteArray())
|
|
||||||
val body = oidb_sso.OIDBSSOPkg()
|
|
||||||
if (respBuffer == null) {
|
|
||||||
return Result.failure(Exception("unable to send packet"))
|
|
||||||
}
|
|
||||||
body.mergeFrom(respBuffer.slice(4))
|
|
||||||
return runCatching {
|
|
||||||
body.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0xf88Rsp>().userInfo!!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getGuildListByOldApi(result: ArrayList<GuildInfo>) {
|
|
||||||
app.getRuntimeService(IGPSService::class.java, "all").guildList?.forEach {
|
|
||||||
result.add(GuildInfo(
|
|
||||||
guildId = it.guildID.toLong(),
|
|
||||||
guildName = it.guildName ?: "",
|
|
||||||
guildDisplayId = it.guildNumber ?: "",
|
|
||||||
profile = it.profile ?: "",
|
|
||||||
status = GuildStatus(
|
|
||||||
isEnable = !it.isFrozen && !it.isBanned,
|
|
||||||
isBanned = it.isBanned,
|
|
||||||
isFrozen = it.isFrozen
|
|
||||||
),
|
|
||||||
ownerId = 0,
|
|
||||||
shutUpTime = it.shutUpExpireTime,
|
|
||||||
allowSearch = it.allowSearch
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getGuildListByNt(result: ArrayList<GuildInfo>) {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
kernelGProService.guildListFromCache.forEach {
|
|
||||||
if (it.result != 0) return@forEach
|
|
||||||
val guildInfo = it.guildInfo
|
|
||||||
result.add(GuildInfo(
|
|
||||||
guildId = it.guildId,
|
|
||||||
guildName = guildInfo.guildName ?: "",
|
|
||||||
guildDisplayId = guildInfo.guildNumber ?: "",
|
|
||||||
profile = guildInfo.profile ?: "",
|
|
||||||
status = GuildStatus(
|
|
||||||
isEnable = guildInfo.guildStatus?.isEnable == 1,
|
|
||||||
isBanned = guildInfo.guildStatus?.isBanned == 1,
|
|
||||||
isFrozen = guildInfo.guildStatus?.isFrozen == 1
|
|
||||||
),
|
|
||||||
ownerId = guildInfo.ownerTinyid,
|
|
||||||
shutUpTime = guildInfo.shutupExpireTime,
|
|
||||||
allowSearch = guildInfo.allowSearch == 1
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun fetchGuildMemberRoles(guildId: ULong, tinyId: ULong, refresh: Boolean = false): Result<ArrayList<GProGuildRole>> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
if (refresh) {
|
|
||||||
kernelGProService.refreshGuildUserProfileInfo(guildId.toLong(), tinyId.toLong(), 1)
|
|
||||||
}
|
|
||||||
val result: ArrayList<GProGuildRole> = withTimeoutOrNull(5000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
kernelGProService.fetchMemberRoles(guildId.toLong(), 0, tinyId.toLong(), 2) { code, reason, roles ->
|
|
||||||
it.resume(roles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return Result.failure(Exception("unable to fetch guild member roles"))
|
|
||||||
return Result.success(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getGuildList(refresh: Boolean = false, forceOldApi: Boolean): ArrayList<GuildInfo> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
if (refresh) {
|
|
||||||
kernelGProService.refreshGuildList(true)
|
|
||||||
kernelGProService.guildListFromCache.forEach {
|
|
||||||
refreshGuildInfo(it.guildId.toULong())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val result = arrayListOf<GuildInfo>()
|
|
||||||
if (PlatformUtils.getQQVersionCode() < PlatformUtils.QQ_9_0_8_VER || forceOldApi) {
|
|
||||||
getGuildListByOldApi(result)
|
|
||||||
} else {
|
|
||||||
runCatching {
|
|
||||||
getGuildListByNt(result)
|
|
||||||
}.onFailure {
|
|
||||||
LogCenter.log("GetGuildListByNt failed: ${it.stackTraceToString()}", Level.ERROR)
|
|
||||||
getGuildListByOldApi(result) // 防止QQ更新API导致无法获取
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGuildRoles(guildId: ULong): Result<List<GProGuildRole>> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
val roles: List<GProGuildRole> = withTimeoutOrNull(5000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
kernelGProService.fetchRoleListWithPermission(guildId.toLong(), 1) { code, _, roles, _, _, _ ->
|
|
||||||
if (code != 0) it.resume(null) else it.resume(roles)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return Result.failure(Exception("unable to fetch guild roles"))
|
|
||||||
return Result.success(roles)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteGuildRole(guildId: ULong, roleId: ULong) {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
kernelGProService.deleteRole(guildId.toLong(), roleId.toLong()) { code, msg, result ->
|
|
||||||
if (code != 0) {
|
|
||||||
LogCenter.log("deleteGuildRole failed: $code($msg) => $result", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setMemberRole(guildId: ULong, tinyId: ULong, roleId: ULong, isSet: Boolean) {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
val addList = arrayListOf<Long>()
|
|
||||||
val rmList = arrayListOf<Long>()
|
|
||||||
(if (isSet) addList else rmList).add(roleId.toLong())
|
|
||||||
kernelGProService.setMemberRoles(guildId.toLong(), 0, 0, tinyId.toLong(), addList, rmList) { code, msg, result ->
|
|
||||||
if (code != 0) {
|
|
||||||
LogCenter.log("setMemberRole failed: $code($msg) => $result", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGuildRolePermission(guildId: ULong, roleId: ULong): Result<GProGuildRole> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
val role:GProGuildRole = withTimeoutOrNull(5000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
kernelGProService.fetchRoleWithPermission(guildId.toLong(), roleId.toLong(), 1) { code, msg, role, _, _, _ ->
|
|
||||||
if (code != 0) {
|
|
||||||
LogCenter.log("getGuildRolePermission failed: $code($msg)", Level.WARN)
|
|
||||||
it.resume(null)
|
|
||||||
} else it.resume(role)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return Result.failure(Exception("unable to fetch guild role permission"))
|
|
||||||
return Result.success(role)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateGuildRole(guildId: ULong, roleId: ULong, name: String, color: Long): Result<Unit> {
|
|
||||||
val oldInfo = getGuildRolePermission(guildId, roleId).onFailure {
|
|
||||||
return Result.failure(it)
|
|
||||||
}.getOrThrow()
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
val info = GProRoleCreateInfo(
|
|
||||||
name, color, oldInfo.bHoist, oldInfo.rolePermissions
|
|
||||||
)
|
|
||||||
kernelGProService.setRoleInfo(guildId.toLong(), roleId.toLong(), info) { code, msg, result ->
|
|
||||||
if (code != 0) {
|
|
||||||
LogCenter.log("updateGuildRole failed: $code($msg) => $result", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Result.success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun createGuildRole(guildId: ULong, name: String, color: Long, initialUsers: ArrayList<Long>): Result<GProGuildRole> {
|
|
||||||
val kernelGProService = NTServiceFetcher.kernelService.wrapperSession.guildService
|
|
||||||
val permission = GProRolePermission(false, arrayListOf())
|
|
||||||
val info = GProRoleCreateInfo(name, color, false, permission)
|
|
||||||
val role: GProGuildRole = withTimeoutOrNull(5000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
kernelGProService.createRole(guildId.toLong(), info, initialUsers) { code, msg, result, role ->
|
|
||||||
if (code != 0) {
|
|
||||||
LogCenter.log("createGuildRole failed: $code($msg) => $result", Level.WARN)
|
|
||||||
it.resume(null)
|
|
||||||
} else it.resume(role)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return Result.failure(Exception("unable to create guild role"))
|
|
||||||
return Result.success(role)
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -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<Unit> {
|
|
||||||
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<String> {
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Unit> {
|
|
||||||
LogCenter.log("主动临时消息,创建临时会话。", Level.INFO)
|
|
||||||
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
|
|
||||||
?: return Result.failure(Exception("获取消息服务失败"))
|
|
||||||
msgService.prepareTempChat(
|
|
||||||
TempChatPrepareInfo(
|
|
||||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
|
|
||||||
ContactHelper.getUidByUinAsync(peerId = peerId.toLong()),
|
|
||||||
app.getRuntimeService(ITroopMemberNameService::class.java, "all")
|
|
||||||
.getTroopMemberNameRemarkFirst(groupId, peerId),
|
|
||||||
groupId,
|
|
||||||
EMPTY_BYTE_ARRAY,
|
|
||||||
app.currentUid,
|
|
||||||
"",
|
|
||||||
TempChatGameSession()
|
|
||||||
)
|
|
||||||
) { code, reason ->
|
|
||||||
if (code != 0) {
|
|
||||||
LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Result.success(Unit)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getTempChatInfo(chatType: Int, uid: String): Result<TempChatInfo> {
|
|
||||||
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
|
|
||||||
?: return Result.failure(Exception("获取消息服务失败"))
|
|
||||||
val info: TempChatInfo = withTimeoutOrNull(5000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
msgService.getTempChatInfo(chatType, uid) { code, msg, tempChatInfo ->
|
|
||||||
if (code == 0) {
|
|
||||||
it.resume(tempChatInfo)
|
|
||||||
} else {
|
|
||||||
LogCenter.log("获取临时会话信息失败: $code:$msg", Level.ERROR)
|
|
||||||
it.resume(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return Result.failure(Exception("获取临时会话信息失败"))
|
|
||||||
return Result.success(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 正常获取
|
|
||||||
*/
|
|
||||||
suspend fun getMsg(hash: Int): Result<MsgRecord> {
|
|
||||||
val mapping = MessageHelper.getMsgMappingByHash(hash)
|
|
||||||
?: return Result.failure(Exception("没有对应消息映射,消息获取失败"))
|
|
||||||
|
|
||||||
val peerId = mapping.peerId
|
|
||||||
val contact = MessageHelper.generateContact(mapping.chatType, peerId, mapping.subPeerId)
|
|
||||||
|
|
||||||
val msg = withTimeoutOrNull(5000) {
|
|
||||||
val service = QRoute.api(IMsgService::class.java)
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
|
||||||
service.getMsgsByMsgId(contact, arrayListOf(mapping.qqMsgId)) { code, _, msgRecords ->
|
|
||||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
|
||||||
continuation.resume(msgRecords.first())
|
|
||||||
} else {
|
|
||||||
continuation.resume(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continuation.invokeOnCancellation {
|
|
||||||
continuation.resume(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (msg != null) {
|
|
||||||
Result.success(msg)
|
|
||||||
} else {
|
|
||||||
Result.failure(Exception("获取消息失败"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getMsgByQMsgId(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
qqMsgId: Long,
|
|
||||||
subPeerId: String = ""
|
|
||||||
): Result<MsgRecord> {
|
|
||||||
val contact = MessageHelper.generateContact(chatType, peerId, subPeerId)
|
|
||||||
val service = QRoute.api(IMsgService::class.java)
|
|
||||||
|
|
||||||
val msg = withTimeoutOrNull(5000) {
|
|
||||||
suspendCoroutine { continuation ->
|
|
||||||
service.getMsgsByMsgId(contact, arrayListOf(qqMsgId)) { code, _, msgRecords ->
|
|
||||||
if (code == 0 && msgRecords.isNotEmpty()) {
|
|
||||||
continuation.resume(msgRecords.first())
|
|
||||||
} else {
|
|
||||||
continuation.resume(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (msg != null) {
|
|
||||||
Result.success(msg)
|
|
||||||
} else {
|
|
||||||
Result.failure(Exception("获取消息失败"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 什么鸟屎都获取不到
|
|
||||||
*/
|
|
||||||
suspend fun getMsgBySeq(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
seq: Long
|
|
||||||
): Result<MsgRecord> {
|
|
||||||
val contact = MessageHelper.generateContact(chatType, peerId)
|
|
||||||
val msg = withTimeoutOrNull(1000) {
|
|
||||||
val service = QRoute.api(IMsgService::class.java)
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
|
||||||
service.getMsgsBySeqs(contact, arrayListOf(seq)) { code, _, msgRecords ->
|
|
||||||
continuation.resume(msgRecords?.firstOrNull())
|
|
||||||
}
|
|
||||||
continuation.invokeOnCancellation {
|
|
||||||
continuation.resume(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (msg != null) {
|
|
||||||
Result.success(msg)
|
|
||||||
} else {
|
|
||||||
Result.failure(Exception("获取消息失败"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 撤回消息 同步 HTTP API
|
|
||||||
*/
|
|
||||||
suspend fun recallMsg(msgHash: Int): Pair<Int, String> {
|
|
||||||
val kernelService = NTServiceFetcher.kernelService
|
|
||||||
val sessionService = kernelService.wrapperSession
|
|
||||||
val msgService = sessionService.msgService
|
|
||||||
|
|
||||||
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
|
|
||||||
?: return -1 to "无法找到消息映射"
|
|
||||||
|
|
||||||
val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId, mapping.subPeerId)
|
|
||||||
|
|
||||||
return suspendCancellableCoroutine { continuation ->
|
|
||||||
msgService.recallMsg(contact, arrayListOf(mapping.qqMsgId)) { code, why ->
|
|
||||||
continuation.resume(code to why)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送消息
|
|
||||||
*
|
|
||||||
* Aio 腾讯内部命名 All In One
|
|
||||||
*/
|
|
||||||
suspend fun sendToAio(
|
|
||||||
chatType: Int,
|
|
||||||
peedId: String,
|
|
||||||
message: JsonArray,
|
|
||||||
fromId: String = peedId,
|
|
||||||
retryCnt: Int
|
|
||||||
): Result<SendMsgResult> {
|
|
||||||
// 主动临时消息
|
|
||||||
when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
|
|
||||||
prepareTempChatFromGroup(fromId, peedId).onFailure {
|
|
||||||
LogCenter.log("主动临时消息,创建临时会话失败。", Level.ERROR)
|
|
||||||
return Result.failure(Exception("主动临时消息,创建临时会话失败。"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val result =
|
|
||||||
MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
|
|
||||||
.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<MessageSegment> {
|
|
||||||
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<MessageSegment> {
|
|
||||||
var i = -1
|
|
||||||
val desc = MutableList(messages.size) { "" }
|
|
||||||
val forwardMsg = mutableMapOf<String, String>()
|
|
||||||
|
|
||||||
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<LongMsgRsp>()
|
|
||||||
}.getOrElse {
|
|
||||||
buffer.decodeProtobuf<LongMsgRsp>()
|
|
||||||
}
|
|
||||||
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<List<LongMsgAction>> {
|
|
||||||
val req = LongMsgReq(
|
|
||||||
recvInfo = RecvLongMsgInfo(
|
|
||||||
uid = LongMsgUid(TicketSvc.getUid()),
|
|
||||||
resId = resId,
|
|
||||||
u1 = 3
|
|
||||||
),
|
|
||||||
setting = LongMsgSettings(
|
|
||||||
field1 = 2,
|
|
||||||
field2 = 2,
|
|
||||||
field3 = 9,
|
|
||||||
field4 = 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val buffer = sendBufferAW(
|
|
||||||
"trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg",
|
|
||||||
true,
|
|
||||||
req.toByteArray()
|
|
||||||
) ?: return Result.failure(Exception("unable to get multi message"))
|
|
||||||
val rsp = buffer.slice(4).decodeProtobuf<LongMsgRsp>()
|
|
||||||
val zippedPayload = DeflateTools.ungzip(
|
|
||||||
rsp.recvResult?.payload ?: return Result.failure(Exception("payload is empty"))
|
|
||||||
)
|
|
||||||
LogCenter.log(zippedPayload.toHexString(), Level.DEBUG)
|
|
||||||
return Result.success(
|
|
||||||
zippedPayload.decodeProtobuf<LongMsgPayload>().action
|
|
||||||
?: return Result.failure(Exception("action is empty"))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getForwardMsg(resId: String): Result<List<MessageDetail>> {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Elem>): Long {
|
|
||||||
val latestMsg = withTimeoutOrNull(3000) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
msgService.getMsgs(
|
|
||||||
Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""),
|
|
||||||
0L,
|
|
||||||
1,
|
|
||||||
true
|
|
||||||
) { code, why, msgs ->
|
|
||||||
it.resume(GetHistoryMsg.GetMsgResult(code, why, msgs))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}?.data?.firstOrNull()
|
|
||||||
val msgSeq = (latestMsg?.msgSeq ?: 0) + 1
|
|
||||||
|
|
||||||
val msgPush = MessagePush(
|
|
||||||
msgBody = PushMsgBody(
|
|
||||||
msgHead = ResponseHead(
|
|
||||||
peer = app.longAccountUin,
|
|
||||||
peerUid = app.currentUid,
|
|
||||||
flag = 1001,
|
|
||||||
receiver = app.longAccountUin,
|
|
||||||
receiverUid = app.currentUid
|
|
||||||
),
|
|
||||||
contentHead = ContentHead(
|
|
||||||
msgType = 166,
|
|
||||||
msgSubType = 11,
|
|
||||||
msgSeq = msgSeq,
|
|
||||||
msgViaRandom = msgSeq,
|
|
||||||
msgTime = System.currentTimeMillis() / 1000,
|
|
||||||
u2 = 1,
|
|
||||||
sequence = msgSeq,
|
|
||||||
msgRandom = msgService.getMsgUniqueId(System.currentTimeMillis()),
|
|
||||||
u4 = msgSeq - 2,
|
|
||||||
u5 = msgSeq
|
|
||||||
),
|
|
||||||
body = MsgBody(
|
|
||||||
RichText(
|
|
||||||
elements = builder()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
fakeReceive("trpc.msg.olpush.OlPushService.MsgPush", 10000, msgPush.toByteArray())
|
|
||||||
return withTimeoutOrNull(5000L) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
MessageTempHandler.registerTemporaryMsgListener(msgSeq) {
|
|
||||||
it.resume(this.msgId)
|
|
||||||
}
|
|
||||||
it.invokeOnCancellation {
|
|
||||||
MessageTempHandler.unregisterTemporaryMsgListener(msgSeq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: -1L
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 伪造QQ收到某个包
|
|
||||||
*/
|
|
||||||
private fun fakeReceive(cmd: String, seq: Int, buffer: ByteArray) {
|
|
||||||
MobileQQ.getContext().broadcast("msf") {
|
|
||||||
putExtra("__cmd", "fake_packet")
|
|
||||||
putExtra("package_cmd", cmd)
|
|
||||||
putExtra("package_uin", app.currentUin)
|
|
||||||
putExtra("package_seq", seq)
|
|
||||||
val wupBuffer = BytePacketBuilder().apply {
|
|
||||||
writeInt(buffer.size + 4)
|
|
||||||
writeFully(buffer)
|
|
||||||
}.build()
|
|
||||||
putExtra("package_buffer", wupBuffer.readBytes())
|
|
||||||
wupBuffer.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,402 +0,0 @@
|
|||||||
@file:OptIn(ExperimentalSerializationApi::class)
|
|
||||||
|
|
||||||
package moe.fuqiuluo.qqinterface.servlet
|
|
||||||
|
|
||||||
import com.tencent.mobileqq.app.QQAppInterface
|
|
||||||
import com.tencent.mobileqq.transfile.HttpNetReq
|
|
||||||
import com.tencent.mobileqq.transfile.INetEngineListener
|
|
||||||
import com.tencent.mobileqq.transfile.NetReq
|
|
||||||
import com.tencent.mobileqq.transfile.NetResp
|
|
||||||
import com.tencent.mobileqq.transfile.ServerAddr
|
|
||||||
import com.tencent.mobileqq.transfile.api.IHttpEngineService
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.io.core.BytePacketBuilder
|
|
||||||
import kotlinx.io.core.readBytes
|
|
||||||
import kotlinx.io.core.writeFully
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.encodeToByteArray
|
|
||||||
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
|
||||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
|
||||||
import moe.fuqiuluo.shamrock.utils.MD5
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
|
||||||
import protobuf.fav.WeiyunAddRichMediaReq
|
|
||||||
import protobuf.fav.WeiyunAuthor
|
|
||||||
import protobuf.fav.WeiyunCollectCommInfo
|
|
||||||
import protobuf.fav.WeiyunComm
|
|
||||||
import protobuf.fav.WeiyunCommonReq
|
|
||||||
import protobuf.fav.WeiyunFastUploadResourceReq
|
|
||||||
import protobuf.fav.WeiyunGetFavContentReq
|
|
||||||
import protobuf.fav.WeiyunGetFavListReq
|
|
||||||
import protobuf.fav.WeiyunMsgHead
|
|
||||||
import protobuf.fav.WeiyunPicInfo
|
|
||||||
import protobuf.fav.WeiyunRichMediaContent
|
|
||||||
import protobuf.fav.WeiyunRichMediaSummary
|
|
||||||
import mqq.manager.TicketManager
|
|
||||||
import oicq.wlogin_sdk.request.Ticket
|
|
||||||
import oicq.wlogin_sdk.request.WtTicketPromise
|
|
||||||
import oicq.wlogin_sdk.tools.ErrMsg
|
|
||||||
import protobuf.auto.toByteArray
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.io.File
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* QQ收藏相关接口
|
|
||||||
*/
|
|
||||||
internal object QFavSvc: BaseSvc() {
|
|
||||||
private val SERVER_LIST_COLLECTOR = listOf(ServerAddr().also {
|
|
||||||
it.isIpv6 = false
|
|
||||||
it.mIp = "collector.weiyun.com"
|
|
||||||
it.port = 80
|
|
||||||
})
|
|
||||||
private val SERVER_LIST_PICUP = listOf(ServerAddr().also {
|
|
||||||
it.isIpv6 = false
|
|
||||||
it.mIp = "pic.pieceup.qq.com"
|
|
||||||
it.port = 80
|
|
||||||
})
|
|
||||||
private const val VERSION = 12820
|
|
||||||
private const val APPID = 30244
|
|
||||||
private const val SUB_APPID = 538116905
|
|
||||||
private const val MAJOR_VERSION = 8
|
|
||||||
private const val MINOR_VERSION = 9
|
|
||||||
private var seq = 1
|
|
||||||
|
|
||||||
suspend fun getItemList(
|
|
||||||
category: Int,
|
|
||||||
startPos: Int,
|
|
||||||
pageSize: Int,
|
|
||||||
): Result<NetResp> {
|
|
||||||
val baseReq = WeiyunCommonReq(
|
|
||||||
getFavListReq = WeiyunGetFavListReq(
|
|
||||||
type = 0u,
|
|
||||||
bid = 0u,
|
|
||||||
category = category.toUInt(),
|
|
||||||
startTime = 0u,
|
|
||||||
orderType = 0u,
|
|
||||||
startPos = startPos.toUInt(),
|
|
||||||
pageSize = pageSize.toUInt(),
|
|
||||||
syncPolicy = 0u,
|
|
||||||
reqSource = 0u
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return sendWeiyunReq(20000, baseReq)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getItemContent(
|
|
||||||
id: String
|
|
||||||
): Result<NetResp> {
|
|
||||||
return sendWeiyunReq(20001, WeiyunCommonReq(
|
|
||||||
getFavContentReq = WeiyunGetFavContentReq(
|
|
||||||
cidList = arrayListOf(id)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun addImageMsg(
|
|
||||||
uin: Long,
|
|
||||||
name: String,
|
|
||||||
groupId: Long = 0,
|
|
||||||
groupName: String = "",
|
|
||||||
picUrl: String,
|
|
||||||
pid: String,
|
|
||||||
width: Int, height: Int,
|
|
||||||
size: Long,
|
|
||||||
md5: String,
|
|
||||||
): Result<NetResp> {
|
|
||||||
val md5Bytes = md5.hex2ByteArray()
|
|
||||||
return sendWeiyunReq(20009, WeiyunCommonReq(
|
|
||||||
addRichMediaReq = WeiyunAddRichMediaReq(
|
|
||||||
commInfo = WeiyunCollectCommInfo(
|
|
||||||
bid = 1u,
|
|
||||||
category = 1u,
|
|
||||||
author = WeiyunAuthor(
|
|
||||||
type = if (groupId == 0L) 1u else 2u,
|
|
||||||
numId = uin.toULong(),
|
|
||||||
strId = name,
|
|
||||||
groupId = groupId.toULong(),
|
|
||||||
groupName = groupName
|
|
||||||
),
|
|
||||||
createTime = System.currentTimeMillis().toULong() - 2000u,
|
|
||||||
seq = System.currentTimeMillis().toULong() - 1000u,
|
|
||||||
bizDataList = arrayListOf("""{"recordAudioOnly":false,"audioOnly":false,"fileOnly":false}""".toByteArray()),
|
|
||||||
originalAppId = 0u,
|
|
||||||
customGroupId = 0u
|
|
||||||
),
|
|
||||||
summary = WeiyunRichMediaSummary(
|
|
||||||
title = "",
|
|
||||||
brief = "[图片]",
|
|
||||||
picList = arrayListOf(
|
|
||||||
WeiyunPicInfo(
|
|
||||||
uri = picUrl,
|
|
||||||
md5 = md5Bytes,
|
|
||||||
sha1 = md5.toByteArray(),
|
|
||||||
name = "",
|
|
||||||
note = "",
|
|
||||||
width = width.toUInt(),
|
|
||||||
height = height.toUInt(),
|
|
||||||
size = size.toULong(),
|
|
||||||
type = 0u,
|
|
||||||
picId = pid
|
|
||||||
)
|
|
||||||
),
|
|
||||||
contentType = 1u
|
|
||||||
),
|
|
||||||
richMediaContent = listOf(
|
|
||||||
WeiyunRichMediaContent(
|
|
||||||
rawData = """<img src="$picUrl" />""".toByteArray(),
|
|
||||||
picList = listOf(
|
|
||||||
WeiyunPicInfo(
|
|
||||||
uri = picUrl,
|
|
||||||
md5 = md5Bytes,
|
|
||||||
sha1 = md5.toByteArray(),
|
|
||||||
name = "",
|
|
||||||
note = "",
|
|
||||||
width = width.toUInt(),
|
|
||||||
height = height.toUInt(),
|
|
||||||
size = size.toULong(),
|
|
||||||
type = 0u,
|
|
||||||
picId = pid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun applyUpImageMsg(
|
|
||||||
uin: Long,
|
|
||||||
name: String,
|
|
||||||
groupId: Long = 0,
|
|
||||||
groupName: String = "",
|
|
||||||
width: Int, height: Int,
|
|
||||||
image: File
|
|
||||||
): Result<NetResp> {
|
|
||||||
if (!image.exists()) {
|
|
||||||
return Result.failure(IllegalArgumentException("image file not exists"))
|
|
||||||
}
|
|
||||||
val md5 = MD5.genFileMd5(image.absolutePath)
|
|
||||||
return sendWeiyunReq(20010, WeiyunCommonReq(
|
|
||||||
fastUploadResourceReq = WeiyunFastUploadResourceReq(
|
|
||||||
picInfoList = listOf(
|
|
||||||
WeiyunPicInfo(
|
|
||||||
md5 = md5,
|
|
||||||
name = md5.toHexString(),
|
|
||||||
width = width.toUInt(),
|
|
||||||
height = height.toUInt(),
|
|
||||||
size = image.length().toULong(),
|
|
||||||
type = 1u,
|
|
||||||
picId = "/storage/emulated/0/DCIM/temp.jpeg",
|
|
||||||
owner = WeiyunAuthor(
|
|
||||||
type = if (groupId == 0L) 1u else 2u,
|
|
||||||
numId = uin.toULong(),
|
|
||||||
strId = name,
|
|
||||||
groupId = groupId.toULong(),
|
|
||||||
groupName = groupName
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun addRichMediaMsg(
|
|
||||||
uin: Long,
|
|
||||||
name: String,
|
|
||||||
groupId: Long = 0,
|
|
||||||
groupName: String = "",
|
|
||||||
time: Long = System.currentTimeMillis(),
|
|
||||||
content: String
|
|
||||||
): Result<NetResp> {
|
|
||||||
return sendWeiyunReq(20009, WeiyunCommonReq(
|
|
||||||
addRichMediaReq = WeiyunAddRichMediaReq(
|
|
||||||
commInfo = WeiyunCollectCommInfo(
|
|
||||||
bid = 1u,
|
|
||||||
category = 1u,
|
|
||||||
author = WeiyunAuthor(
|
|
||||||
type = if (groupId == 0L) 1u else 2u,
|
|
||||||
numId = uin.toULong(),
|
|
||||||
strId = name,
|
|
||||||
groupId = groupId.toULong(),
|
|
||||||
groupName = groupName
|
|
||||||
),
|
|
||||||
createTime = time.toULong() - 2000u,
|
|
||||||
seq = time.toULong() - 1000u,
|
|
||||||
originalAppId = 0u,
|
|
||||||
customGroupId = 0u
|
|
||||||
),
|
|
||||||
summary = WeiyunRichMediaSummary(
|
|
||||||
brief = content,
|
|
||||||
contentType = 1u
|
|
||||||
),
|
|
||||||
richMediaContent = listOf(
|
|
||||||
WeiyunRichMediaContent(
|
|
||||||
rawData = content.textToHtml().toByteArray(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun String.textToHtml(): String {
|
|
||||||
return replace("\n", "<div><br/></div>")
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendPicUpBlock(
|
|
||||||
fileSize: Long,
|
|
||||||
offset: Long,
|
|
||||||
block: ByteArray,
|
|
||||||
blockSize: Long,
|
|
||||||
sha: ByteArray,
|
|
||||||
pid: String,
|
|
||||||
outputStream: ByteArrayOutputStream = ByteArrayOutputStream(),
|
|
||||||
): Result<NetResp> {
|
|
||||||
return suspendCancellableCoroutine {
|
|
||||||
val httpNetReq = HttpNetReq()
|
|
||||||
httpNetReq.userData = null
|
|
||||||
httpNetReq.mCallback = object: INetEngineListener {
|
|
||||||
override fun onResp(netResp: NetResp) {
|
|
||||||
if (netResp.mHttpCode != 200 && netResp.mResult != 0 && netResp.mErrDesc.isNullOrEmpty()) {
|
|
||||||
netResp.mErrDesc = netResp.mRespProperties["User-ErrMsg"]
|
|
||||||
}
|
|
||||||
netResp.mRespData = outputStream.toByteArray().copyOf()
|
|
||||||
it.resume(Result.success(netResp))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUpdateProgeress(netReq: NetReq, curr: Long, final: Long) {}
|
|
||||||
}
|
|
||||||
val vi = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getA2(app.currentAccountUin)
|
|
||||||
//LogCenter.log(pSKey)
|
|
||||||
httpNetReq.mHttpMethod = HttpNetReq.HTTP_POST
|
|
||||||
httpNetReq.mSendData = BytePacketBuilder().apply {
|
|
||||||
writeInt(-1412589450)
|
|
||||||
writeInt(10000)
|
|
||||||
writeInt(0)
|
|
||||||
writeInt(sha.size + 16 + blockSize.toInt())
|
|
||||||
writeShort(0)
|
|
||||||
writeShort(sha.size.toShort())
|
|
||||||
writeFully(sha)
|
|
||||||
writeInt(fileSize.toInt())
|
|
||||||
writeInt(offset.toInt())
|
|
||||||
writeInt(blockSize.toInt())
|
|
||||||
writeFully(block)
|
|
||||||
}.build().readBytes()
|
|
||||||
httpNetReq.mOutStream = outputStream
|
|
||||||
httpNetReq.mStartDownOffset = 0L
|
|
||||||
httpNetReq.mReqProperties["Shamrock"] = "true"
|
|
||||||
httpNetReq.mReqProperties["Cookie"] = String.format("uin=%s;vt=%d;vi=%s;pid=%s;appid=%d", app.currentAccountUin, 8, vi, pid, APPID)
|
|
||||||
httpNetReq.mReqProperties["host"] = "pic.pieceup.qq.com"
|
|
||||||
httpNetReq.mReqProperties["Range"] = "bytes=0-"
|
|
||||||
httpNetReq.mReqProperties["Content-Length"] = httpNetReq.mSendData.size.toString()
|
|
||||||
httpNetReq.mReqProperties["Accept-Encoding"] = "gzip"
|
|
||||||
httpNetReq.mReqProperties["Content-Encoding"] = "gzip"
|
|
||||||
httpNetReq.mPrioty = 1
|
|
||||||
httpNetReq.mReqUrl = "https://pic.pieceup.qq.com/"
|
|
||||||
httpNetReq.mServerList = SERVER_LIST_PICUP
|
|
||||||
val service = AppRuntimeFetcher.appRuntime
|
|
||||||
.getRuntimeService(IHttpEngineService::class.java, "qqfav")
|
|
||||||
service.sendReq(httpNetReq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendWeiyunReq(
|
|
||||||
cmd: Int,
|
|
||||||
req: WeiyunCommonReq,
|
|
||||||
outputStream: ByteArrayOutputStream = ByteArrayOutputStream(),
|
|
||||||
): Result<NetResp> {
|
|
||||||
return suspendCancellableCoroutine {
|
|
||||||
val httpNetReq = HttpNetReq()
|
|
||||||
httpNetReq.userData = null
|
|
||||||
httpNetReq.mCallback = object: INetEngineListener {
|
|
||||||
override fun onResp(netResp: NetResp) {
|
|
||||||
if (netResp.mHttpCode != 200 && netResp.mResult != 0 && netResp.mErrDesc.isNullOrEmpty()) {
|
|
||||||
netResp.mErrDesc = netResp.mRespProperties["User-ErrMsg"]
|
|
||||||
}
|
|
||||||
netResp.mRespData = outputStream.toByteArray().copyOf()
|
|
||||||
it.resume(Result.success(netResp))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUpdateProgeress(netReq: NetReq, curr: Long, final: Long) {}
|
|
||||||
}
|
|
||||||
val pSKey = getWeiYunPSKey()
|
|
||||||
httpNetReq.mHttpMethod = HttpNetReq.HTTP_POST
|
|
||||||
httpNetReq.mSendData = DeflateTools.gzip(packData(packHead(cmd, pSKey), WeiyunComm(
|
|
||||||
req = req
|
|
||||||
).toByteArray()))
|
|
||||||
httpNetReq.mOutStream = outputStream
|
|
||||||
httpNetReq.mStartDownOffset = 0L
|
|
||||||
httpNetReq.mReqProperties["Shamrock"] = "true"
|
|
||||||
httpNetReq.mReqProperties["Cookie"] = String.format("uin=%s;vt=%d;vi=%s;appid=%d", app.currentAccountUin, 27, pSKey, APPID)
|
|
||||||
httpNetReq.mReqProperties["host"] = "collector.weiyun.com"
|
|
||||||
httpNetReq.mReqProperties["Range"] = "bytes=0-"
|
|
||||||
httpNetReq.mReqProperties["Content-Length"] = httpNetReq.mSendData.size.toString()
|
|
||||||
httpNetReq.mReqProperties["Accept-Encoding"] = "gzip"
|
|
||||||
httpNetReq.mReqProperties["Content-Encoding"] = "gzip"
|
|
||||||
httpNetReq.mPrioty = 1
|
|
||||||
httpNetReq.mReqUrl = "https://collector.weiyun.com/collector.fcg"
|
|
||||||
httpNetReq.mServerList = SERVER_LIST_COLLECTOR
|
|
||||||
val service = AppRuntimeFetcher.appRuntime
|
|
||||||
.getRuntimeService(IHttpEngineService::class.java, "qqfav")
|
|
||||||
service.sendReq(httpNetReq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun packHead(cmd: Int, pskey: String): ByteArray {
|
|
||||||
return WeiyunMsgHead(
|
|
||||||
uin = app.longAccountUin.toULong(),
|
|
||||||
seq = seq++.toUInt(),
|
|
||||||
type = 1u,
|
|
||||||
cmd = cmd.toUInt(),
|
|
||||||
appId = APPID.toUInt(),
|
|
||||||
version = VERSION.toUInt(),
|
|
||||||
netType = 3u,
|
|
||||||
keyType = 27u,
|
|
||||||
key = pskey.toByteArray(),
|
|
||||||
majorVersion = MAJOR_VERSION.toUInt(),
|
|
||||||
minorVersion = MINOR_VERSION.toUInt(),
|
|
||||||
).toByteArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun packData(head: ByteArray, body: ByteArray): ByteArray {
|
|
||||||
val len = 16 + head.size + body.size
|
|
||||||
val buf = ByteBuffer.allocate(len)
|
|
||||||
buf.putInt(SUB_APPID)
|
|
||||||
buf.putShort(1)
|
|
||||||
buf.putInt(len)
|
|
||||||
buf.putInt(body.size)
|
|
||||||
buf.putShort(0)
|
|
||||||
buf.put(head)
|
|
||||||
buf.put(body)
|
|
||||||
return buf.array()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getWeiYunPSKey(): String {
|
|
||||||
val pskey = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager)
|
|
||||||
.getPskey(app.currentAccountUin, 16L, arrayOf("weiyun.com"), WeiYunPSKeyPromise)
|
|
||||||
return if (pskey != null) pskey.getPSkey("weiyun.com") else ""
|
|
||||||
}
|
|
||||||
|
|
||||||
private object WeiYunPSKeyPromise: WtTicketPromise {
|
|
||||||
override fun Done(ticket: Ticket) {
|
|
||||||
LogCenter.log("Fav: getPskeyPromise: done", Level.DEBUG)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun Failed(errMsg: ErrMsg) {
|
|
||||||
LogCenter.log("Fav: getPskeyPromise: failed, $errMsg", Level.DEBUG)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun Timeout(errMsg: ErrMsg) {
|
|
||||||
LogCenter.log("Fav: getPskeyPromise: timeout, $errMsg", Level.DEBUG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet
|
|
||||||
|
|
||||||
import QQService.SvcDevLoginInfo
|
|
||||||
import QQService.SvcReqGetDevLoginInfo
|
|
||||||
import QQService.SvcRspGetDevLoginInfo
|
|
||||||
import com.qq.jce.wup.UniPacket
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
|
||||||
import mqq.app.MobileQQ
|
|
||||||
import mqq.app.Packet
|
|
||||||
import oicq.wlogin_sdk.tools.util
|
|
||||||
|
|
||||||
internal object QSafeSvc: BaseSvc() {
|
|
||||||
|
|
||||||
suspend fun getOnlineClients(): ArrayList<SvcDevLoginInfo>? {
|
|
||||||
val req = SvcReqGetDevLoginInfo()
|
|
||||||
req.vecGuid = util.getGuidFromFile(MobileQQ.getContext())
|
|
||||||
req.strAppName = MobileQQ.getMobileQQ().qqProcessName.split(":")[0]
|
|
||||||
req.iLoginType = 1
|
|
||||||
req.iRequireMax = 20
|
|
||||||
req.iGetDevListType = 6
|
|
||||||
|
|
||||||
val uniPacket = UniPacket()
|
|
||||||
uniPacket.servantName = "StatSvc"
|
|
||||||
uniPacket.funcName = "SvcReqGetDevLoginInfo"
|
|
||||||
uniPacket.put("SvcReqGetDevLoginInfo", req)
|
|
||||||
val resp = sendBufferAW("StatSvc.GetDevLoginInfo", false, uniPacket.encode())
|
|
||||||
?: return null
|
|
||||||
|
|
||||||
return Packet.decodePacket(resp, "SvcRspGetDevLoginInfo", SvcRspGetDevLoginInfo()).vecCurrentLoginDevInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,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<List<oidb_cmd0x102a.PSKey>> {
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet
|
|
||||||
|
|
||||||
internal object VisitorSvc: BaseSvc() {
|
|
||||||
const val FROM_C2C_AIO = 2
|
|
||||||
const val FROM_CONDITION_SEARCH = 9
|
|
||||||
const val FROM_CONTACTS_TAB = 5
|
|
||||||
const val FROM_FACE_2_FACE_ADD_FRIEND = 11
|
|
||||||
const val FROM_MAYKNOW_FRIEND = 3
|
|
||||||
const val FROM_QCIRCLE = 4
|
|
||||||
const val FROM_QQ_TROOP = 1
|
|
||||||
const val FROM_QZONE = 7
|
|
||||||
const val FROM_SCAN = 6
|
|
||||||
const val FROM_SEARCH = 8
|
|
||||||
const val FROM_SETTING_ME = 12
|
|
||||||
const val FROM_SHARE_CARD = 10
|
|
||||||
|
|
||||||
const val IS_BLACK_LIST = "is_blacklist_user_profile"
|
|
||||||
const val PROFILE_CARD_IS_BLACK = 2
|
|
||||||
const val PROFILE_CARD_IS_BLACKED = 1
|
|
||||||
const val PROFILE_CARD_NOT_BLACK = 3
|
|
||||||
|
|
||||||
const val SUB_FROM_C2C_AIO = 21
|
|
||||||
const val SUB_FROM_C2C_INTERACTIVE_LOGO = 25
|
|
||||||
const val SUB_FROM_C2C_LEFT_SLIDE = 23
|
|
||||||
const val SUB_FROM_C2C_OTHER = 24
|
|
||||||
const val SUB_FROM_C2C_SETTING = 22
|
|
||||||
const val SUB_FROM_C2C_TOFU = 26
|
|
||||||
const val SUB_FROM_CONDITION_SEARCH_OTHER = 99
|
|
||||||
const val SUB_FROM_CONDITION_SEARCH_RESULT = 91
|
|
||||||
const val SUB_FROM_CONTACTS_FRIEND_TAB = 51
|
|
||||||
const val SUB_FROM_CONTACTS_TAB = 55
|
|
||||||
const val SUB_FROM_FACE_2_FACE_ADD_FRIEND_RESULT_AVATAR = 111
|
|
||||||
const val SUB_FROM_FACE_2_FACE_OTHER = 119
|
|
||||||
const val SUB_FROM_FRIEND_APPLY = 56
|
|
||||||
const val SUB_FROM_FRIEND_NOTIFY_MORE = 57
|
|
||||||
const val SUB_FROM_FRIEND_NOTIFY_TAB = 54
|
|
||||||
const val SUB_FROM_GROUPING_TAB = 52
|
|
||||||
const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB = 31
|
|
||||||
const val SUB_FROM_MAYKNOW_FRIEND_CONTACT_TAB_MORE = 37
|
|
||||||
const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE = 34
|
|
||||||
const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_MORE = 39
|
|
||||||
const val SUB_FROM_MAYKNOW_FRIEND_FIND_PEOPLE_SEARCH = 36
|
|
||||||
const val SUB_FROM_MAYKNOW_FRIEND_NEW_FRIEND_PAGE = 32
|
|
||||||
const val SUB_FROM_MAYKNOW_FRIEND_OTHER = 35
|
|
||||||
const val SUB_FROM_MAYKNOW_FRIEND_SEARCH = 33
|
|
||||||
const val SUB_FROM_MAYKNOW_FRIEND_SEARCH_MORE = 38
|
|
||||||
const val SUB_FROM_PHONE_LIST_TAB = 53
|
|
||||||
const val SUB_FROM_QCIRCLE_OTHER = 42
|
|
||||||
const val SUB_FROM_QCIRCLE_PROFILE = 41
|
|
||||||
const val SUB_FROM_QQ_TROOP_ACTIVE_MEMBER = 15
|
|
||||||
const val SUB_FROM_QQ_TROOP_ADMIN = 16
|
|
||||||
const val SUB_FROM_QQ_TROOP_AIO = 11
|
|
||||||
const val SUB_FROM_QQ_TROOP_MEMBER = 12
|
|
||||||
const val SUB_FROM_QQ_TROOP_OTHER = 14
|
|
||||||
const val SUB_FROM_QQ_TROOP_SETTING_MEMBER_LIST = 17
|
|
||||||
const val SUB_FROM_QQ_TROOP_TEMP_SESSION = 13
|
|
||||||
const val SUB_FROM_QRCODE_SCAN_DRAWER = 64
|
|
||||||
const val SUB_FROM_QRCODE_SCAN_NEW = 61
|
|
||||||
const val SUB_FROM_QRCODE_SCAN_OLD = 62
|
|
||||||
const val SUB_FROM_QRCODE_SCAN_OTHER = 69
|
|
||||||
const val SUB_FROM_QRCODE_SCAN_PROFILE = 63
|
|
||||||
const val SUB_FROM_QZONE_HOME = 71
|
|
||||||
const val SUB_FROM_QZONE_OTHER = 79
|
|
||||||
const val SUB_FROM_SEARCH_CONTACT_TAB_MORE_FIND_PROFILE = 83
|
|
||||||
const val SUB_FROM_SEARCH_FIND_PROFILE_TAB = 82
|
|
||||||
const val SUB_FROM_SEARCH_MESSAGE_TAB_MORE_FIND_PROFILE = 84
|
|
||||||
const val SUB_FROM_SEARCH_NEW_FRIEND_MORE_FIND_PROFILE = 85
|
|
||||||
const val SUB_FROM_SEARCH_OTHER = 89
|
|
||||||
const val SUB_FROM_SEARCH_TAB = 81
|
|
||||||
const val SUB_FROM_SETTING_ME_AVATAR = 121
|
|
||||||
const val SUB_FROM_SETTING_ME_OTHER = 129
|
|
||||||
const val SUB_FROM_SHARE_CARD_C2C = 101
|
|
||||||
const val SUB_FROM_SHARE_CARD_OTHER = 109
|
|
||||||
const val SUB_FROM_SHARE_CARD_TROOP = 102
|
|
||||||
const val SUB_FROM_TYPE_DEFAULT = 0
|
|
||||||
|
|
||||||
suspend fun vote(target: Long, count: Int): Result<Unit> {
|
|
||||||
if(count !in 1 .. 20) {
|
|
||||||
return Result.failure(IllegalArgumentException("vote count must be in 1 .. 20"))
|
|
||||||
}
|
|
||||||
val card = CardSvc.getProfileCard(target).onFailure {
|
|
||||||
return Result.failure(RuntimeException("unable to fetch contact info"))
|
|
||||||
}.getOrThrow()
|
|
||||||
sendExtra("VisitorSvc.ReqFavorite") {
|
|
||||||
it.putLong(moe.fuqiuluo.shamrock.remote.service.data.profile.ProfileProtocolConst.PARAM_SELF_UIN, currentUin.toLong())
|
|
||||||
it.putLong(moe.fuqiuluo.shamrock.remote.service.data.profile.ProfileProtocolConst.PARAM_TARGET_UIN, target)
|
|
||||||
it.putByteArray("vCookies", card.vCookies)
|
|
||||||
it.putBoolean("nearby_people", true)
|
|
||||||
it.putInt("favoriteSource", FROM_CONTACTS_TAB)
|
|
||||||
it.putInt("iCount", count)
|
|
||||||
it.putInt("from", FROM_CONTACTS_TAB)
|
|
||||||
}
|
|
||||||
return Result.success(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.ark
|
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
|
|
||||||
import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
|
|
||||||
|
|
||||||
internal object ArkMsgSvc: BaseSvc() {
|
|
||||||
fun tryShareMusic(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: Long,
|
|
||||||
msgId: Long,
|
|
||||||
arkAppInfo: ArkAppInfo,
|
|
||||||
title: String,
|
|
||||||
singer: String,
|
|
||||||
jumpUrl: String,
|
|
||||||
previewUrl: String,
|
|
||||||
musicUrl: String,
|
|
||||||
) {
|
|
||||||
val req = oidb_cmd0xb77.ReqBody()
|
|
||||||
req.appid.set(arkAppInfo.appId)
|
|
||||||
req.app_type.set(1)
|
|
||||||
req.msg_style.set(4)
|
|
||||||
req.client_info.set(oidb_cmd0xb77.ClientInfo().also {
|
|
||||||
it.platform.set(1)
|
|
||||||
it.sdk_version.set(arkAppInfo.version)
|
|
||||||
it.android_package_name.set(arkAppInfo.packageName)
|
|
||||||
it.android_signature.set(arkAppInfo.signature)
|
|
||||||
})
|
|
||||||
req.ext_info.set(oidb_cmd0xb77.ExtInfo().also {
|
|
||||||
it.msg_seq.set(msgId)
|
|
||||||
})
|
|
||||||
req.recv_uin.set(peerId)
|
|
||||||
req.rich_msg_body.set(oidb_cmd0xb77.RichMsgBody().also {
|
|
||||||
it.title.set(title)
|
|
||||||
it.summary.set(singer)
|
|
||||||
it.url.set(jumpUrl)
|
|
||||||
it.picture_url.set(previewUrl)
|
|
||||||
it.music_url.set(musicUrl)
|
|
||||||
})
|
|
||||||
when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> req.send_type.set(1)
|
|
||||||
MsgConstant.KCHATTYPEC2C -> req.send_type.set(0)
|
|
||||||
else -> error("不支持该聊天类型发送音乐分享")
|
|
||||||
}
|
|
||||||
sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray())
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
suspend fun tryShareJsonMessage(
|
|
||||||
jsonString: String,
|
|
||||||
arkAppInfo: ArkAppInfo = ArkAppInfo.DanMaKu,
|
|
||||||
): Result<String> {
|
|
||||||
val msgSeq = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEC2C).qqMsgId
|
|
||||||
val req = oidb_cmd0xb77.ReqBody()
|
|
||||||
req.appid.set(arkAppInfo.appId)
|
|
||||||
req.app_type.set(1)
|
|
||||||
req.msg_style.set(10)
|
|
||||||
req.client_info.set(oidb_cmd0xb77.ClientInfo().also {
|
|
||||||
it.platform.set(1)
|
|
||||||
it.sdk_version.set(arkAppInfo.version)
|
|
||||||
it.android_package_name.set(arkAppInfo.packageName)
|
|
||||||
it.android_signature.set(arkAppInfo.signature)
|
|
||||||
})
|
|
||||||
req.ext_info.set(oidb_cmd0xb77.ExtInfo().also {
|
|
||||||
it.tag_name.set(ByteStringMicro.copyFromUtf8("shamrock"))
|
|
||||||
it.msg_seq.set(msgSeq)
|
|
||||||
})
|
|
||||||
req.send_type.set(0)
|
|
||||||
req.recv_uin.set(TicketSvc.getLongUin())
|
|
||||||
req.mini_app_msg_body.set(oidb_cmd0xb77.MiniAppMsgBody().also {
|
|
||||||
it.mini_app_appid.set(arkAppInfo.miniAppId)
|
|
||||||
it.mini_app_path.set("pages")
|
|
||||||
it.web_page_url.set("https://im.qq.com/index/")
|
|
||||||
it.title.set("title")
|
|
||||||
it.desc.set("desc")
|
|
||||||
it.json_str.set(jsonString)
|
|
||||||
})
|
|
||||||
sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray())
|
|
||||||
val signedJson: String = withTimeoutOrNull(5.seconds) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
AioListener.registerTemporaryMsgListener(msgSeq) {
|
|
||||||
it.resume(elements.first {
|
|
||||||
it.elementType == MsgConstant.KELEMTYPEARKSTRUCT
|
|
||||||
}.arkElement.bytesData)
|
|
||||||
}
|
|
||||||
it.invokeOnCancellation {
|
|
||||||
AioListener.unregisterTemporaryMsgListener(msgSeq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: return Result.failure(Exception("unable to sign json"))
|
|
||||||
return Result.success(signedJson)
|
|
||||||
}*/
|
|
||||||
}
|
|
@ -1,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<String> {
|
|
||||||
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<QWebRsp>()?.buffer?.decodeProtobuf<AdaptShareInfoResp>()
|
|
||||||
if (rsp == null || rsp.json.isNullOrEmpty())
|
|
||||||
return Result.failure(Exception("unable to adapt ShareInfo"))
|
|
||||||
return Result.success(rsp.json!!)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<JsonObject> {
|
|
||||||
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<List<Region>> {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
@ -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?
|
|
||||||
)
|
|
@ -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<MessageSegment> {
|
|
||||||
val messageData = arrayListOf<MessageSegment>()
|
|
||||||
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<MsgElement>.toSegments(chatType: Int, peerId: String, subPeer: String): List<MessageSegment> {
|
|
||||||
val messageData = arrayListOf<MessageSegment>()
|
|
||||||
this.forEach { msg ->
|
|
||||||
kotlin.runCatching {
|
|
||||||
val converter = NtMsgElementConverter[msg.elementType]
|
|
||||||
converter?.invoke(chatType, peerId, subPeer, msg)
|
|
||||||
?: throw UnsupportedOperationException("不支持的消息element类型:${msg.elementType}")
|
|
||||||
}.onSuccess {
|
|
||||||
messageData.add(it)
|
|
||||||
}.onFailure {
|
|
||||||
if (it is UnknownError) {
|
|
||||||
// 不处理的消息类型,抛出unknown error
|
|
||||||
} else {
|
|
||||||
LogCenter.log("消息element转换错误:$it, elementType: ${msg.elementType}", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return messageData
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun List<MsgElement>.toCQCode(chatType: Int, peerId: String, subPeer: String): String {
|
|
||||||
if (this.isEmpty()) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return MessageHelper.nativeEncodeCQCode(this.toSegments(chatType, peerId, subPeer).map {
|
|
||||||
val params = hashMapOf<String, String>()
|
|
||||||
params["_type"] = it.type
|
|
||||||
it.data.forEach { (key, value) ->
|
|
||||||
params[key] = value.toString()
|
|
||||||
}
|
|
||||||
params
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.msg
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonArray
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import moe.fuqiuluo.shamrock.tools.json
|
|
||||||
|
|
||||||
|
|
||||||
internal data class MessageSegment(
|
|
||||||
val type: String,
|
|
||||||
val data: Map<String, Any?> = emptyMap()
|
|
||||||
) {
|
|
||||||
fun toJson(): JsonObject {
|
|
||||||
return mapOf(
|
|
||||||
"type" to type,
|
|
||||||
"data" to data
|
|
||||||
).json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun List<MessageSegment>.toJson(): JsonArray {
|
|
||||||
return this.map {
|
|
||||||
it.toJson()
|
|
||||||
}.json
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> {
|
|
||||||
return this.map {
|
|
||||||
mapOf(
|
|
||||||
"type" to it.type.json,
|
|
||||||
"data" to it.data.json
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.msg
|
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import java.util.Collections
|
|
||||||
|
|
||||||
internal object MessageTempHandler {
|
|
||||||
// 通过MSG SEQ临时监听器
|
|
||||||
private val tempMessageListenerMap = Collections.synchronizedMap(HashMap<Long, suspend MsgRecord.() -> Unit>())
|
|
||||||
|
|
||||||
fun registerTemporaryMsgListener(
|
|
||||||
msgSeq: Long,
|
|
||||||
listener: suspend MsgRecord.() -> Unit
|
|
||||||
) {
|
|
||||||
LogCenter.log({ "注册临时消息监听器: $msgSeq" }, Level.DEBUG)
|
|
||||||
tempMessageListenerMap[msgSeq] = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
fun unregisterTemporaryMsgListener(msgSeq: Long) {
|
|
||||||
tempMessageListenerMap.remove(msgSeq)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun notify(record: MsgRecord): Boolean {
|
|
||||||
tempMessageListenerMap.firstNotNullOfOrNull {
|
|
||||||
if (it.key == record.msgSeq) it else null
|
|
||||||
}?.let {
|
|
||||||
it.value(record)
|
|
||||||
tempMessageListenerMap.remove(it.key)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,593 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.msg.converter
|
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import io.ktor.util.*
|
|
||||||
import kotlinx.io.core.ByteReadPacket
|
|
||||||
import kotlinx.io.core.discardExact
|
|
||||||
import kotlinx.io.core.readUInt
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.helper.db.ImageDB
|
|
||||||
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
|
|
||||||
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonArray
|
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asString
|
|
||||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
|
||||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
|
||||||
import moe.fuqiuluo.shamrock.tools.slice
|
|
||||||
import protobuf.message.Elem
|
|
||||||
import protobuf.message.element.commelem.ButtonExtra
|
|
||||||
import protobuf.message.element.commelem.MarkdownExtra
|
|
||||||
import protobuf.message.element.commelem.QFaceExtra
|
|
||||||
|
|
||||||
|
|
||||||
internal typealias IElemConverter = suspend (Int, String, String, Elem) -> MessageSegment
|
|
||||||
|
|
||||||
internal object ElemConverter {
|
|
||||||
private val convertMap = mapOf(
|
|
||||||
1 to ElemConverter::convertTextElem,
|
|
||||||
2 to ElemConverter::convertFaceElem,
|
|
||||||
4 to ElemConverter::convertNotOnlineImageElem,
|
|
||||||
8 to ElemConverter::convertCustomFaceElem,
|
|
||||||
// MsgConstant.KELEMTYPEPTT to ElemConverter::convertVoiceElem,
|
|
||||||
// MsgConstant.KELEMTYPEVIDEO to ElemConverter::convertVideoElem,
|
|
||||||
// MsgConstant.KELEMTYPEMARKETFACE to ElemConverter::convertMarketFaceElem,
|
|
||||||
37 to ElemConverter::convertGeneralFlagsElem,
|
|
||||||
45 to ElemConverter::convertReplyElem,
|
|
||||||
51 to ElemConverter::convertStructJsonElem,
|
|
||||||
53 to ElemConverter::convertCommonElem,
|
|
||||||
// MsgConstant.KELEMTYPEGRAYTIP to ElemConverter::convertGrayTipsElem,
|
|
||||||
// MsgConstant.KELEMTYPEFILE to ElemConverter::convertFileElem,
|
|
||||||
// //MsgConstant.KELEMTYPEMULTIFORWARD to ElemConverter::convertXmlMultiMsgElem,
|
|
||||||
// //MsgConstant.KELEMTYPESTRUCTLONGMSG to ElemConverter::convertXmlLongMsgElem,
|
|
||||||
// MsgConstant.KELEMTYPEFACEBUBBLE to ElemConverter::convertBubbleFaceElem,
|
|
||||||
)
|
|
||||||
|
|
||||||
operator fun get(type: Int): IElemConverter? = convertMap[type]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文本 / 艾特 消息转换消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertTextElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: Elem
|
|
||||||
): MessageSegment {
|
|
||||||
val text = element.text!!
|
|
||||||
if (text.attr6Buf != null) {
|
|
||||||
val at = ByteReadPacket(text.attr6Buf!!)
|
|
||||||
at.discardExact(7)
|
|
||||||
val uin = at.readUInt()
|
|
||||||
return MessageSegment(
|
|
||||||
type = "at",
|
|
||||||
data = mapOf(
|
|
||||||
"qq" to uin
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return MessageSegment(
|
|
||||||
type = "text",
|
|
||||||
data = mapOf(
|
|
||||||
"text" to text.str!!
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 小表情 / 戳一戳 消息转换消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertFaceElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: Elem
|
|
||||||
): MessageSegment {
|
|
||||||
val face = element.face!!
|
|
||||||
return MessageSegment(
|
|
||||||
type = "face",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to face.index!!
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 图片消息转换消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertCustomFaceElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: Elem
|
|
||||||
): MessageSegment {
|
|
||||||
val customFace = element.customFace!!
|
|
||||||
|
|
||||||
val md5 = customFace.md5.toHexString()
|
|
||||||
|
|
||||||
ImageDB.getInstance().imageMappingDao().insert(
|
|
||||||
ImageMapping(
|
|
||||||
fileName = md5.uppercase(),
|
|
||||||
md5 = md5.uppercase(),
|
|
||||||
chatType = chatType,
|
|
||||||
size = customFace.size!!.toLong(),
|
|
||||||
sha = "",
|
|
||||||
fileId = "",
|
|
||||||
storeId = 0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val origUrl = customFace.origUrl!!
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "image",
|
|
||||||
data = mapOf(
|
|
||||||
"file" to md5,
|
|
||||||
"url" to when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
|
||||||
origUrl,
|
|
||||||
md5
|
|
||||||
)
|
|
||||||
|
|
||||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
|
|
||||||
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
|
||||||
},
|
|
||||||
"type" to if (customFace.origin == true) "original" else "show"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun convertNotOnlineImageElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: Elem
|
|
||||||
): MessageSegment {
|
|
||||||
val notOnlineImage = element.notOnlineImage!!
|
|
||||||
|
|
||||||
val md5 = notOnlineImage.picMd5.toHexString()
|
|
||||||
|
|
||||||
ImageDB.getInstance().imageMappingDao().insert(
|
|
||||||
ImageMapping(
|
|
||||||
fileName = md5.uppercase(),
|
|
||||||
md5 = md5.uppercase(),
|
|
||||||
chatType = chatType,
|
|
||||||
size = notOnlineImage.fileLen!!.toLong(),
|
|
||||||
sha = "",
|
|
||||||
fileId = "",
|
|
||||||
storeId = 0,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val origUrl = notOnlineImage.origUrl!!
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "image",
|
|
||||||
data = mapOf(
|
|
||||||
"file" to md5,
|
|
||||||
"url" to when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
|
||||||
origUrl,
|
|
||||||
md5
|
|
||||||
)
|
|
||||||
|
|
||||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
|
|
||||||
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
|
||||||
},
|
|
||||||
"type" to if (notOnlineImage.original == true) "original" else "show"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun convertGeneralFlagsElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: Elem
|
|
||||||
): MessageSegment {
|
|
||||||
val generalFlags = element.generalFlags!!
|
|
||||||
if (generalFlags.longTextFlag == 1u) {
|
|
||||||
return MessageSegment(
|
|
||||||
type = "general_flags",
|
|
||||||
data = mapOf(
|
|
||||||
"res_id" to generalFlags.longTextResid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
throw UnknownError("no segment")
|
|
||||||
}
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * 视频消息转换消息段
|
|
||||||
// */
|
|
||||||
// private suspend fun convertVideoElem(
|
|
||||||
// chatType: Int,
|
|
||||||
// peerId: String,
|
|
||||||
// subPeer: String,
|
|
||||||
// element: Elem
|
|
||||||
// ): MessageSegment {
|
|
||||||
// val video = element.videoElement
|
|
||||||
// val md5 = if (video.fileName.contains("/")) {
|
|
||||||
// video.videoMd5.takeIf {
|
|
||||||
// !it.isNullOrEmpty()
|
|
||||||
// }?.hex2ByteArray() ?: video.fileName.split("/").let {
|
|
||||||
// it[it.size - 2].hex2ByteArray()
|
|
||||||
// }
|
|
||||||
// } else video.fileName.split(".")[0].hex2ByteArray()
|
|
||||||
//
|
|
||||||
// //LogCenter.log({ "receive video msg: $video" }, Level.DEBUG)
|
|
||||||
//
|
|
||||||
// return MessageSegment(
|
|
||||||
// type = "video",
|
|
||||||
// data = mapOf(
|
|
||||||
// "file" to video.fileName,
|
|
||||||
// "url" to when (chatType) {
|
|
||||||
// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
|
||||||
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
|
|
||||||
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
|
||||||
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
|
||||||
// }
|
|
||||||
// ).also {
|
|
||||||
// if ((it["url"] as String).isBlank())
|
|
||||||
// it.remove("url")
|
|
||||||
// }
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * 商城大表情消息转换消息段
|
|
||||||
// */
|
|
||||||
// private suspend fun convertMarketFaceElem(
|
|
||||||
// chatType: Int,
|
|
||||||
// peerId: String,
|
|
||||||
// subPeer: String,
|
|
||||||
// element: Elem
|
|
||||||
// ): MessageSegment {
|
|
||||||
// val face = element.marketFaceElement
|
|
||||||
// return when (face.emojiId.lowercase()) {
|
|
||||||
// "4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice")
|
|
||||||
// "83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps")
|
|
||||||
// else -> MessageSegment(
|
|
||||||
// type = "mface",
|
|
||||||
// data = mapOf(
|
|
||||||
// "id" to face.emojiId
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
/**
|
|
||||||
* 回复消息转消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertReplyElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: Elem
|
|
||||||
): MessageSegment {
|
|
||||||
val srcMsg = element.srcMsg!!
|
|
||||||
val msgId = srcMsg.pbReserve?.msgRand?.toLong() ?: 0
|
|
||||||
val msgHash = if (msgId != 0L) {
|
|
||||||
MessageHelper.generateMsgIdHash(chatType, msgId)
|
|
||||||
} else {
|
|
||||||
val msgSeq = srcMsg.origSeqs?.first()?.toInt() ?: 0
|
|
||||||
MessageDB.getInstance().messageMappingDao()
|
|
||||||
.queryByMsgSeq(chatType, peerId, msgSeq)?.msgHashId
|
|
||||||
?: kotlin.run {
|
|
||||||
LogCenter.log("消息映射关系未找到: Message($msgSeq)", Level.WARN)
|
|
||||||
MessageHelper.generateMsgIdHash(chatType, msgId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "reply",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to msgHash
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON消息转消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertStructJsonElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: Elem
|
|
||||||
): MessageSegment {
|
|
||||||
val data = element.lightApp!!.data!!
|
|
||||||
val jsonStr = String(if (data[0].toInt() == 1) DeflateTools.uncompress(data.slice(1)) else data.slice(1))
|
|
||||||
val json = jsonStr.asJsonObject
|
|
||||||
return when (json["app"].asString) {
|
|
||||||
"com.tencent.multimsg" -> {
|
|
||||||
val info = json["meta"].asJsonObject["detail"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "forward",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to info["resid"].asString,
|
|
||||||
"filename" to info["uniseq"].asString,
|
|
||||||
"summary" to info["summary"].asString,
|
|
||||||
"desc" to info["news"].asJsonArray.joinToString("\n") { it.asJsonObject["text"].asString }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"com.tencent.troopsharecard" -> {
|
|
||||||
val info = json["meta"].asJsonObject["contact"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "contact",
|
|
||||||
data = mapOf(
|
|
||||||
"type" to "group",
|
|
||||||
"id" to info["jumpUrl"].asString.split("group_code=")[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"com.tencent.contact.lua" -> {
|
|
||||||
val info = json["meta"].asJsonObject["contact"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "contact",
|
|
||||||
data = mapOf(
|
|
||||||
"type" to "private",
|
|
||||||
"id" to info["jumpUrl"].asString.split("uin=")[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"com.tencent.map" -> {
|
|
||||||
val info = json["meta"].asJsonObject["Location.Search"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "location",
|
|
||||||
data = mapOf(
|
|
||||||
"lat" to info["lat"].asString,
|
|
||||||
"lon" to info["lng"].asString,
|
|
||||||
"content" to info["address"].asString,
|
|
||||||
"title" to info["name"].asString
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> MessageSegment(
|
|
||||||
type = "json",
|
|
||||||
data = mapOf(
|
|
||||||
"data" to jsonStr
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private suspend fun convertCommonElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: Elem
|
|
||||||
): MessageSegment {
|
|
||||||
val commonElem = element.commonElem!!
|
|
||||||
return when (commonElem.serviceType) {
|
|
||||||
|
|
||||||
37 -> {
|
|
||||||
val qFaceExtra = commonElem.elem!!.decodeProtobuf<QFaceExtra>()
|
|
||||||
when (qFaceExtra.faceId) {
|
|
||||||
358 -> MessageSegment(
|
|
||||||
type = "dice",
|
|
||||||
data = mapOf(
|
|
||||||
"result" to qFaceExtra.result!!
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
359 -> MessageSegment(
|
|
||||||
type = "rps",
|
|
||||||
data = mapOf(
|
|
||||||
"result" to qFaceExtra.result!!
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> MessageSegment(
|
|
||||||
type = "face",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to qFaceExtra.faceId!!,
|
|
||||||
"big" to true,
|
|
||||||
"result" to qFaceExtra.result!! // (1布 2剪 3锤) (骰子123456)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
45 -> {
|
|
||||||
val markdownExtra = commonElem.elem!!.decodeProtobuf<MarkdownExtra>()
|
|
||||||
MessageSegment(
|
|
||||||
type = "markdown",
|
|
||||||
data = mapOf(
|
|
||||||
"content" to markdownExtra.content!!
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
46 -> {
|
|
||||||
val buttonExtra = commonElem.elem!!.decodeProtobuf<ButtonExtra>()
|
|
||||||
MessageSegment(
|
|
||||||
type = "button",
|
|
||||||
data = buttonExtra.field1!!.let {
|
|
||||||
mapOf(
|
|
||||||
"rows" to it.rows!!.map { row ->
|
|
||||||
mapOf(
|
|
||||||
"buttons" to row.buttons!!.map { button ->
|
|
||||||
val renderData = button.renderData
|
|
||||||
val action = button.action
|
|
||||||
val permission = action?.permission
|
|
||||||
mapOf(
|
|
||||||
"id" to button.id,
|
|
||||||
"render_data" to mapOf(
|
|
||||||
"label" to (renderData?.label ?: ""),
|
|
||||||
"visited_label" to (renderData?.visitedLabel ?: ""),
|
|
||||||
"style" to (renderData?.style ?: 0)
|
|
||||||
),
|
|
||||||
"action" to mapOf(
|
|
||||||
"type" to (action?.type ?: 0),
|
|
||||||
"permission" to mapOf(
|
|
||||||
"type" to (permission?.type ?: 0),
|
|
||||||
"specify_role_ids" to permission?.specifyRoleIds,
|
|
||||||
"specify_user_ids" to permission?.specifyUserIds
|
|
||||||
),
|
|
||||||
"unsupport_tips" to (action?.unsupportTips ?: ""),
|
|
||||||
"data" to (action?.data ?: ""),
|
|
||||||
"reply" to action?.reply,
|
|
||||||
"enter" to action?.enter,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
"appid" to it.appid
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> MessageSegment(
|
|
||||||
type = "common",
|
|
||||||
data = mapOf(
|
|
||||||
"data" to commonElem.elem!!.encodeBase64()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * 灰色提示条消息过滤
|
|
||||||
// */
|
|
||||||
// private suspend fun convertGrayTipsElem(
|
|
||||||
// chatType: Int,
|
|
||||||
// peerId: String,
|
|
||||||
// subPeer: String,
|
|
||||||
// element: Elem
|
|
||||||
// ): MessageSegment {
|
|
||||||
// val tip = element.grayTipElement
|
|
||||||
// when (tip.subElementType) {
|
|
||||||
// MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
|
|
||||||
// val notify = tip.jsonGrayTipElement
|
|
||||||
// when (notify.busiId) {
|
|
||||||
// /* 新人入群 */ 17L, /* 群戳一戳 */1061L,
|
|
||||||
// /* 群撤回 */1014L, /* 群设精消息 */2401L,
|
|
||||||
// /* 群头衔 */2407L -> {
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
|
|
||||||
// val notify = tip.xmlElement
|
|
||||||
// when (notify.busiId) {
|
|
||||||
// /* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
|
|
||||||
// else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
|
|
||||||
// }
|
|
||||||
// // 提示类消息,这里提供的是一个xml,不具备解析通用性
|
|
||||||
// // 在这里不推送
|
|
||||||
// throw UnknownError()
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * 文件消息转换消息段
|
|
||||||
// */
|
|
||||||
// private suspend fun convertFileElem(
|
|
||||||
// chatType: Int,
|
|
||||||
// peerId: String,
|
|
||||||
// subPeer: String,
|
|
||||||
// element: Elem
|
|
||||||
// ): MessageSegment {
|
|
||||||
// val fileMsg = element.fileElement
|
|
||||||
// val fileName = fileMsg.fileName
|
|
||||||
// val fileSize = fileMsg.fileSize
|
|
||||||
// val expireTime = fileMsg.expireTime ?: 0
|
|
||||||
// val fileId = fileMsg.fileUuid
|
|
||||||
// val bizId = fileMsg.fileBizId ?: 0
|
|
||||||
// val fileSubId = fileMsg.fileSubId ?: ""
|
|
||||||
// val url = when (chatType) {
|
|
||||||
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
|
||||||
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
|
|
||||||
// else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// return MessageSegment(
|
|
||||||
// type = "file",
|
|
||||||
// data = mapOf(
|
|
||||||
// "name" to fileName,
|
|
||||||
// "size" to fileSize,
|
|
||||||
// "expire" to expireTime,
|
|
||||||
// "id" to fileId,
|
|
||||||
// "url" to url,
|
|
||||||
// "biz" to bizId,
|
|
||||||
// "sub" to fileSubId
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * 老板QQ的合并转发信息
|
|
||||||
// */
|
|
||||||
// private suspend fun convertXmlMultiMsgElem(
|
|
||||||
// chatType: Int,
|
|
||||||
// peerId: String,
|
|
||||||
// subPeer: String,
|
|
||||||
// element: Elem
|
|
||||||
// ): MessageSegment {
|
|
||||||
// val multiMsg = element.multiForwardElem
|
|
||||||
// return MessageSegment(
|
|
||||||
// type = "forward",
|
|
||||||
// data = mapOf(
|
|
||||||
// "id" to multiMsg.resId
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private suspend fun convertXmlLongMsgElem(
|
|
||||||
// chatType: Int,
|
|
||||||
// peerId: String,
|
|
||||||
// subPeer: String,
|
|
||||||
// element: Elem
|
|
||||||
// ): MessageSegment {
|
|
||||||
// val longMsg = element.structLongElem
|
|
||||||
// return MessageSegment(
|
|
||||||
// type = "forward",
|
|
||||||
// data = mapOf(
|
|
||||||
// "id" to longMsg.resId
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// private suspend fun convertBubbleFaceElem(
|
|
||||||
// chatType: Int,
|
|
||||||
// peerId: String,
|
|
||||||
// subPeer: String,
|
|
||||||
// element: Elem
|
|
||||||
// ): MessageSegment {
|
|
||||||
// val bubbleElement = element.faceBubbleElement
|
|
||||||
// return MessageSegment(
|
|
||||||
// type = "bubble_face",
|
|
||||||
// data = mapOf(
|
|
||||||
// "id" to bubbleElement.yellowFaceInfo.index,
|
|
||||||
// "count" to (bubbleElement.faceCount ?: 1),
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,608 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.msg.converter
|
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
|
|
||||||
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
|
||||||
import moe.fuqiuluo.shamrock.helper.db.ImageDB
|
|
||||||
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
|
|
||||||
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonArray
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asString
|
|
||||||
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
|
||||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
|
||||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils.QQ_9_0_8_VER
|
|
||||||
|
|
||||||
internal typealias IMsgElementConverter = suspend (Int, String, String, MsgElement) -> MessageSegment
|
|
||||||
|
|
||||||
internal object NtMsgElementConverter {
|
|
||||||
private val convertMap = hashMapOf(
|
|
||||||
MsgConstant.KELEMTYPETEXT to NtMsgElementConverter::convertTextElem,
|
|
||||||
MsgConstant.KELEMTYPEFACE to NtMsgElementConverter::convertFaceElem,
|
|
||||||
MsgConstant.KELEMTYPEPIC to NtMsgElementConverter::convertImageElem,
|
|
||||||
MsgConstant.KELEMTYPEPTT to NtMsgElementConverter::convertVoiceElem,
|
|
||||||
MsgConstant.KELEMTYPEVIDEO to NtMsgElementConverter::convertVideoElem,
|
|
||||||
MsgConstant.KELEMTYPEMARKETFACE to NtMsgElementConverter::convertMarketFaceElem,
|
|
||||||
MsgConstant.KELEMTYPEARKSTRUCT to NtMsgElementConverter::convertStructJsonElem,
|
|
||||||
MsgConstant.KELEMTYPEREPLY to NtMsgElementConverter::convertReplyElem,
|
|
||||||
MsgConstant.KELEMTYPEGRAYTIP to NtMsgElementConverter::convertGrayTipsElem,
|
|
||||||
MsgConstant.KELEMTYPEFILE to NtMsgElementConverter::convertFileElem,
|
|
||||||
MsgConstant.KELEMTYPEMARKDOWN to NtMsgElementConverter::convertMarkdownElem,
|
|
||||||
//MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem,
|
|
||||||
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem,
|
|
||||||
MsgConstant.KELEMTYPEFACEBUBBLE to NtMsgElementConverter::convertBubbleFaceElem,
|
|
||||||
MsgConstant.KELEMTYPEINLINEKEYBOARD to NtMsgElementConverter::convertInlineKeyboardElem
|
|
||||||
)
|
|
||||||
|
|
||||||
operator fun get(type: Int): IMsgElementConverter? = convertMap[type]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文本 / 艾特 消息转换消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertTextElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val text = element.textElement
|
|
||||||
return if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
|
|
||||||
MessageSegment(
|
|
||||||
type = "at",
|
|
||||||
data = hashMapOf(
|
|
||||||
"qq" to ContactHelper.getUinByUidAsync(text.atNtUid),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
MessageSegment(
|
|
||||||
type = "text",
|
|
||||||
data = hashMapOf(
|
|
||||||
"text" to text.content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 小表情 / 戳一戳 消息转换消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertFaceElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val face = element.faceElement
|
|
||||||
|
|
||||||
if (face.faceType == 5) {
|
|
||||||
return MessageSegment(
|
|
||||||
type = "poke",
|
|
||||||
data = hashMapOf(
|
|
||||||
"type" to face.pokeType,
|
|
||||||
"id" to face.vaspokeId,
|
|
||||||
"strength" to face.pokeStrength
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
when (face.faceIndex) {
|
|
||||||
114 -> {
|
|
||||||
return MessageSegment(
|
|
||||||
type = "basketball",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.resultId.ifEmpty { "0" }.toInt(),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
358 -> {
|
|
||||||
if (face.sourceType == 1) return MessageSegment("new_dice")
|
|
||||||
return MessageSegment(
|
|
||||||
type = "new_dice",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.resultId.ifEmpty { "0" }.toInt()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
359 -> {
|
|
||||||
if (face.resultId.isEmpty()) return MessageSegment("new_rps")
|
|
||||||
return MessageSegment(
|
|
||||||
type = "new_rps",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.resultId.ifEmpty { "0" }.toInt()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
394 -> {
|
|
||||||
//LogCenter.log(face.toString())
|
|
||||||
return MessageSegment(
|
|
||||||
type = "face",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.faceIndex,
|
|
||||||
"big" to (face.faceType == 3),
|
|
||||||
"result" to (face.resultId ?: "1")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> return MessageSegment(
|
|
||||||
type = "face",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.faceIndex,
|
|
||||||
"big" to (face.faceType == 3)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 图片消息转换消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertImageElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val image = element.picElement
|
|
||||||
val md5 = (image.md5HexStr ?: image.fileName
|
|
||||||
.replace("{", "")
|
|
||||||
.replace("}", "")
|
|
||||||
.replace("-", "").split(".")[0])
|
|
||||||
.uppercase()
|
|
||||||
|
|
||||||
var storeId = 0
|
|
||||||
if (PlatformUtils.getQQVersionCode() > QQ_9_0_8_VER) {
|
|
||||||
storeId = image.storeID
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageDB.getInstance().imageMappingDao().insert(
|
|
||||||
ImageMapping(
|
|
||||||
fileName = md5,
|
|
||||||
md5 = md5,
|
|
||||||
chatType = chatType,
|
|
||||||
size = image.fileSize,
|
|
||||||
sha = "",
|
|
||||||
fileId = image.fileUuid,
|
|
||||||
storeId = storeId,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
//LogCenter.log(image.toString())
|
|
||||||
|
|
||||||
val originalUrl = image.originImageUrl ?: ""
|
|
||||||
LogCenter.log({ "receive image: $image" }, Level.DEBUG)
|
|
||||||
|
|
||||||
/*
|
|
||||||
PicElement{picSubType=0,fileName=A655FCDADABC40D0CEAF6F9AF92937CD.jpg,fileSize=142865,picWidth=886,picHeight=1920,original=false,md5HexStr=a655fcdadabc40d0ceaf6f9af92937cd,sourcePath=null,thumbPath=null,transferStatus=2,progress=0,picType=1000,invalidState=0,fileUuid=CgoxMDI5Mzc0MTE1EhTnucgrUbp3MJjjagUM2-VxSQ5V7hiR3Agg_goo9ZCZt-HNhANQgJqeAQ,fileSubId=,thumbFileSize=0,fileBizId=null,downloadIndex=null,summary=,emojiFrom=null,emojiWebUrl=null,emojiAd=EmojiAD{url=,desc=,},emojiMall=EmojiMall{packageId=0,emojiId=0,},emojiZplan=EmojiZPlan{actionId=0,actionName=,actionType=0,playerNumber=0,peerUid=0,bytesReserveInfo=,},originImageMd5=,originImageUrl=null,importRichMediaContext=null,isFlashPic=false,}
|
|
||||||
*/
|
|
||||||
return MessageSegment(
|
|
||||||
type = "image",
|
|
||||||
data = hashMapOf(
|
|
||||||
"file" to md5,
|
|
||||||
"url" to when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
|
|
||||||
originalUrl = originalUrl,
|
|
||||||
md5 = md5,
|
|
||||||
fileId = image.fileUuid,
|
|
||||||
width = image.picWidth.toUInt(),
|
|
||||||
height = image.picHeight.toUInt(),
|
|
||||||
sha = "",
|
|
||||||
fileSize = image.fileSize.toULong(),
|
|
||||||
peer = peerId
|
|
||||||
)
|
|
||||||
|
|
||||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(
|
|
||||||
originalUrl = originalUrl,
|
|
||||||
md5 = md5,
|
|
||||||
fileId = image.fileUuid,
|
|
||||||
width = image.picWidth.toUInt(),
|
|
||||||
height = image.picHeight.toUInt(),
|
|
||||||
sha = "",
|
|
||||||
fileSize = image.fileSize.toULong(),
|
|
||||||
peer = peerId,
|
|
||||||
storeId = storeId
|
|
||||||
)
|
|
||||||
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(
|
|
||||||
originalUrl = originalUrl,
|
|
||||||
md5 = md5,
|
|
||||||
fileId = image.fileUuid,
|
|
||||||
width = image.picWidth.toUInt(),
|
|
||||||
height = image.picHeight.toUInt(),
|
|
||||||
sha = "",
|
|
||||||
fileSize = image.fileSize.toULong(),
|
|
||||||
peer = peerId,
|
|
||||||
subPeer = subPeer
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
|
||||||
},
|
|
||||||
"subType" to image.picSubType,
|
|
||||||
"type" to if (image.isFlashPic == true) "flash" else if (image.original) "original" else "show"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 语音消息转换消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertVoiceElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val record = element.pttElement
|
|
||||||
|
|
||||||
val md5 = if (record.fileName.startsWith("silk"))
|
|
||||||
record.fileName.substring(5)
|
|
||||||
else record.md5HexStr
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "record",
|
|
||||||
data = hashMapOf(
|
|
||||||
"file" to md5,
|
|
||||||
"url" to when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
|
|
||||||
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
|
|
||||||
"0",
|
|
||||||
md5.hex2ByteArray(),
|
|
||||||
record.fileUuid
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
|
||||||
}
|
|
||||||
).also {
|
|
||||||
if (record.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE) {
|
|
||||||
it["magic"] = "1"
|
|
||||||
}
|
|
||||||
if ((it["url"] as String).isBlank()) {
|
|
||||||
it.remove("url")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 视频消息转换消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertVideoElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val video = element.videoElement
|
|
||||||
val md5 = if (video.fileName.contains("/")) {
|
|
||||||
video.videoMd5.takeIf {
|
|
||||||
!it.isNullOrEmpty()
|
|
||||||
}?.hex2ByteArray() ?: video.fileName.split("/").let {
|
|
||||||
it[it.size - 2].hex2ByteArray()
|
|
||||||
}
|
|
||||||
} else video.fileName.split(".")[0].hex2ByteArray()
|
|
||||||
|
|
||||||
//LogCenter.log({ "receive video msg: $video" }, Level.DEBUG)
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "video",
|
|
||||||
data = hashMapOf(
|
|
||||||
"file" to video.fileName,
|
|
||||||
"url" to when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
|
||||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
|
|
||||||
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
|
|
||||||
}
|
|
||||||
).also {
|
|
||||||
if ((it["url"] as String).isBlank())
|
|
||||||
it.remove("url")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 商城大表情消息转换消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertMarketFaceElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val face = element.marketFaceElement
|
|
||||||
return when (face.emojiId.lowercase()) {
|
|
||||||
"4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice")
|
|
||||||
"83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps")
|
|
||||||
else -> MessageSegment(
|
|
||||||
type = "mface",
|
|
||||||
data = hashMapOf(
|
|
||||||
"id" to face.emojiId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON消息转消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertStructJsonElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val data = element.arkElement.bytesData.asJsonObject
|
|
||||||
return when (data["app"].asString) {
|
|
||||||
"com.tencent.multimsg" -> {
|
|
||||||
val info = data["meta"].asJsonObject["detail"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "forward",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to info["resid"].asString,
|
|
||||||
"filename" to info["uniseq"].asString,
|
|
||||||
"summary" to info["summary"].asString,
|
|
||||||
"desc" to info["news"].asJsonArray.joinToString("\n") { it.asJsonObject["text"].asString }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"com.tencent.troopsharecard" -> {
|
|
||||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "contact",
|
|
||||||
data = hashMapOf(
|
|
||||||
"type" to "group",
|
|
||||||
"id" to info["jumpUrl"].asString.split("group_code=")[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"com.tencent.contact.lua" -> {
|
|
||||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "contact",
|
|
||||||
data = hashMapOf(
|
|
||||||
"type" to "private",
|
|
||||||
"id" to info["jumpUrl"].asString.split("uin=")[1]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
"com.tencent.map" -> {
|
|
||||||
val info = data["meta"].asJsonObject["Location.Search"].asJsonObject
|
|
||||||
MessageSegment(
|
|
||||||
type = "location",
|
|
||||||
data = hashMapOf(
|
|
||||||
"lat" to info["lat"].asString,
|
|
||||||
"lon" to info["lng"].asString,
|
|
||||||
"content" to info["address"].asString,
|
|
||||||
"title" to info["name"].asString
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> MessageSegment(
|
|
||||||
type = "json",
|
|
||||||
data = mapOf(
|
|
||||||
"data" to element.arkElement.bytesData.asJsonObject.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 回复消息转消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertReplyElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val reply = element.replyElement
|
|
||||||
val msgId = reply.replayMsgId
|
|
||||||
val msgHash = if (msgId != 0L) {
|
|
||||||
MessageHelper.generateMsgIdHash(chatType, msgId)
|
|
||||||
} else {
|
|
||||||
MessageDB.getInstance().messageMappingDao()
|
|
||||||
.queryByMsgSeq(chatType, peerId, reply.replayMsgSeq?.toInt() ?: 0)?.msgHashId
|
|
||||||
?: kotlin.run {
|
|
||||||
LogCenter.log("消息映射关系未找到: Message($reply)", Level.WARN)
|
|
||||||
MessageHelper.generateMsgIdHash(chatType, reply.sourceMsgIdInRecords)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "reply",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to msgHash
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 灰色提示条消息过滤
|
|
||||||
*/
|
|
||||||
private suspend fun convertGrayTipsElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val tip = element.grayTipElement
|
|
||||||
when (tip.subElementType) {
|
|
||||||
MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
|
|
||||||
val notify = tip.jsonGrayTipElement
|
|
||||||
when (notify.busiId) {
|
|
||||||
/* 新人入群 */ 17L, /* 群戳一戳 */1061L,
|
|
||||||
/* 群撤回 */1014L, /* 群设精消息 */2401L,
|
|
||||||
/* 群头衔 */2407L -> {
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
|
|
||||||
val notify = tip.xmlElement
|
|
||||||
when (notify.busiId) {
|
|
||||||
/* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
|
|
||||||
else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
|
|
||||||
}
|
|
||||||
// 提示类消息,这里提供的是一个xml,不具备解析通用性
|
|
||||||
// 在这里不推送
|
|
||||||
throw UnknownError()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 文件消息转换消息段
|
|
||||||
*/
|
|
||||||
private suspend fun convertFileElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val fileMsg = element.fileElement
|
|
||||||
val fileName = fileMsg.fileName
|
|
||||||
val fileSize = fileMsg.fileSize
|
|
||||||
val expireTime = fileMsg.expireTime ?: 0
|
|
||||||
val fileId = fileMsg.fileUuid
|
|
||||||
val bizId = fileMsg.fileBizId ?: 0
|
|
||||||
val fileSubId = fileMsg.fileSubId ?: ""
|
|
||||||
val url = when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
|
|
||||||
else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return MessageSegment(
|
|
||||||
type = "file",
|
|
||||||
data = mapOf(
|
|
||||||
"name" to fileName,
|
|
||||||
"size" to fileSize,
|
|
||||||
"expire" to expireTime,
|
|
||||||
"id" to fileId,
|
|
||||||
"url" to url,
|
|
||||||
"biz" to bizId,
|
|
||||||
"sub" to fileSubId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 老板QQ的合并转发信息
|
|
||||||
*/
|
|
||||||
private suspend fun convertXmlMultiMsgElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val multiMsg = element.multiForwardMsgElement
|
|
||||||
return MessageSegment(
|
|
||||||
type = "forward",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to multiMsg.resId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun convertXmlLongMsgElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val longMsg = element.structLongMsgElement
|
|
||||||
return MessageSegment(
|
|
||||||
type = "forward",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to longMsg.resId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun convertMarkdownElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val markdown = element.markdownElement
|
|
||||||
return MessageSegment(
|
|
||||||
type = "markdown",
|
|
||||||
data = mapOf(
|
|
||||||
"content" to markdown.content
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun convertBubbleFaceElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val bubbleElement = element.faceBubbleElement
|
|
||||||
return MessageSegment(
|
|
||||||
type = "bubble_face",
|
|
||||||
data = mapOf(
|
|
||||||
"id" to bubbleElement.yellowFaceInfo.index,
|
|
||||||
"count" to (bubbleElement.faceCount ?: 1),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun convertInlineKeyboardElem(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeer: String,
|
|
||||||
element: MsgElement
|
|
||||||
): MessageSegment {
|
|
||||||
val keyboard = element.inlineKeyboardElement
|
|
||||||
return MessageSegment(
|
|
||||||
type = "button",
|
|
||||||
data = mapOf(
|
|
||||||
"rows" to keyboard.rows.map { row ->
|
|
||||||
mapOf("buttons" to row.buttons.map { button ->
|
|
||||||
mapOf(
|
|
||||||
"id" to button.id,
|
|
||||||
"render_data" to mapOf(
|
|
||||||
"label" to (button?.label ?: ""),
|
|
||||||
"visited_label" to (button?.visitedLabel ?: ""),
|
|
||||||
"style" to (button?.style ?: 0)
|
|
||||||
|
|
||||||
),
|
|
||||||
"action" to mapOf(
|
|
||||||
"type" to (button?.type ?: 0),
|
|
||||||
"permission" to mapOf(
|
|
||||||
"type" to (button?.permissionType ?: 0),
|
|
||||||
"specify_role_ids" to button?.specifyRoleIds,
|
|
||||||
"specify_user_ids" to button?.specifyTinyids
|
|
||||||
),
|
|
||||||
"unsupport_tips" to (button?.unsupportTips ?: ""),
|
|
||||||
"data" to (button?.data ?: ""),
|
|
||||||
"reply" to button?.isReply,
|
|
||||||
"enter" to button?.enter
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
},
|
|
||||||
"appid" to keyboard.botAppid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,718 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.msg.maker
|
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import androidx.exifinterface.media.ExifInterface
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.CardSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.toJson
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc.fetchGroupResUploadTo
|
|
||||||
import moe.fuqiuluo.shamrock.helper.*
|
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToRichText
|
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper.obtainMessageTypeByDetailType
|
|
||||||
import moe.fuqiuluo.shamrock.tools.*
|
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
|
||||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
|
||||||
import protobuf.auto.toByteArray
|
|
||||||
import protobuf.message.Elem
|
|
||||||
import protobuf.message.Ptt
|
|
||||||
import protobuf.message.RichText
|
|
||||||
import protobuf.message.element.*
|
|
||||||
import protobuf.message.element.commelem.*
|
|
||||||
import protobuf.oidb.cmd0x11c5.C2CUserInfo
|
|
||||||
import protobuf.oidb.cmd0x11c5.GroupUserInfo
|
|
||||||
import java.io.File
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.util.*
|
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlin.random.nextULong
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
internal typealias IElemMaker = suspend (ElemMaker, Int, Long, String, JsonObject) -> Unit
|
|
||||||
|
|
||||||
internal class ElemMaker {
|
|
||||||
companion object {
|
|
||||||
private val makerArray = hashMapOf(
|
|
||||||
"text" to ElemMaker::createTextElem,
|
|
||||||
"at" to ElemMaker::createAtElem,
|
|
||||||
"face" to ElemMaker::createFaceElem,
|
|
||||||
"pic" to ElemMaker::createImageElem,
|
|
||||||
"image" to ElemMaker::createImageElem,
|
|
||||||
"reply" to ElemMaker::createReplyElem,
|
|
||||||
"forward" to ElemMaker::createForwardStruct,
|
|
||||||
"weather" to ElemMaker::createWeatherElem,
|
|
||||||
"json" to ElemMaker::createJsonElem,
|
|
||||||
"poke" to ElemMaker::createPokeElem,
|
|
||||||
"dice" to ElemMaker::createNewDiceElem,
|
|
||||||
"rps" to ElemMaker::createNewRpsElem,
|
|
||||||
"markdown" to ElemMaker::createMarkdownElem,
|
|
||||||
"button" to ElemMaker::createButtonElem,
|
|
||||||
// "anonymous" to ElemMaker::createAnonymousElem,
|
|
||||||
// "share" to ElemMaker::createShareElem,
|
|
||||||
// "contact" to ElemMaker::createContactElem,
|
|
||||||
// "location" to ElemMaker::createLocationElem,
|
|
||||||
// "music" to ElemMaker::createMusicElem,
|
|
||||||
// "touch" to ElemMaker::createTouchElem,
|
|
||||||
// "multi_msg" to MessageMaker::createLongMsgStruct,
|
|
||||||
// "bubble_face" to ElemMaker::createBubbleFaceElem,
|
|
||||||
"voice" to ElemMaker::createRecordElem,
|
|
||||||
"record" to ElemMaker::createRecordElem,
|
|
||||||
// "video" to ElemMaker::createVideoElem,
|
|
||||||
)
|
|
||||||
|
|
||||||
operator fun get(type: String): IElemMaker? = makerArray[type]
|
|
||||||
}
|
|
||||||
|
|
||||||
private var rich = RichText()
|
|
||||||
private val elems = mutableListOf<Elem>()
|
|
||||||
private var summary = StringBuilder()
|
|
||||||
|
|
||||||
fun getRich(): RichText {
|
|
||||||
rich.elements = elems
|
|
||||||
return rich
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getDesc(): String {
|
|
||||||
return summary.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createTextElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
data.checkAndThrow("text")
|
|
||||||
val text = data["text"].asString
|
|
||||||
val elem = Elem(
|
|
||||||
text = TextMsg(text)
|
|
||||||
)
|
|
||||||
elems.add(elem)
|
|
||||||
summary.append(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createAtElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> {
|
|
||||||
data.checkAndThrow("qq")
|
|
||||||
val qq: Long
|
|
||||||
val type: Int
|
|
||||||
val display = when (val qqStr = data["qq"].asString) {
|
|
||||||
"0", "all" -> {
|
|
||||||
qq = 0
|
|
||||||
type = 1
|
|
||||||
"@全体成员"
|
|
||||||
}
|
|
||||||
|
|
||||||
"online" -> {
|
|
||||||
qq = 0
|
|
||||||
type = 64
|
|
||||||
"@在线成员"
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
qq = qqStr.toLong()
|
|
||||||
type = 0
|
|
||||||
"@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2(
|
|
||||||
peerId.toLong(),
|
|
||||||
qq,
|
|
||||||
true
|
|
||||||
).let {
|
|
||||||
val info = it.getOrNull()
|
|
||||||
if (info == null)
|
|
||||||
LogCenter.log("无法获取群成员信息: $qqStr", Level.ERROR)
|
|
||||||
else info.troopnick
|
|
||||||
.ifNullOrEmpty(info.friendnick)
|
|
||||||
.ifNullOrEmpty(qqStr)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val attr6: ByteBuffer = ByteBuffer.allocate(6)
|
|
||||||
attr6.put(byteArrayOf(0, 1, 0, 0, 0))
|
|
||||||
attr6.putChar(display.length.toChar())
|
|
||||||
attr6.putChar(type.toChar())
|
|
||||||
attr6.putBuf32Long(qq)
|
|
||||||
attr6.put(byteArrayOf(0, 0))
|
|
||||||
val elem = Elem(
|
|
||||||
text = TextMsg(str = display, attr6Buf = attr6.array())
|
|
||||||
)
|
|
||||||
elems.add(elem)
|
|
||||||
summary.append(display)
|
|
||||||
}
|
|
||||||
|
|
||||||
MsgConstant.KCHATTYPEC2C -> {
|
|
||||||
data.checkAndThrow("qq")
|
|
||||||
|
|
||||||
val qq = data["qq"].asLong
|
|
||||||
val display =
|
|
||||||
"@" + (data["name"].asStringOrNull ?: CardSvc.getProfileCard(qq)
|
|
||||||
.onSuccess {
|
|
||||||
it.strNick.ifNullOrEmpty(qq.toString())
|
|
||||||
}.onFailure {
|
|
||||||
LogCenter.log("无法获取QQ信息: $qq", Level.WARN)
|
|
||||||
})
|
|
||||||
|
|
||||||
val elem = Elem(
|
|
||||||
text = TextMsg(str = display)
|
|
||||||
)
|
|
||||||
elems.add(elem)
|
|
||||||
summary.append(display)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> throw UnsupportedOperationException("Unsupported chatType($chatType) for AtMsg")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createFaceElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
data.checkAndThrow("id")
|
|
||||||
val faceId = data["id"].asInt
|
|
||||||
val elem = if (data["big"].asBooleanOrNull == true) {
|
|
||||||
Elem(
|
|
||||||
commonElem = CommonElem(
|
|
||||||
serviceType = 37,
|
|
||||||
elem = QFaceExtra(
|
|
||||||
packId = "1",
|
|
||||||
stickerId = "1",
|
|
||||||
faceId = faceId,
|
|
||||||
field4 = 1,
|
|
||||||
field5 = 1,
|
|
||||||
result = "",
|
|
||||||
faceText = "", //todo 表情名字
|
|
||||||
field9 = 1
|
|
||||||
).toByteArray(),
|
|
||||||
businessType = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Elem(
|
|
||||||
face = FaceMsg(
|
|
||||||
index = faceId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
elems.add(elem)
|
|
||||||
summary.append("[表情]")
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createImageElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
val type = data["type"].asStringOrNull ?: "original"
|
|
||||||
val isOriginal = type == "original"
|
|
||||||
val filePath = data["file"].asStringOrNull
|
|
||||||
val url = data["url"].asStringOrNull
|
|
||||||
var file: File? = null
|
|
||||||
if (filePath != null) {
|
|
||||||
val md5 = filePath
|
|
||||||
.replace(regex = "[{}\\-]".toRegex(), replacement = "")
|
|
||||||
.split(".")[0].lowercase()
|
|
||||||
file = if (md5.length == 32) {
|
|
||||||
FileUtils.getFileByMd5(md5)
|
|
||||||
} else {
|
|
||||||
FileUtils.parseAndSave(filePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((file == null || !file.exists()) && url != null) {
|
|
||||||
file = FileUtils.parseAndSave(url)
|
|
||||||
}
|
|
||||||
if (file?.exists() == false) {
|
|
||||||
throw LogicException("Image(${file.name}) file is not exists, please check your filename.")
|
|
||||||
}
|
|
||||||
requireNotNull(file)
|
|
||||||
|
|
||||||
val options = BitmapFactory.Options()
|
|
||||||
options.inJustDecodeBounds = true
|
|
||||||
BitmapFactory.decodeFile(file.absolutePath, options)
|
|
||||||
val exifInterface = ExifInterface(file.absolutePath)
|
|
||||||
val orientation = exifInterface.getAttributeInt(
|
|
||||||
ExifInterface.TAG_ORIENTATION,
|
|
||||||
ExifInterface.ORIENTATION_UNDEFINED
|
|
||||||
)
|
|
||||||
val picWidth: Int
|
|
||||||
val picHeight: Int
|
|
||||||
if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) {
|
|
||||||
picWidth = options.outWidth
|
|
||||||
picHeight = options.outHeight
|
|
||||||
} else {
|
|
||||||
picWidth = options.outHeight
|
|
||||||
picHeight = options.outWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
val fileInfo = NtV2RichMediaSvc.tryUploadResourceByNt(
|
|
||||||
chatType = chatType,
|
|
||||||
elementType = MsgConstant.KELEMTYPEPIC,
|
|
||||||
resources = arrayListOf(file),
|
|
||||||
timeout = 30.seconds
|
|
||||||
).getOrThrow().first()
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
fileInfo.uuid.toUInt()
|
|
||||||
}.onFailure {
|
|
||||||
NtV2RichMediaSvc.requestUploadNtPic(file, fileInfo.md5, fileInfo.sha, fileInfo.fileName, picWidth.toUInt(), picHeight.toUInt(), 5, chatType) {
|
|
||||||
when(chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> {
|
|
||||||
sceneType = 2u
|
|
||||||
grp = GroupUserInfo(fetchGroupResUploadTo().toULong())
|
|
||||||
}
|
|
||||||
MsgConstant.KCHATTYPEC2C -> {
|
|
||||||
sceneType = 1u
|
|
||||||
c2c = C2CUserInfo(
|
|
||||||
accountType = 2u,
|
|
||||||
uid = ContactHelper.getUidByUinAsync(peerId.toLong())
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> error("不支持的合并转发图片类型")
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
LogCenter.log("获取MultiMedia图片信息失败: $it", Level.ERROR)
|
|
||||||
}.onSuccess {
|
|
||||||
//LogCenter.log({ "获取MultiMedia图片信息成功: ${it.hashCode()}" }, Level.INFO)
|
|
||||||
elems.add(Elem(
|
|
||||||
commonElem = CommonElem(
|
|
||||||
serviceType = 48,
|
|
||||||
businessType = 10,
|
|
||||||
elem = it.msgInfo!!.toByteArray()
|
|
||||||
)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}.onSuccess { uuid ->
|
|
||||||
elems.add(when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> Elem(
|
|
||||||
customFace = CustomFace(
|
|
||||||
filePath = fileInfo.fileName,
|
|
||||||
fileId = uuid,
|
|
||||||
serverIp = 0u,
|
|
||||||
serverPort = 0u,
|
|
||||||
fileType = FileUtils.getPicType(file).toUInt(),
|
|
||||||
useful = 1u,
|
|
||||||
md5 = fileInfo.md5.hex2ByteArray(),
|
|
||||||
bizType = data["subType"].asIntOrNull?.toUInt(),
|
|
||||||
imageType = FileUtils.getPicType(file).toUInt(),
|
|
||||||
width = picWidth.toUInt(),
|
|
||||||
height = picHeight.toUInt(),
|
|
||||||
size = fileInfo.fileSize.toUInt(),
|
|
||||||
origin = isOriginal,
|
|
||||||
thumbWidth = 0u,
|
|
||||||
thumbHeight = 0u,
|
|
||||||
pbReserve = CustomFace.Companion.PbReserve(
|
|
||||||
field1 = 0,
|
|
||||||
field3 = 0,
|
|
||||||
field4 = 0,
|
|
||||||
field10 = 0,
|
|
||||||
field21 = CustomFace.Companion.Object1(
|
|
||||||
field1 = 0,
|
|
||||||
field2 = "",
|
|
||||||
field3 = 0,
|
|
||||||
field4 = 0,
|
|
||||||
field5 = 0,
|
|
||||||
md5Str = fileInfo.md5
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
MsgConstant.KCHATTYPEC2C -> Elem(
|
|
||||||
notOnlineImage = NotOnlineImage(
|
|
||||||
filePath = fileInfo.fileName,
|
|
||||||
fileLen = fileInfo.fileSize.toUInt(),
|
|
||||||
downloadPath = fileInfo.uuid,
|
|
||||||
imgType = FileUtils.getPicType(file).toUInt(),
|
|
||||||
picMd5 = fileInfo.md5.hex2ByteArray(),
|
|
||||||
picHeight = picWidth.toUInt(),
|
|
||||||
picWidth = picHeight.toUInt(),
|
|
||||||
resId = fileInfo.uuid,
|
|
||||||
original = isOriginal, // true
|
|
||||||
pbReserve = NotOnlineImage.Companion.PbReserve(
|
|
||||||
field1 = 0,
|
|
||||||
field3 = 0,
|
|
||||||
field4 = 0,
|
|
||||||
field10 = 0,
|
|
||||||
field20 = NotOnlineImage.Companion.Object1(
|
|
||||||
field1 = 0,
|
|
||||||
field2 = "",
|
|
||||||
field3 = 0,
|
|
||||||
field4 = 0,
|
|
||||||
field5 = 0,
|
|
||||||
field7 = "",
|
|
||||||
),
|
|
||||||
md5Str = fileInfo.md5
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else -> throw LogicException("Not supported chatType($chatType) for PictureMsg")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
summary.append("[图片]")
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createReplyElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
data.checkAndThrow("id")
|
|
||||||
val msgHash = data["id"].asInt
|
|
||||||
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
|
|
||||||
?: throw Exception("不存在该消息映射,无法回复消息")
|
|
||||||
|
|
||||||
if (mapping.qqMsgId == 0L) {
|
|
||||||
// 貌似获取失败了,555
|
|
||||||
throw Exception("无法获取被回复消息")
|
|
||||||
}
|
|
||||||
|
|
||||||
val elem = if (data.containsKey("text")) {
|
|
||||||
data.checkAndThrow("qq", "time", "seq")
|
|
||||||
Elem(
|
|
||||||
srcMsg = SourceMsg(
|
|
||||||
origSeqs = listOf(data["seq"].asInt),
|
|
||||||
senderUin = data["qq"].asString.toULong(),
|
|
||||||
time = data["time"].asString.toULong(),
|
|
||||||
flag = 1u,
|
|
||||||
elems = listOf(
|
|
||||||
Elem(
|
|
||||||
text = TextMsg(
|
|
||||||
data["text"].asString
|
|
||||||
)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
type = 0u,
|
|
||||||
pbReserve = SourceMsg.Companion.PbReserve(
|
|
||||||
msgRand = Random.nextInt().toULong(),
|
|
||||||
field8 = Random.nextInt(0, 10000)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val msg =
|
|
||||||
MsgSvc.getMsgByQMsgId(chatType, mapping.peerId, mapping.qqMsgId).getOrNull()
|
|
||||||
?: throw Exception("无法获取被回复消息")
|
|
||||||
Elem(
|
|
||||||
srcMsg = SourceMsg(
|
|
||||||
origSeqs = listOf(msg.msgSeq.toInt()),
|
|
||||||
senderUin = msg.senderUin.toULong(),
|
|
||||||
time = msg.msgTime.toULong(),
|
|
||||||
flag = 1u,
|
|
||||||
elems = messageArrayToRichText(
|
|
||||||
msg.chatType,
|
|
||||||
msg.msgId,
|
|
||||||
msg.peerUin.toString(),
|
|
||||||
msg.elements.toSegments(
|
|
||||||
msg.chatType,
|
|
||||||
if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(),
|
|
||||||
msg.channelId ?: msg.peerUin.toString()
|
|
||||||
).toJson()
|
|
||||||
).getOrElse { throw Exception("解析回复消息失败: $it") }.second.elements,
|
|
||||||
type = 0u,
|
|
||||||
pbReserve = SourceMsg.Companion.PbReserve(
|
|
||||||
msgRand = Random.nextULong(),
|
|
||||||
senderUid = msg.senderUid,
|
|
||||||
receiverUid = TicketSvc.getUid(),
|
|
||||||
field8 = Random.nextInt(0, 10000)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
elems.add(elem)
|
|
||||||
summary.append("[回复消息]")
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createJsonElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
data.checkAndThrow("data")
|
|
||||||
|
|
||||||
val elem = Elem(
|
|
||||||
lightApp = LightAppElem(
|
|
||||||
data = byteArrayOf(1) + DeflateTools.compress(data.toString().toByteArray())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elems.add(elem)
|
|
||||||
summary .append( "[Json消息]" )
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createForwardStruct(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
data.checkAndThrow("id")
|
|
||||||
val resId = data["id"].asString
|
|
||||||
val filename = data["filename"].asStringOrNull ?: UUID.randomUUID().toString().uppercase()
|
|
||||||
var summary = data["summary"].asStringOrNull
|
|
||||||
val descriptions = data["desc"].asStringOrNull
|
|
||||||
var news = descriptions?.split("\n")?.map { "text" to it }
|
|
||||||
|
|
||||||
if (news == null || summary == null) {
|
|
||||||
val forwardMsg = MsgSvc.getForwardMsg(resId).getOrThrow()
|
|
||||||
if (news == null) {
|
|
||||||
news = forwardMsg.map {
|
|
||||||
"text" to it.sender.nickName + ": " + messageArrayToRichText(
|
|
||||||
obtainMessageTypeByDetailType(it.msgType),
|
|
||||||
it.qqMsgId,
|
|
||||||
it.peerId.toString(),
|
|
||||||
it.message.json
|
|
||||||
).getOrThrow().first
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (summary == null) {
|
|
||||||
summary = "查看${forwardMsg.size}条转发消息"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val json = mapOf(
|
|
||||||
"app" to "com.tencent.multimsg",
|
|
||||||
"config" to mapOf(
|
|
||||||
"autosize" to 1,
|
|
||||||
"forward" to 1,
|
|
||||||
"round" to 1,
|
|
||||||
"type" to "normal",
|
|
||||||
"width" to 300
|
|
||||||
),
|
|
||||||
"desc" to "[聊天记录]",
|
|
||||||
"extra" to mapOf(
|
|
||||||
"filename" to filename,
|
|
||||||
"tsum" to 2
|
|
||||||
).json.toString(),
|
|
||||||
"meta" to mapOf(
|
|
||||||
"detail" to mapOf(
|
|
||||||
"news" to news,
|
|
||||||
"resid" to resId,
|
|
||||||
"source" to "群聊的聊天记录",
|
|
||||||
"summary" to summary,
|
|
||||||
"uniseq" to filename
|
|
||||||
)
|
|
||||||
),
|
|
||||||
"prompt" to "[聊天记录]",
|
|
||||||
"ver" to "0.0.0.5",
|
|
||||||
"view" to "contact"
|
|
||||||
)
|
|
||||||
val elem = Elem(
|
|
||||||
lightApp = LightAppElem(
|
|
||||||
data = byteArrayOf(1) + DeflateTools.compress(json.json.toString().toByteArray())
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elems.add(elem)
|
|
||||||
this.summary .append( "[聊天记录]" )
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createWeatherElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
var code = data["code"].asIntOrNull
|
|
||||||
|
|
||||||
if (code == null) {
|
|
||||||
data.checkAndThrow("city")
|
|
||||||
val city = data["city"].asString
|
|
||||||
code = WeatherSvc.searchCity(city).onFailure {
|
|
||||||
LogCenter.log("无法获取城市天气: $city", Level.ERROR)
|
|
||||||
}.getOrNull()?.firstOrNull()?.adcode
|
|
||||||
}
|
|
||||||
|
|
||||||
if (code != null) {
|
|
||||||
val weatherCard = WeatherSvc.fetchWeatherCard(code).getOrThrow()
|
|
||||||
// OidbSvc.0xdc2_34
|
|
||||||
// 00 00 00 DF 08 C2 1B 10 22 22 C4 01 0A B7 01 08 A2 E0 F2 2F 10 01 18 00 2A 02 08 01 58 FB 91 F6 AE 02 62 A1 01 08 01 52 08 E5 8C 97 E4 BA AC 20 20 5A 19 2D 33 C2 B0 2F 33 C2 B0 0A E7 A9 BA E6 B0 94 E8 B4 A8 E9 87 8F 3A E8 89 AF 62 11 5B E5 88 86 E4 BA AB 5D 20 E5 8C 97 E4 BA AC 20 20 6A 25 68 74 74 70 73 3A 2F 2F 77 65 61 74 68 65 72 2E 6D 70 2E 71 71 2E 63 6F 6D 2F 3F 73 74 3D 30 26 5F 77 76 3D 31 72 3E 68 74 74 70 73 3A 2F 2F 69 6D 67 63 61 63 68 65 2E 71 71 2E 63 6F 6D 2F 61 63 2F 71 71 77 65 61 74 68 65 72 2F 69 6D 61 67 65 2F 73 68 61 72 65 5F 69 63 6F 6E 2F 66 69 6E 65 2E 70 6E 67 12 08 08 01 10 FB 91 F6 AE 02 32 0D 61 6E 64 72 6F 69 64 20 39 2E 30 2E 38
|
|
||||||
val elem = Elem(
|
|
||||||
lightApp = LightAppElem(
|
|
||||||
data = byteArrayOf(1) + DeflateTools.compress(
|
|
||||||
weatherCard["weekStore"]
|
|
||||||
.asJsonObject["share"].asString.toByteArray()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elems.add(elem)
|
|
||||||
summary .append( "[天气卡片]" )
|
|
||||||
} else {
|
|
||||||
throw LogicException("无法获取城市天气")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createPokeElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
data.checkAndThrow("type", "id")
|
|
||||||
val elem = Elem(
|
|
||||||
commonElem = CommonElem(
|
|
||||||
serviceType = 2,
|
|
||||||
elem = PokeExtra(
|
|
||||||
type = data["type"].asInt,
|
|
||||||
field7 = 0,
|
|
||||||
field8 = 0
|
|
||||||
).toByteArray(),
|
|
||||||
businessType = data["id"].asInt
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elems.add(elem)
|
|
||||||
summary .append( "[戳一戳]" )
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createNewDiceElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
val elem = Elem(
|
|
||||||
commonElem = CommonElem(
|
|
||||||
serviceType = 37,
|
|
||||||
elem = QFaceExtra(
|
|
||||||
packId = "1",
|
|
||||||
stickerId = "33",
|
|
||||||
faceId = 358,
|
|
||||||
field4 = 1,
|
|
||||||
field5 = 2,
|
|
||||||
result = "",
|
|
||||||
faceText = "/骰子",
|
|
||||||
field9 = 1
|
|
||||||
).toByteArray(),
|
|
||||||
businessType = 2
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elems.add(elem)
|
|
||||||
summary .append( "[骰子]" )
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createNewRpsElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
val elem = Elem(
|
|
||||||
commonElem = CommonElem(
|
|
||||||
serviceType = 37,
|
|
||||||
elem = QFaceExtra(
|
|
||||||
packId = "1",
|
|
||||||
stickerId = "34",
|
|
||||||
faceId = 359,
|
|
||||||
field4 = 1,
|
|
||||||
field5 = 2,
|
|
||||||
result = "",
|
|
||||||
faceText = "/包剪锤",
|
|
||||||
field9 = 1
|
|
||||||
).toByteArray(),
|
|
||||||
businessType = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elems.add(elem)
|
|
||||||
summary .append( "[包剪锤]" )
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createMarkdownElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
data.checkAndThrow("content")
|
|
||||||
val elem = Elem(
|
|
||||||
commonElem = CommonElem(
|
|
||||||
serviceType = 45,
|
|
||||||
elem = MarkdownExtra(data["content"].asString).toByteArray(),
|
|
||||||
businessType = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elems.add(elem)
|
|
||||||
summary.append("[Markdown消息]")
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createButtonElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
data.checkAndThrow("rows")
|
|
||||||
val elem = Elem(
|
|
||||||
commonElem = CommonElem(
|
|
||||||
serviceType = 46,
|
|
||||||
elem = ButtonExtra(
|
|
||||||
field1 = Object1(
|
|
||||||
rows = data["rows"].asJsonArray.map { row ->
|
|
||||||
Row(buttons = row.asJsonObject["buttons"].asJsonArray.map {
|
|
||||||
val button = it.asJsonObject
|
|
||||||
val renderData = button["render_data"].asJsonObject
|
|
||||||
val action = button["action"].asJsonObject
|
|
||||||
val permission = action["permission"].asJsonObject
|
|
||||||
Button(
|
|
||||||
id = button["id"].asStringOrNull,
|
|
||||||
renderData = RenderData(
|
|
||||||
label = renderData["label"].asString,
|
|
||||||
visitedLabel = renderData["visited_label"].asString,
|
|
||||||
style = renderData["style"].asInt
|
|
||||||
),
|
|
||||||
action = Action(
|
|
||||||
type = action["type"].asInt,
|
|
||||||
permission = Permission(
|
|
||||||
type = permission["type"].asInt,
|
|
||||||
specifyRoleIds = permission["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString },
|
|
||||||
specifyUserIds = permission["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString }
|
|
||||||
),
|
|
||||||
unsupportTips = action["unsupport_tips"].asString,
|
|
||||||
data = action["data"].asString,
|
|
||||||
reply = action["reply"].asBooleanOrNull,
|
|
||||||
enter = action["enter"].asBooleanOrNull
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
appid = data["appid"].asIntOrNull
|
|
||||||
)
|
|
||||||
).toByteArray(),
|
|
||||||
businessType = 1
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elems.add(elem)
|
|
||||||
summary.append("[Button消息]")
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun createRecordElem(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
data: JsonObject
|
|
||||||
) {
|
|
||||||
data.checkAndThrow("content")
|
|
||||||
rich.ptt= Ptt(
|
|
||||||
|
|
||||||
)
|
|
||||||
summary .append( "[语音消息]" )
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun JsonObject.checkAndThrow(vararg key: String) {
|
|
||||||
key.forEach {
|
|
||||||
if (!containsKey(it)) throw ParamsException(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,53 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.structures
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class FileUrl(
|
|
||||||
@SerialName("url") val url: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GroupFileList(
|
|
||||||
@SerialName("files") val files: List<FileInfo>,
|
|
||||||
@SerialName("folders") val folders: List<FolderInfo>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class FileInfo(
|
|
||||||
@SerialName("group_id") val groupId: Long,
|
|
||||||
@SerialName("file_id") val fileId: String,
|
|
||||||
@SerialName("file_name") val fileName: String,
|
|
||||||
@SerialName("file_size") val fileSize: Long,
|
|
||||||
@SerialName("busid") val busid: Int,
|
|
||||||
@SerialName("upload_time") val uploadTime: Int,
|
|
||||||
@SerialName("dead_time") val deadTime: Int,
|
|
||||||
@SerialName("modify_time") val modifyTime: Int,
|
|
||||||
@SerialName("download_times") val downloadTimes: Int,
|
|
||||||
@SerialName("uploader") val uploadUin: Long,
|
|
||||||
@SerialName("upload_name") val uploadNick: String,
|
|
||||||
@SerialName("sha") val sha: String,
|
|
||||||
@SerialName("sha3") val sha3: String,
|
|
||||||
@SerialName("md5") val md5: String,
|
|
||||||
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class FolderInfo(
|
|
||||||
@SerialName("group_id") val groupId: Long,
|
|
||||||
@SerialName("folder_id") val folderId: String,
|
|
||||||
@SerialName("folder_name") val folderName: String,
|
|
||||||
@SerialName("total_file_count") val totalFileCount: Int,
|
|
||||||
@SerialName("create_time") val createTime: Int,
|
|
||||||
@SerialName("creator") val creator: Long,
|
|
||||||
@SerialName("creator_name") val creatorNick: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class FileSystemInfo(
|
|
||||||
@SerialName("file_count") val fileCount: Int,
|
|
||||||
@SerialName("limit_count") val fileLimitCount: Int,
|
|
||||||
@SerialName("used_space") val usedSpace: Long,
|
|
||||||
@SerialName("total_space") val totalSpace: Long,
|
|
||||||
)
|
|
@ -1,30 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.structures
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
internal data class ProhibitedMemberInfo(
|
|
||||||
@SerialName("user_id") val memberUin: Long,
|
|
||||||
@SerialName("time") val shutuptimestap: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
internal data class GroupAtAllRemainInfo(
|
|
||||||
@SerialName("can_at_all") val canAtAll: Boolean,
|
|
||||||
@SerialName("remain_at_all_count_for_group") val remainAtAllCountForGroup: Int,
|
|
||||||
@SerialName("remain_at_all_count_for_uin") val remainAtAllCountForUin: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
internal data class NotJoinedGroupInfo(
|
|
||||||
@SerialName("group_id") val groupId: Long,
|
|
||||||
@SerialName("max_member_cnt") val maxMember: Int,
|
|
||||||
@SerialName("member_count") val memberCount: Int,
|
|
||||||
@SerialName("group_name") val groupName: String,
|
|
||||||
@SerialName("group_desc") val groupDesc: String,
|
|
||||||
@SerialName("owner") val owner: Long,
|
|
||||||
@SerialName("create_time") val createTime: Long,
|
|
||||||
@SerialName("group_flag") val groupFlag: Int,
|
|
||||||
@SerialName("group_flag_ext") val groupFlagExt: Int,
|
|
||||||
)
|
|
@ -1,79 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.structures
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import moe.fuqiuluo.symbols.Protobuf
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GuildInfo(
|
|
||||||
@SerialName("guild_id") var guildId: Long,
|
|
||||||
@SerialName("guild_name") var guildName: String,
|
|
||||||
@SerialName("guild_display_id") var guildDisplayId: String,
|
|
||||||
@SerialName("profile") var profile: String,
|
|
||||||
@SerialName("status") var status: GuildStatus,
|
|
||||||
@SerialName("owner_id") var ownerId: Long,
|
|
||||||
@SerialName("shutup_expire_time") var shutUpTime: Long,
|
|
||||||
@SerialName("allow_search") var allowSearch: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GuildStatus(
|
|
||||||
@SerialName("is_enable") var isEnable: Boolean,
|
|
||||||
@SerialName("is_banned") var isBanned: Boolean,
|
|
||||||
@SerialName("is_frozen") var isFrozen: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GProChannelInfo(
|
|
||||||
@SerialName("owner_guild_id") val ownerGuildId: ULong,
|
|
||||||
@SerialName("channel_id") val channelId: Long,
|
|
||||||
@SerialName("channel_uin") val channelUin: Long,
|
|
||||||
@SerialName("guild_id") val guildId: String,
|
|
||||||
@SerialName("channel_type") val channelType: Int,
|
|
||||||
@SerialName("channel_name") val channelName: String,
|
|
||||||
@SerialName("create_time") val createTime: Long,
|
|
||||||
@SerialName("max_member_count") val maxMemberCount: Int,
|
|
||||||
@SerialName("creator_tiny_id") val creatorTinyId: Long,
|
|
||||||
@SerialName("talk_permission") val talkPermission: Int,
|
|
||||||
@SerialName("visible_type") val visibleType: Int,
|
|
||||||
@SerialName("current_slow_mode") val currentSlowMode: Int,
|
|
||||||
@SerialName("slow_modes") val slowModes: List<SlowModeInfo>,
|
|
||||||
@SerialName("icon_url") val appIconUrl: String? = null,
|
|
||||||
@SerialName("jump_switch") val jumpSwitch: Int = Int.MIN_VALUE,
|
|
||||||
@SerialName("jump_type") val jumpType: Int = Int.MIN_VALUE,
|
|
||||||
@SerialName("jump_url") val jumpUrl: String? = null,
|
|
||||||
@SerialName("category_id") val categoryId: Long = Long.MIN_VALUE,
|
|
||||||
@SerialName("my_talk_permission") val myTalkPermission: Int = Int.MIN_VALUE,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SlowModeInfo(
|
|
||||||
@SerialName("slow_mode_key") val slowModeKey: Int,
|
|
||||||
@SerialName("slow_mode_text") val slowModeText: String,
|
|
||||||
@SerialName("speak_frequency") val speakFrequency: Int,
|
|
||||||
@SerialName("slow_mode_circle") val slowModeCircle: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GetGuildMemberListNextToken(
|
|
||||||
@SerialName("start_index") val startIndex: Long,
|
|
||||||
@SerialName("role_index") val roleIndex: Long,
|
|
||||||
@SerialName("seq") val seq: Int,
|
|
||||||
@SerialName("finish") val finish: Boolean
|
|
||||||
): Protobuf<GetGuildMemberListNextToken>
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class GuildMemberInfo(
|
|
||||||
@SerialName("tiny_id") val tinyId: Long,
|
|
||||||
@SerialName("title") val title: String,
|
|
||||||
@SerialName("nickname") val nickname: String,
|
|
||||||
@SerialName("role_id") val roleId: Long,
|
|
||||||
@SerialName("role_name") val roleName: String,
|
|
||||||
@SerialName("role_color") val roleColor: Long,
|
|
||||||
@SerialName("join_time") val joinTime: Long,
|
|
||||||
@SerialName("robot_type") val robotType: Int,
|
|
||||||
@SerialName("type") val type: Int,
|
|
||||||
@SerialName("in_black") val inBlack: Boolean,
|
|
||||||
@SerialName("platform") val platform: Int,
|
|
||||||
)
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.structures
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class UploadResult(
|
|
||||||
@SerialName("files") val files: List<CommFileInfo>
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CommFileInfo(
|
|
||||||
@SerialName("mode_id") val modeId: Long,
|
|
||||||
@SerialName("name") val fileName: String,
|
|
||||||
@SerialName("size") val fileSize: Long,
|
|
||||||
@SerialName("md5") val md5: String,
|
|
||||||
@SerialName("uuid") val uuid: String,
|
|
||||||
@SerialName("sub_id") val subId: String,
|
|
||||||
@SerialName("sha") val sha: String,
|
|
||||||
)
|
|
@ -1,190 +0,0 @@
|
|||||||
@file:OptIn(DelicateCoroutinesApi::class)
|
|
||||||
|
|
||||||
package moe.fuqiuluo.qqinterface.servlet.transfile
|
|
||||||
|
|
||||||
import com.tencent.mobileqq.transfile.BaseTransProcessor
|
|
||||||
import com.tencent.mobileqq.transfile.FileMsg
|
|
||||||
import com.tencent.mobileqq.transfile.TransferRequest
|
|
||||||
import com.tencent.mobileqq.transfile.api.ITransFileController
|
|
||||||
import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.utils.MD5
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
|
||||||
import mqq.app.AppRuntime
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.random.Random
|
|
||||||
|
|
||||||
internal abstract class FileTransfer {
|
|
||||||
suspend fun transC2CResource(
|
|
||||||
peerId: String,
|
|
||||||
file: File,
|
|
||||||
fileType: Int, busiType: Int,
|
|
||||||
wait: Boolean = true,
|
|
||||||
builder: (TransferRequest) -> Unit
|
|
||||||
): Boolean {
|
|
||||||
val runtime = AppRuntimeFetcher.appRuntime
|
|
||||||
val transferRequest = TransferRequest()
|
|
||||||
transferRequest.needSendMsg = false
|
|
||||||
transferRequest.mSelfUin = runtime.account
|
|
||||||
transferRequest.mPeerUin = peerId
|
|
||||||
transferRequest.mSecondId = runtime.currentAccountUin
|
|
||||||
transferRequest.mUinType = FileMsg.UIN_BUDDY
|
|
||||||
transferRequest.mFileType = fileType
|
|
||||||
transferRequest.mUniseq = createMessageUniseq()
|
|
||||||
transferRequest.mIsUp = true
|
|
||||||
builder(transferRequest)
|
|
||||||
transferRequest.mBusiType = busiType
|
|
||||||
transferRequest.mMd5 = MD5.genFileMd5Hex(file.absolutePath)
|
|
||||||
transferRequest.mLocalPath = file.absolutePath
|
|
||||||
return transAndWait(runtime, transferRequest, wait)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun transTroopResource(
|
|
||||||
groupId: String,
|
|
||||||
file: File,
|
|
||||||
fileType: Int, busiType: Int,
|
|
||||||
wait: Boolean = true,
|
|
||||||
builder: (TransferRequest) -> Unit
|
|
||||||
): Boolean {
|
|
||||||
val runtime = AppRuntimeFetcher.appRuntime
|
|
||||||
val transferRequest = TransferRequest()
|
|
||||||
transferRequest.needSendMsg = false
|
|
||||||
transferRequest.mSelfUin = runtime.account
|
|
||||||
transferRequest.mPeerUin = groupId
|
|
||||||
transferRequest.mSecondId = runtime.currentAccountUin
|
|
||||||
transferRequest.mUinType = FileMsg.UIN_TROOP
|
|
||||||
transferRequest.mFileType = fileType
|
|
||||||
transferRequest.mUniseq = createMessageUniseq()
|
|
||||||
transferRequest.mIsUp = true
|
|
||||||
builder(transferRequest)
|
|
||||||
transferRequest.mBusiType = busiType
|
|
||||||
transferRequest.mMd5 = MD5.genFileMd5Hex(file.absolutePath)
|
|
||||||
transferRequest.mLocalPath = file.absolutePath
|
|
||||||
return transAndWait(runtime, transferRequest, wait)
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun transAndWait(
|
|
||||||
runtime: AppRuntime,
|
|
||||||
transferRequest: TransferRequest,
|
|
||||||
wait: Boolean
|
|
||||||
): Boolean {
|
|
||||||
return withTimeoutOrNull(60_000) {
|
|
||||||
val service = runtime.getRuntimeService(ITransFileController::class.java, "all")
|
|
||||||
if(service.transferAsync(transferRequest)) {
|
|
||||||
if (!wait) { // 如果无需等待直接返回
|
|
||||||
return@withTimeoutOrNull true
|
|
||||||
}
|
|
||||||
suspendCancellableCoroutine { continuation ->
|
|
||||||
GlobalScope.launch {
|
|
||||||
lateinit var processor: IHttpCommunicatorListener
|
|
||||||
while (
|
|
||||||
//service.findProcessor(
|
|
||||||
// transferRequest.keyForTransfer // uin + uniseq
|
|
||||||
//) != null
|
|
||||||
service.containsProcessor(runtime.currentAccountUin, transferRequest.mUniseq)
|
|
||||||
// 如果上传处理器依旧存在,说明没有上传成功
|
|
||||||
&& service.isWorking.get()
|
|
||||||
) {
|
|
||||||
processor = service.findProcessor(runtime.currentAccountUin, transferRequest.mUniseq)
|
|
||||||
delay(100)
|
|
||||||
}
|
|
||||||
if (processor is BaseTransProcessor && processor.file != null) {
|
|
||||||
val fileMsg = processor.file
|
|
||||||
LogCenter.log("[OldBDH] 资源上传结束(fileId = ${fileMsg.fileID}, fileKey = ${fileMsg.fileKey}, path = ${fileMsg.filePath})")
|
|
||||||
}
|
|
||||||
continuation.resume(true)
|
|
||||||
}
|
|
||||||
// 实现取消上传器
|
|
||||||
// 目前没什么用
|
|
||||||
continuation.invokeOnCancellation {
|
|
||||||
continuation.resume(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else true
|
|
||||||
} ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_AIO_ALBUM_PIC = 1031
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_AIO_KEY_WORD_PIC = 1046
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_AIO_QZONE_PIC = 1045
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_ALBUM_PIC = 1007
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_BLESS = 1056
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_CAPTURE_PIC = 1008
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_COMMEN_FALSH_PIC = 1040
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_CUSTOM = 1006
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_DOUTU_PIC = 1044
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_FALSH_PIC = 1039
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_FAST_IMAGE = 1037
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_FORWARD_EDIT = 1048
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_FORWARD_PIC = 1009
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_FULL_SCREEN_ESSENCE = 1057
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_GALEERY_PIC = 1041
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_GAME_CENTER_STRATEGY = 1058
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_HOT_PIC = 1042
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_MIXED_PICS = 1043
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_PIC_AIO_ALBUM = 1052
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_PIC_CAMERA = 1050
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_PIC_FAV = 1053
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_PIC_SCREEN = 1027
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_PIC_SHARE = 1030
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_PIC_TAB_CAMERA = 1051
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_QQPINYIN_SEND_PIC = 1038
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_RECOMMENDED_STICKER = 1047
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_RELATED_EMOTION = 1054
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_SHOWLOVE = 1036
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_SOGOU_SEND_PIC = 1034
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_TROOP_BAR = 1035
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_WLAN_RECV_NOTIFY = 1055
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_ZHITU_PIC = 1049
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_ZPLAN_EMOTICON_GIF = 1060
|
|
||||||
const val SEND_MSG_BUSINESS_TYPE_ZPLAN_PIC = 1059
|
|
||||||
|
|
||||||
const val VIDEO_FORMAT_AFS = 7
|
|
||||||
const val VIDEO_FORMAT_AVI = 1
|
|
||||||
const val VIDEO_FORMAT_MKV = 4
|
|
||||||
const val VIDEO_FORMAT_MOD = 9
|
|
||||||
const val VIDEO_FORMAT_MOV = 8
|
|
||||||
const val VIDEO_FORMAT_MP4 = 2
|
|
||||||
const val VIDEO_FORMAT_MTS = 11
|
|
||||||
const val VIDEO_FORMAT_RM = 6
|
|
||||||
const val VIDEO_FORMAT_RMVB = 5
|
|
||||||
const val VIDEO_FORMAT_TS = 10
|
|
||||||
const val VIDEO_FORMAT_WMV = 3
|
|
||||||
|
|
||||||
const val BUSI_TYPE_GUILD_VIDEO = 4601
|
|
||||||
const val BUSI_TYPE_MULTI_FORWARD_VIDEO = 1010
|
|
||||||
const val BUSI_TYPE_PUBACCOUNT_PERM_VIDEO = 1009
|
|
||||||
const val BUSI_TYPE_PUBACCOUNT_TEMP_VIDEO = 1007
|
|
||||||
const val BUSI_TYPE_SHORT_VIDEO = 1
|
|
||||||
const val BUSI_TYPE_SHORT_VIDEO_PTV = 2
|
|
||||||
const val BUSI_TYPE_VIDEO = 0
|
|
||||||
const val BUSI_TYPE_VIDEO_EMOTICON_PIC = 1022
|
|
||||||
const val BUSI_TYPE_VIDEO_EMOTICON_VIDEO = 1021
|
|
||||||
|
|
||||||
const val TRANSFILE_TYPE_PIC = 1
|
|
||||||
const val TRANSFILE_TYPE_PIC_EMO = 65538
|
|
||||||
const val TRANSFILE_TYPE_PIC_THUMB = 65537
|
|
||||||
const val TRANSFILE_TYPE_PISMA = 49
|
|
||||||
const val TRANSFILE_TYPE_RAWPIC = 131075
|
|
||||||
|
|
||||||
const val TRANSFILE_TYPE_PROFILE_COVER = 35
|
|
||||||
const val TRANSFILE_TYPE_PTT = 2
|
|
||||||
const val TRANSFILE_TYPE_PTT_SLICE_TO_TEXT = 327696
|
|
||||||
const val TRANSFILE_TYPE_QQHEAD_PIC = 131074
|
|
||||||
|
|
||||||
internal fun createMessageUniseq(time: Long = System.currentTimeMillis()): Long {
|
|
||||||
var uniseq = (time / 1000).toInt().toLong()
|
|
||||||
uniseq = uniseq shl 32 or abs(Random.nextInt()).toLong()
|
|
||||||
return uniseq
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,537 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.transfile
|
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import androidx.exifinterface.media.ExifInterface
|
|
||||||
import com.tencent.mobileqq.qroute.QRoute
|
|
||||||
import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.CommonFileInfo
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.PicElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.PttElement
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.VideoElement
|
|
||||||
import kotlinx.atomicfu.atomic
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.data.TryUpPicData
|
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
|
||||||
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
|
||||||
import moe.fuqiuluo.shamrock.tools.slice
|
|
||||||
import moe.fuqiuluo.shamrock.utils.AudioUtils
|
|
||||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
|
||||||
import moe.fuqiuluo.shamrock.utils.MediaType
|
|
||||||
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.oidb.TrpcOidb
|
|
||||||
import protobuf.oidb.cmd0x11c5.ClientMeta
|
|
||||||
import protobuf.oidb.cmd0x11c5.CodecConfigReq
|
|
||||||
import protobuf.oidb.cmd0x11c5.CommonHead
|
|
||||||
import protobuf.oidb.cmd0x11c5.DownloadExt
|
|
||||||
import protobuf.oidb.cmd0x11c5.DownloadReq
|
|
||||||
import protobuf.oidb.cmd0x11c5.FileInfo
|
|
||||||
import protobuf.oidb.cmd0x11c5.FileType
|
|
||||||
import protobuf.oidb.cmd0x11c5.IndexNode
|
|
||||||
import protobuf.oidb.cmd0x11c5.MultiMediaReqHead
|
|
||||||
import protobuf.oidb.cmd0x11c5.NtV2RichMediaReq
|
|
||||||
import protobuf.oidb.cmd0x11c5.NtV2RichMediaRsp
|
|
||||||
import protobuf.oidb.cmd0x11c5.SceneInfo
|
|
||||||
import protobuf.oidb.cmd0x11c5.UploadInfo
|
|
||||||
import protobuf.oidb.cmd0x11c5.UploadReq
|
|
||||||
import protobuf.oidb.cmd0x11c5.UploadRsp
|
|
||||||
import protobuf.oidb.cmd0x11c5.VideoDownloadExt
|
|
||||||
import protobuf.oidb.cmd0x388.Cmd0x388ReqBody
|
|
||||||
import protobuf.oidb.cmd0x388.Cmd0x388RspBody
|
|
||||||
import protobuf.oidb.cmd0x388.TryUpImgReq
|
|
||||||
import java.io.File
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
import kotlin.random.Random
|
|
||||||
import kotlin.random.nextUInt
|
|
||||||
import kotlin.random.nextULong
|
|
||||||
import kotlin.time.Duration
|
|
||||||
|
|
||||||
internal object NtV2RichMediaSvc: BaseSvc() {
|
|
||||||
private val requestIdSeq = atomic(2L)
|
|
||||||
|
|
||||||
fun fetchGroupResUploadTo(): String {
|
|
||||||
return ShamrockConfig.getUpResGroup().ifEmpty { "100000000" }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun tryUploadResourceByNt(
|
|
||||||
chatType: Int,
|
|
||||||
elementType: Int,
|
|
||||||
resources: ArrayList<File>,
|
|
||||||
timeout: Duration,
|
|
||||||
retryCnt: Int = 5
|
|
||||||
): Result<MutableList<CommonFileInfo>> {
|
|
||||||
return internalTryUploadResourceByNt(chatType, elementType, resources, timeout).onFailure {
|
|
||||||
if (retryCnt > 0) {
|
|
||||||
return tryUploadResourceByNt(chatType, elementType, resources, timeout, retryCnt - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量上传图片
|
|
||||||
*/
|
|
||||||
private suspend fun internalTryUploadResourceByNt(
|
|
||||||
chatType: Int,
|
|
||||||
elementType: Int,
|
|
||||||
resources: ArrayList<File>,
|
|
||||||
timeout: Duration
|
|
||||||
): Result<MutableList<CommonFileInfo>> {
|
|
||||||
require(resources.size in 1 .. 10) { "imageFiles.size() must be in 1 .. 10" }
|
|
||||||
|
|
||||||
val messages = resources.map { file ->
|
|
||||||
val elem = MsgElement()
|
|
||||||
elem.elementType = elementType
|
|
||||||
when(elementType) {
|
|
||||||
MsgConstant.KELEMTYPEPIC -> {
|
|
||||||
val pic = PicElement()
|
|
||||||
pic.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
|
|
||||||
val msgService = NTServiceFetcher.kernelService.msgService!!
|
|
||||||
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
|
|
||||||
RichMediaFilePathInfo(
|
|
||||||
2, 0, pic.md5HexStr, file.name, 1, 0, null, "", true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
|
|
||||||
originalPath
|
|
||||||
) != file.length()
|
|
||||||
) {
|
|
||||||
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
|
|
||||||
RichMediaFilePathInfo(
|
|
||||||
2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
|
|
||||||
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, thumbPath)
|
|
||||||
}
|
|
||||||
val options = BitmapFactory.Options()
|
|
||||||
options.inJustDecodeBounds = true
|
|
||||||
BitmapFactory.decodeFile(file.absolutePath, options)
|
|
||||||
val exifInterface = ExifInterface(file.absolutePath)
|
|
||||||
val orientation = exifInterface.getAttributeInt(
|
|
||||||
ExifInterface.TAG_ORIENTATION,
|
|
||||||
ExifInterface.ORIENTATION_UNDEFINED
|
|
||||||
)
|
|
||||||
if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) {
|
|
||||||
pic.picWidth = options.outWidth
|
|
||||||
pic.picHeight = options.outHeight
|
|
||||||
} else {
|
|
||||||
pic.picWidth = options.outHeight
|
|
||||||
pic.picHeight = options.outWidth
|
|
||||||
}
|
|
||||||
pic.sourcePath = file.absolutePath
|
|
||||||
pic.fileSize = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath)
|
|
||||||
pic.original = true
|
|
||||||
pic.picType = FileUtils.getPicType(file)
|
|
||||||
elem.picElement = pic
|
|
||||||
}
|
|
||||||
MsgConstant.KELEMTYPEPTT -> {
|
|
||||||
require(resources.size == 1) // 语音只能单个上传
|
|
||||||
var pttFile = file
|
|
||||||
val ptt = PttElement()
|
|
||||||
when (AudioUtils.getMediaType(pttFile)) {
|
|
||||||
MediaType.Silk -> {
|
|
||||||
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
|
||||||
ptt.duration = QRoute.api(IAIOPttApi::class.java)
|
|
||||||
.getPttFileDuration(pttFile.absolutePath)
|
|
||||||
}
|
|
||||||
MediaType.Amr -> {
|
|
||||||
ptt.duration = AudioUtils.getDurationSec(pttFile)
|
|
||||||
ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR
|
|
||||||
}
|
|
||||||
MediaType.Pcm -> {
|
|
||||||
val result = AudioUtils.pcmToSilk(pttFile)
|
|
||||||
ptt.duration = (result.second * 0.001).roundToInt()
|
|
||||||
pttFile = result.first
|
|
||||||
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
val result = AudioUtils.audioToSilk(pttFile)
|
|
||||||
ptt.duration = runCatching {
|
|
||||||
QRoute.api(IAIOPttApi::class.java)
|
|
||||||
.getPttFileDuration(result.second.absolutePath)
|
|
||||||
}.getOrElse {
|
|
||||||
result.first
|
|
||||||
}
|
|
||||||
pttFile = result.second
|
|
||||||
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ptt.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(pttFile.absolutePath)
|
|
||||||
val msgService = NTServiceFetcher.kernelService.msgService!!
|
|
||||||
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
|
|
||||||
RichMediaFilePathInfo(
|
|
||||||
MsgConstant.KELEMTYPEPTT, 0, ptt.md5HexStr, file.name, 1, 0, null, "", true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(originalPath) != pttFile.length()) {
|
|
||||||
QQNTWrapperUtil.CppProxy.copyFile(pttFile.absolutePath, originalPath)
|
|
||||||
}
|
|
||||||
if (originalPath != null) {
|
|
||||||
ptt.filePath = originalPath
|
|
||||||
} else {
|
|
||||||
ptt.filePath = pttFile.absolutePath
|
|
||||||
}
|
|
||||||
ptt.canConvert2Text = true
|
|
||||||
ptt.fileId = 0
|
|
||||||
ptt.fileUuid = ""
|
|
||||||
ptt.text = ""
|
|
||||||
ptt.voiceType = MsgConstant.KPTTVOICETYPESOUNDRECORD
|
|
||||||
ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPENONE
|
|
||||||
elem.pttElement = ptt
|
|
||||||
}
|
|
||||||
MsgConstant.KELEMTYPEVIDEO -> {
|
|
||||||
require(resources.size == 1) // 视频只能单个上传
|
|
||||||
val video = VideoElement()
|
|
||||||
video.videoMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
|
|
||||||
val msgService = NTServiceFetcher.kernelService.msgService!!
|
|
||||||
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
|
|
||||||
RichMediaFilePathInfo(
|
|
||||||
5, 2, video.videoMd5, file.name, 1, 0, null, "", true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
|
|
||||||
RichMediaFilePathInfo(
|
|
||||||
5, 1, video.videoMd5, file.name, 2, 0, null, "", true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
|
|
||||||
originalPath
|
|
||||||
) != file.length()
|
|
||||||
) {
|
|
||||||
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
|
|
||||||
AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!)
|
|
||||||
}
|
|
||||||
video.fileTime = AudioUtils.getVideoTime(file)
|
|
||||||
video.fileSize = file.length()
|
|
||||||
video.fileName = file.name
|
|
||||||
video.fileFormat = FileTransfer.VIDEO_FORMAT_MP4
|
|
||||||
video.thumbSize = QQNTWrapperUtil.CppProxy.getFileSize(thumbPath).toInt()
|
|
||||||
val options = BitmapFactory.Options()
|
|
||||||
BitmapFactory.decodeFile(thumbPath, options)
|
|
||||||
video.thumbWidth = options.outWidth
|
|
||||||
video.thumbHeight = options.outHeight
|
|
||||||
video.thumbMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(thumbPath)
|
|
||||||
video.thumbPath = hashMapOf(0 to thumbPath)
|
|
||||||
elem.videoElement = video
|
|
||||||
}
|
|
||||||
|
|
||||||
/*MsgConstant.KELEMTYPEFILE -> {
|
|
||||||
require(resources.size == 1) // 文件只能单个上传
|
|
||||||
val fileElement = FileElement()
|
|
||||||
fileElement.fileMd5 = ""
|
|
||||||
fileElement.fileName = file.name
|
|
||||||
fileElement.filePath = file.absolutePath
|
|
||||||
fileElement.fileSize = file.length()
|
|
||||||
fileElement.picWidth = 0
|
|
||||||
fileElement.picHeight = 0
|
|
||||||
fileElement.videoDuration = 0
|
|
||||||
fileElement.picThumbPath = HashMap()
|
|
||||||
fileElement.expireTime = 0L
|
|
||||||
fileElement.fileSha = ""
|
|
||||||
fileElement.fileSha3 = ""
|
|
||||||
fileElement.file10MMd5 = ""
|
|
||||||
when (TransfileHelper.getExtensionId(file.name)) {
|
|
||||||
0 -> {
|
|
||||||
val wh = QRoute.api(IMsgUtilApi::class.java)
|
|
||||||
.getPicSizeByPath(file.absolutePath)
|
|
||||||
fileElement.picWidth = wh.first
|
|
||||||
fileElement.picHeight = wh.second
|
|
||||||
fileElement.picThumbPath[750] = file.absolutePath
|
|
||||||
}
|
|
||||||
2 -> {
|
|
||||||
val thumbPic = FileUtils.getFileByMd5(MD5.genFileMd5Hex(file.absolutePath))
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val fileOutputStream = FileOutputStream(thumbPic)
|
|
||||||
val retriever = MediaMetadataRetriever()
|
|
||||||
retriever.setDataSource(fileElement.filePath)
|
|
||||||
retriever.frameAtTime?.compress(Bitmap.CompressFormat.JPEG, 60, fileOutputStream)
|
|
||||||
fileOutputStream.flush()
|
|
||||||
fileOutputStream.close()
|
|
||||||
}
|
|
||||||
val options = BitmapFactory.Options()
|
|
||||||
BitmapFactory.decodeFile(thumbPic.absolutePath, options)
|
|
||||||
fileElement.picHeight = options.outHeight
|
|
||||||
fileElement.picWidth = options.outWidth
|
|
||||||
fileElement.picThumbPath = hashMapOf(750 to thumbPic.absolutePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elem.fileElement = fileElement
|
|
||||||
}*/
|
|
||||||
else -> throw IllegalArgumentException("unsupported elementType: $elementType")
|
|
||||||
}
|
|
||||||
return@map elem
|
|
||||||
}
|
|
||||||
if (messages.isEmpty()) {
|
|
||||||
return Result.failure(Exception("no valid image files"))
|
|
||||||
}
|
|
||||||
val contact = when(chatType) {
|
|
||||||
MsgConstant.KCHATTYPEC2C -> MessageHelper.generateContact(chatType, TicketSvc.getUin())
|
|
||||||
else -> Contact(chatType, fetchGroupResUploadTo(), null)
|
|
||||||
}
|
|
||||||
val result = mutableListOf<CommonFileInfo>()
|
|
||||||
withTimeoutOrNull(timeout) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
val uniseq = MessageHelper.generateMsgId(chatType)
|
|
||||||
RichMediaUploadHandler.registerListener(uniseq.qqMsgId) upload@{
|
|
||||||
if (uniseq.qqMsgId == msgId) {
|
|
||||||
result.add(commonFileInfo)
|
|
||||||
}
|
|
||||||
if (result.size == resources.size) {
|
|
||||||
it.resume(true)
|
|
||||||
return@upload true
|
|
||||||
}
|
|
||||||
return@upload false
|
|
||||||
}
|
|
||||||
MessageHelper.sendMessageWithMsgId(
|
|
||||||
contact = contact,
|
|
||||||
message = ArrayList(messages),
|
|
||||||
uniseq = uniseq.qqMsgId
|
|
||||||
) { _, _ ->
|
|
||||||
if (contact.chatType == MsgConstant.KCHATTYPEGROUP && contact.peerUid == "100000000") {
|
|
||||||
val kernelService = NTServiceFetcher.kernelService
|
|
||||||
val sessionService = kernelService.wrapperSession
|
|
||||||
val msgService = sessionService.msgService
|
|
||||||
msgService.deleteMsg(contact, arrayListOf(uniseq.qqMsgId), null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
it.invokeOnCancellation {
|
|
||||||
RichMediaUploadHandler.removeListener(uniseq.qqMsgId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.isEmpty()) {
|
|
||||||
return Result.failure(Exception("upload failed"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.success(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取NT图片的RKEY
|
|
||||||
*/
|
|
||||||
suspend fun getNtPicRKey(
|
|
||||||
fileId: String,
|
|
||||||
md5: String,
|
|
||||||
sha: String,
|
|
||||||
fileSize: ULong,
|
|
||||||
width: UInt,
|
|
||||||
height: UInt,
|
|
||||||
sceneBuilder: suspend SceneInfo.() -> Unit
|
|
||||||
): Result<String> {
|
|
||||||
runCatching {
|
|
||||||
val req = NtV2RichMediaReq(
|
|
||||||
head = MultiMediaReqHead(
|
|
||||||
commonHead = CommonHead(
|
|
||||||
requestId = requestIdSeq.incrementAndGet().toULong(),
|
|
||||||
cmd = 200u
|
|
||||||
),
|
|
||||||
sceneInfo = SceneInfo(
|
|
||||||
requestType = 2u,
|
|
||||||
businessType = 1u,
|
|
||||||
).apply {
|
|
||||||
sceneBuilder()
|
|
||||||
},
|
|
||||||
clientMeta = ClientMeta(2u)
|
|
||||||
),
|
|
||||||
download = DownloadReq(
|
|
||||||
IndexNode(
|
|
||||||
FileInfo(
|
|
||||||
fileSize = fileSize,
|
|
||||||
md5 = md5.lowercase(),
|
|
||||||
sha1 = sha.lowercase(),
|
|
||||||
name = "${md5}.jpg",
|
|
||||||
fileType = FileType(
|
|
||||||
fileType = 1u,
|
|
||||||
picFormat = 1000u,
|
|
||||||
videoFormat = 0u,
|
|
||||||
voiceFormat = 0u
|
|
||||||
),
|
|
||||||
width = width,
|
|
||||||
height = height,
|
|
||||||
time = 0u,
|
|
||||||
original = 1u
|
|
||||||
),
|
|
||||||
fileUuid = fileId,
|
|
||||||
storeId = 1u,
|
|
||||||
uploadTime = 0u,
|
|
||||||
ttl = 0u,
|
|
||||||
subType = 0u,
|
|
||||||
storeAppId = 0u
|
|
||||||
),
|
|
||||||
DownloadExt(
|
|
||||||
video = VideoDownloadExt(
|
|
||||||
busiType = 0u,
|
|
||||||
subBusiType = 0u,
|
|
||||||
msgCodecConfig = CodecConfigReq(
|
|
||||||
platformChipinfo = "",
|
|
||||||
osVer = "",
|
|
||||||
deviceName = ""
|
|
||||||
),
|
|
||||||
flag = 1u
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).toByteArray()
|
|
||||||
val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11c5_200", 4549, 200, req, true)?.slice(4)
|
|
||||||
buffer?.decodeProtobuf<TrpcOidb>()?.buffer?.decodeProtobuf<NtV2RichMediaRsp>()?.download?.rkeyParam?.let {
|
|
||||||
return Result.success(it)
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
return Result.failure(it)
|
|
||||||
}
|
|
||||||
return Result.failure(Exception("unable to get c2c nt pic"))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun requestUploadNtPic(
|
|
||||||
file: File,
|
|
||||||
md5: String,
|
|
||||||
sha: String,
|
|
||||||
name: String,
|
|
||||||
width: UInt,
|
|
||||||
height: UInt,
|
|
||||||
retryCnt: Int,
|
|
||||||
chatType: Int = MsgConstant.KCHATTYPEGROUP,
|
|
||||||
sceneBuilder: suspend SceneInfo.() -> Unit
|
|
||||||
): Result<UploadRsp> {
|
|
||||||
return runCatching {
|
|
||||||
requestUploadNtPic(file, md5, sha, name, width, height, chatType, sceneBuilder).getOrThrow()
|
|
||||||
}.onFailure {
|
|
||||||
if (retryCnt > 0) {
|
|
||||||
return requestUploadNtPic(file, md5, sha, name, width, height, retryCnt - 1, chatType, sceneBuilder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun requestUploadNtPic(
|
|
||||||
file: File,
|
|
||||||
md5: String,
|
|
||||||
sha: String,
|
|
||||||
name: String,
|
|
||||||
width: UInt,
|
|
||||||
height: UInt,
|
|
||||||
chatType: Int,
|
|
||||||
sceneBuilder: suspend SceneInfo.() -> Unit
|
|
||||||
): Result<UploadRsp> {
|
|
||||||
val req = NtV2RichMediaReq(
|
|
||||||
head = MultiMediaReqHead(
|
|
||||||
commonHead = CommonHead(
|
|
||||||
requestId = requestIdSeq.incrementAndGet().toULong(),
|
|
||||||
cmd = 100u
|
|
||||||
),
|
|
||||||
sceneInfo = SceneInfo(
|
|
||||||
requestType = 2u,
|
|
||||||
businessType = 1u,
|
|
||||||
).apply {
|
|
||||||
sceneBuilder()
|
|
||||||
},
|
|
||||||
clientMeta = ClientMeta(2u)
|
|
||||||
),
|
|
||||||
upload = UploadReq(
|
|
||||||
listOf(UploadInfo(
|
|
||||||
FileInfo(
|
|
||||||
fileSize = file.length().toULong(),
|
|
||||||
md5 = md5,
|
|
||||||
sha1 = sha,
|
|
||||||
name = name,
|
|
||||||
fileType = FileType(
|
|
||||||
fileType = 1u,
|
|
||||||
picFormat = 1000u,
|
|
||||||
videoFormat = 0u,
|
|
||||||
voiceFormat = 0u
|
|
||||||
),
|
|
||||||
width = width,
|
|
||||||
height = height,
|
|
||||||
time = 0u,
|
|
||||||
original = 1u
|
|
||||||
),
|
|
||||||
subFileType = 0u
|
|
||||||
)),
|
|
||||||
tryFastUploadCompleted = true,
|
|
||||||
srvSendMsg = false,
|
|
||||||
clientRandomId = Random.nextULong(),
|
|
||||||
compatQMsgSceneType = 2u,
|
|
||||||
clientSeq = Random.nextUInt(),
|
|
||||||
noNeedCompatMsg = true
|
|
||||||
)
|
|
||||||
).toByteArray()
|
|
||||||
val buffer = when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> {
|
|
||||||
sendOidbAW("OidbSvcTrpcTcp.0x11c4_100", 4548, 100, req, true, timeout = 3_000)?.slice(4)
|
|
||||||
?: return Result.failure(Exception("no response: timeout"))
|
|
||||||
}
|
|
||||||
MsgConstant.KCHATTYPEC2C -> {
|
|
||||||
sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true, timeout = 3_000)?.slice(4)
|
|
||||||
?: return Result.failure(Exception("no response: timeout"))
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> return Result.failure(Exception("unknown chat type: $chatType"))
|
|
||||||
}
|
|
||||||
val rspBuffer = buffer.decodeProtobuf<TrpcOidb>().buffer
|
|
||||||
val rsp = rspBuffer.decodeProtobuf<NtV2RichMediaRsp>()
|
|
||||||
if (rsp.upload == null) {
|
|
||||||
return Result.failure(Exception("unable to request upload nt pic: ${rsp.head}"))
|
|
||||||
}
|
|
||||||
return Result.success(rsp.upload!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用OldBDH获取图片上传状态以及图片上传服务器
|
|
||||||
*/
|
|
||||||
suspend fun requestUploadGroupPic(
|
|
||||||
groupId: ULong,
|
|
||||||
md5: String,
|
|
||||||
fileSize: ULong,
|
|
||||||
width: UInt,
|
|
||||||
height: UInt,
|
|
||||||
): Result<TryUpPicData> {
|
|
||||||
return runCatching {
|
|
||||||
val rspBuffer = sendBufferAW("ImgStore.GroupPicUp", true, Cmd0x388ReqBody(
|
|
||||||
netType = 3,
|
|
||||||
subCmd = 1,
|
|
||||||
msgTryUpImg = arrayListOf(
|
|
||||||
TryUpImgReq(
|
|
||||||
groupCode = groupId.toLong(),
|
|
||||||
srcUin = TicketSvc.getLongUin(),
|
|
||||||
fileMd5 = md5.hex2ByteArray(),
|
|
||||||
fileSize = fileSize.toLong(),
|
|
||||||
fileName = "$md5.jpg",
|
|
||||||
srcTerm = 2,
|
|
||||||
platformType = 9,
|
|
||||||
buType = 212,
|
|
||||||
picWidth = width.toInt(),
|
|
||||||
picHeight = height.toInt(),
|
|
||||||
picType = 1000,
|
|
||||||
buildVer = "1.0.0",
|
|
||||||
originalPic = 1,
|
|
||||||
fileIndex = byteArrayOf(),
|
|
||||||
srvUpload = 0
|
|
||||||
)
|
|
||||||
),
|
|
||||||
).toByteArray())!!
|
|
||||||
val rsp = rspBuffer.decodeProtobuf<Cmd0x388RspBody>()
|
|
||||||
.msgTryUpImgRsp!!.first()
|
|
||||||
TryUpPicData(
|
|
||||||
uKey = rsp.ukey,
|
|
||||||
exist = rsp.fileExist,
|
|
||||||
fileId = rsp.fileId.toULong(),
|
|
||||||
upIp = rsp.upIp,
|
|
||||||
upPort = rsp.upPort
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.transfile
|
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo
|
|
||||||
|
|
||||||
internal object RichMediaUploadHandler {
|
|
||||||
private val listeners by lazy {
|
|
||||||
mutableMapOf<Long, FileTransNotifyInfo.() -> Boolean>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerListener(key: Long, value: FileTransNotifyInfo.() -> Boolean) {
|
|
||||||
listeners[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeListener(key: Long) {
|
|
||||||
listeners.remove(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun notify(info: FileTransNotifyInfo): Boolean {
|
|
||||||
listeners[info.msgId]?.let {
|
|
||||||
if (it(info)) {
|
|
||||||
listeners.remove(info.msgId)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,431 +0,0 @@
|
|||||||
@file:OptIn(ExperimentalSerializationApi::class)
|
|
||||||
package moe.fuqiuluo.qqinterface.servlet.transfile
|
|
||||||
|
|
||||||
import com.tencent.mobileqq.pb.ByteStringMicro
|
|
||||||
import com.tencent.mobileqq.transfile.FileMsg
|
|
||||||
import com.tencent.mobileqq.transfile.api.IProtoReqManager
|
|
||||||
import com.tencent.mobileqq.transfile.protohandler.RichProto
|
|
||||||
import com.tencent.mobileqq.transfile.protohandler.RichProtoProc
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc.getNtPicRKey
|
|
||||||
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
|
||||||
import moe.fuqiuluo.shamrock.tools.slice
|
|
||||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
|
|
||||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
|
||||||
import mqq.app.MobileQQ
|
|
||||||
import protobuf.auto.toByteArray
|
|
||||||
import protobuf.oidb.cmd0x11c5.C2CUserInfo
|
|
||||||
import protobuf.oidb.cmd0x11c5.ChannelUserInfo
|
|
||||||
import protobuf.oidb.cmd0x11c5.GroupUserInfo
|
|
||||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo
|
|
||||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
|
|
||||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
|
|
||||||
import protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody
|
|
||||||
import tencent.im.cs.cmd0x346.cmd0x346
|
|
||||||
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
|
|
||||||
import tencent.im.oidb.cmd0xe37.cmd0xe37
|
|
||||||
import tencent.im.oidb.oidb_sso
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
private const val GPRO_PIC = "gchat.qpic.cn"
|
|
||||||
private const val MULTIMEDIA_DOMAIN = "multimedia.nt.qq.com.cn"
|
|
||||||
private const val C2C_PIC = "c2cpicdw.qpic.cn"
|
|
||||||
|
|
||||||
internal object RichProtoSvc: BaseSvc() {
|
|
||||||
suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String {
|
|
||||||
val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, Oidb0xfc2ReqBody(
|
|
||||||
msgCmd = 1200,
|
|
||||||
msgBusType = 4202,
|
|
||||||
msgChannelInfo = Oidb0xfc2ChannelInfo(
|
|
||||||
guildId = peerId.toULong(),
|
|
||||||
channelId = channelId.toULong()
|
|
||||||
),
|
|
||||||
msgTerminalType = 2,
|
|
||||||
msgApplyDownloadReq = Oidb0xfc2MsgApplyDownloadReq(
|
|
||||||
fieldId = fileId,
|
|
||||||
supportEncrypt = 0
|
|
||||||
)
|
|
||||||
).toByteArray()) ?: return ""
|
|
||||||
val body = oidb_sso.OIDBSSOPkg()
|
|
||||||
body.mergeFrom(buffer.slice(4))
|
|
||||||
body.bytes_bodybuffer
|
|
||||||
.get().toByteArray()
|
|
||||||
.decodeProtobuf<Oidb0xfc2RspBody>()
|
|
||||||
.msgApplyDownloadRsp?.let {
|
|
||||||
it.msgDownloadInfo?.let {
|
|
||||||
return "https://${it.downloadDomain}${it.downloadUrl}&fname=$fileId&isthumb=0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupFileDownUrl(
|
|
||||||
peerId: Long,
|
|
||||||
fileId: String,
|
|
||||||
bizId: Int = 102
|
|
||||||
): String {
|
|
||||||
val buffer = sendOidbAW("OidbSvcTrpcTcp.0x6d6_2", 1750, 2, oidb_0x6d6.ReqBody().apply {
|
|
||||||
download_file_req.set(oidb_0x6d6.DownloadFileReqBody().apply {
|
|
||||||
uint64_group_code.set(peerId)
|
|
||||||
uint32_app_id.set(3)
|
|
||||||
uint32_bus_id.set(bizId)
|
|
||||||
str_file_id.set(fileId)
|
|
||||||
})
|
|
||||||
}.toByteArray()) ?: return ""
|
|
||||||
val body = oidb_sso.OIDBSSOPkg()
|
|
||||||
body.mergeFrom(buffer.slice(4))
|
|
||||||
val result = oidb_0x6d6.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
|
|
||||||
if (body.uint32_result.get() != 0
|
|
||||||
|| result.download_file_rsp.int32_ret_code.get() != 0) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
val domain = if (!result.download_file_rsp.str_download_dns.has())
|
|
||||||
("https://" + result.download_file_rsp.str_download_ip.get())
|
|
||||||
else ("http://" + result.download_file_rsp.str_download_dns.get().toByteArray().decodeToString())
|
|
||||||
val downloadUrl = result.download_file_rsp.bytes_download_url.get().toByteArray().toHexString()
|
|
||||||
val appId = MobileQQ.getMobileQQ().appId
|
|
||||||
val version = PlatformUtils.getQQVersion(MobileQQ.getContext())
|
|
||||||
|
|
||||||
return "$domain/ftn_handler/$downloadUrl/?fname=$fileId&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk"
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getC2CFileDownUrl(
|
|
||||||
fileId: String,
|
|
||||||
subId: String,
|
|
||||||
retryCnt: Int = 0
|
|
||||||
): String {
|
|
||||||
val buffer = sendOidbAW("OidbSvc.0xe37_1200", 3639, 1200, cmd0xe37.Req0xe37().apply {
|
|
||||||
bytes_cmd_0x346_req_body.set(ByteStringMicro.copyFrom(cmd0x346.ReqBody().apply {
|
|
||||||
uint32_cmd.set(1200)
|
|
||||||
uint32_seq.set(1)
|
|
||||||
msg_apply_download_req.set(cmd0x346.ApplyDownloadReq().apply {
|
|
||||||
uint64_uin.set(app.longAccountUin)
|
|
||||||
bytes_uuid.set(ByteStringMicro.copyFrom(fileId.toByteArray()))
|
|
||||||
uint32_owner_type.set(2)
|
|
||||||
str_fileidcrc.set(subId)
|
|
||||||
|
|
||||||
})
|
|
||||||
uint32_business_id.set(3)
|
|
||||||
uint32_client_type.set(104)
|
|
||||||
uint32_flag_support_mediaplatform.set(1)
|
|
||||||
msg_extension_req.set(cmd0x346.ExtensionReq().apply {
|
|
||||||
uint32_download_url_type.set(1)
|
|
||||||
})
|
|
||||||
}.toByteArray()))
|
|
||||||
}.toByteArray())
|
|
||||||
|
|
||||||
if (buffer == null) {
|
|
||||||
if (retryCnt < 5) {
|
|
||||||
return getC2CFileDownUrl(fileId, subId, retryCnt + 1)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
} else {
|
|
||||||
val body = oidb_sso.OIDBSSOPkg()
|
|
||||||
body.mergeFrom(buffer.slice(4))
|
|
||||||
val result = cmd0x346.RspBody().mergeFrom(cmd0xe37.Resp0xe37().mergeFrom(
|
|
||||||
body.bytes_bodybuffer.get().toByteArray()
|
|
||||||
).bytes_cmd_0x346_rsp_body.get().toByteArray())
|
|
||||||
if (body.uint32_result.get() != 0 ||
|
|
||||||
result.msg_apply_download_rsp.int32_ret_code.has() && result.msg_apply_download_rsp.int32_ret_code.get() != 0) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
val oldData = result.msg_apply_download_rsp.msg_download_info
|
|
||||||
//val newData = result[14, 40] NTQQ 文件信息
|
|
||||||
|
|
||||||
val domain = if (oldData.str_download_dns.has()) ("https://" + oldData.str_download_dns.get()) else ("http://" + oldData.rpt_str_downloadip_list.get().first())
|
|
||||||
val params = oldData.str_download_url.get()
|
|
||||||
val appId = MobileQQ.getMobileQQ().appId
|
|
||||||
val version = PlatformUtils.getQQVersion(MobileQQ.getContext())
|
|
||||||
|
|
||||||
return "$domain$params&isthumb=0&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupPicDownUrl(
|
|
||||||
originalUrl: String,
|
|
||||||
md5: String,
|
|
||||||
peer: String = "",
|
|
||||||
fileId: String = "",
|
|
||||||
sha: String = "",
|
|
||||||
fileSize: ULong = 0uL,
|
|
||||||
width: UInt = 0u,
|
|
||||||
height: UInt = 0u
|
|
||||||
): String {
|
|
||||||
val isNtServer = originalUrl.startsWith("/download")
|
|
||||||
val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC
|
|
||||||
if (originalUrl.isNotEmpty()) {
|
|
||||||
if (isNtServer && !originalUrl.contains("rkey=")) {
|
|
||||||
getNtPicRKey(
|
|
||||||
fileId = fileId,
|
|
||||||
md5 = md5,
|
|
||||||
sha = sha,
|
|
||||||
fileSize = fileSize,
|
|
||||||
width = width,
|
|
||||||
height = height
|
|
||||||
) {
|
|
||||||
sceneType = 2u
|
|
||||||
grp = GroupUserInfo(peer.toULong())
|
|
||||||
}.onSuccess {
|
|
||||||
return "https://$domain$originalUrl$it"
|
|
||||||
}.onFailure {
|
|
||||||
LogCenter.log("getGroupPicDownUrl: ${it.stackTraceToString()}", Level.WARN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "https://$domain$originalUrl"
|
|
||||||
}
|
|
||||||
return "https://$domain/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2"
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getC2CPicDownUrl(
|
|
||||||
originalUrl: String,
|
|
||||||
md5: String,
|
|
||||||
peer: String = "",
|
|
||||||
fileId: String = "",
|
|
||||||
sha: String = "",
|
|
||||||
fileSize: ULong = 0uL,
|
|
||||||
width: UInt = 0u,
|
|
||||||
height: UInt = 0u,
|
|
||||||
storeId: Int = 0
|
|
||||||
): String {
|
|
||||||
val isNtServer = storeId == 1 || originalUrl.startsWith("/download")
|
|
||||||
val domain = if (isNtServer) MULTIMEDIA_DOMAIN else C2C_PIC
|
|
||||||
if (originalUrl.isNotEmpty()) {
|
|
||||||
if (fileId.isNotEmpty()) getNtPicRKey(
|
|
||||||
fileId = fileId,
|
|
||||||
md5 = md5,
|
|
||||||
sha = sha,
|
|
||||||
fileSize = fileSize,
|
|
||||||
width = width,
|
|
||||||
height = height
|
|
||||||
) {
|
|
||||||
sceneType = 1u
|
|
||||||
c2c = C2CUserInfo(
|
|
||||||
accountType = 2u,
|
|
||||||
uid = ContactHelper.getUidByUinAsync(peer.toLong())
|
|
||||||
)
|
|
||||||
}.onSuccess {
|
|
||||||
if (isNtServer && !originalUrl.contains("rkey=")) {
|
|
||||||
return "https://$domain$originalUrl$it"
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
LogCenter.log("getC2CPicDownUrl: ${it.stackTraceToString()}", Level.WARN)
|
|
||||||
}
|
|
||||||
if (isNtServer && !originalUrl.contains("rkey=")) {
|
|
||||||
return "https://$domain$originalUrl&rkey="
|
|
||||||
}
|
|
||||||
return "https://$domain$originalUrl"
|
|
||||||
}
|
|
||||||
return "https://$domain/offpic_new/0/0-0-${md5}/0?term=2"
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGuildPicDownUrl(
|
|
||||||
originalUrl: String,
|
|
||||||
md5: String,
|
|
||||||
peer: String = "",
|
|
||||||
subPeer: String = "",
|
|
||||||
fileId: String = "",
|
|
||||||
sha: String = "",
|
|
||||||
fileSize: ULong = 0uL,
|
|
||||||
width: UInt = 0u,
|
|
||||||
height: UInt = 0u
|
|
||||||
): String {
|
|
||||||
val isNtServer = originalUrl.startsWith("/download")
|
|
||||||
val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC
|
|
||||||
if (originalUrl.isNotEmpty()) {
|
|
||||||
if (isNtServer && !originalUrl.contains("rkey=")) {
|
|
||||||
getNtPicRKey(
|
|
||||||
fileId = fileId,
|
|
||||||
md5 = md5,
|
|
||||||
sha = sha,
|
|
||||||
fileSize = fileSize,
|
|
||||||
width = width,
|
|
||||||
height = height
|
|
||||||
) {
|
|
||||||
sceneType = 3u
|
|
||||||
channel = ChannelUserInfo(peer.toULong(), subPeer.toULong(), 1u)
|
|
||||||
}.onSuccess {
|
|
||||||
return "https://$domain$originalUrl$it"
|
|
||||||
}.onFailure {
|
|
||||||
LogCenter.log("getGuildPicDownUrl: ${it.stackTraceToString()}", Level.WARN)
|
|
||||||
}
|
|
||||||
return "https://$domain$originalUrl&rkey="
|
|
||||||
}
|
|
||||||
return "https://$domain$originalUrl"
|
|
||||||
}
|
|
||||||
return "https://$domain/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2"
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getC2CVideoDownUrl(
|
|
||||||
peerId: String,
|
|
||||||
md5: ByteArray,
|
|
||||||
fileUUId: String
|
|
||||||
): String {
|
|
||||||
return suspendCancellableCoroutine {
|
|
||||||
val runtime = AppRuntimeFetcher.appRuntime
|
|
||||||
val richProtoReq = RichProto.RichProtoReq()
|
|
||||||
val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq()
|
|
||||||
downReq.selfUin = runtime.currentAccountUin
|
|
||||||
downReq.peerUin = peerId
|
|
||||||
downReq.secondUin = peerId
|
|
||||||
downReq.uinType = FileMsg.UIN_BUDDY
|
|
||||||
downReq.agentType = 0
|
|
||||||
downReq.chatType = 1
|
|
||||||
downReq.troopUin = peerId
|
|
||||||
downReq.clientType = 2
|
|
||||||
downReq.fileId = fileUUId
|
|
||||||
downReq.md5 = md5
|
|
||||||
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
|
|
||||||
downReq.subBusiType = 0
|
|
||||||
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
|
|
||||||
downReq.downType = 1
|
|
||||||
downReq.sceneType = 1
|
|
||||||
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
|
|
||||||
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
|
|
||||||
LogCenter.log("requestDownPrivateVideo: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
|
|
||||||
it.resume("")
|
|
||||||
} else {
|
|
||||||
val videoDownResp = resp.resps.first() as RichProto.RichProtoResp.ShortVideoDownResp
|
|
||||||
val url = StringBuilder()
|
|
||||||
url.append(videoDownResp.mIpList.random().getServerUrl("http://"))
|
|
||||||
url.append(videoDownResp.mUrl)
|
|
||||||
it.resume(url.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
richProtoReq.protoKey = RichProtoProc.SHORT_VIDEO_DW
|
|
||||||
richProtoReq.reqs.add(downReq)
|
|
||||||
richProtoReq.protoReqMgr = runtime.getRuntimeService(IProtoReqManager::class.java, "all")
|
|
||||||
RichProtoProc.procRichProtoReq(richProtoReq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupVideoDownUrl(
|
|
||||||
peerId: String,
|
|
||||||
md5: ByteArray,
|
|
||||||
fileUUId: String
|
|
||||||
): String {
|
|
||||||
return suspendCancellableCoroutine {
|
|
||||||
val runtime = AppRuntimeFetcher.appRuntime
|
|
||||||
val richProtoReq = RichProto.RichProtoReq()
|
|
||||||
val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq()
|
|
||||||
downReq.selfUin = runtime.currentAccountUin
|
|
||||||
downReq.peerUin = peerId
|
|
||||||
downReq.secondUin = peerId
|
|
||||||
downReq.uinType = FileMsg.UIN_TROOP
|
|
||||||
downReq.agentType = 0
|
|
||||||
downReq.chatType = 1
|
|
||||||
downReq.troopUin = peerId
|
|
||||||
downReq.clientType = 2
|
|
||||||
downReq.fileId = fileUUId
|
|
||||||
downReq.md5 = md5
|
|
||||||
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
|
|
||||||
downReq.subBusiType = 0
|
|
||||||
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
|
|
||||||
downReq.downType = 1
|
|
||||||
downReq.sceneType = 1
|
|
||||||
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
|
|
||||||
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
|
|
||||||
LogCenter.log("requestDownGroupVideo: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
|
|
||||||
it.resume("")
|
|
||||||
} else {
|
|
||||||
val videoDownResp = resp.resps.first() as RichProto.RichProtoResp.ShortVideoDownResp
|
|
||||||
val url = StringBuilder()
|
|
||||||
url.append(videoDownResp.mIpList.random().getServerUrl("http://"))
|
|
||||||
url.append(videoDownResp.mUrl)
|
|
||||||
it.resume(url.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
richProtoReq.protoKey = RichProtoProc.SHORT_VIDEO_DW
|
|
||||||
richProtoReq.reqs.add(downReq)
|
|
||||||
richProtoReq.protoReqMgr = runtime.getRuntimeService(IProtoReqManager::class.java, "all")
|
|
||||||
RichProtoProc.procRichProtoReq(richProtoReq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getC2CPttDownUrl(
|
|
||||||
peerId: String,
|
|
||||||
fileUUId: String
|
|
||||||
): String {
|
|
||||||
return suspendCancellableCoroutine {
|
|
||||||
val runtime = AppRuntimeFetcher.appRuntime
|
|
||||||
val richProtoReq = RichProto.RichProtoReq()
|
|
||||||
val pttDownReq: RichProto.RichProtoReq.C2CPttDownReq = RichProto.RichProtoReq.C2CPttDownReq()
|
|
||||||
pttDownReq.selfUin = runtime.currentAccountUin
|
|
||||||
pttDownReq.peerUin = peerId
|
|
||||||
pttDownReq.secondUin = peerId
|
|
||||||
pttDownReq.uinType = FileMsg.UIN_BUDDY
|
|
||||||
pttDownReq.busiType = 1002
|
|
||||||
pttDownReq.uuid = fileUUId
|
|
||||||
pttDownReq.storageSource = "pttcenter"
|
|
||||||
pttDownReq.isSelfSend = false
|
|
||||||
|
|
||||||
pttDownReq.voiceType = 1
|
|
||||||
pttDownReq.downType = 1
|
|
||||||
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
|
|
||||||
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
|
|
||||||
LogCenter.log("requestDownPrivateVoice: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
|
|
||||||
it.resume("")
|
|
||||||
} else {
|
|
||||||
val pttDownResp = resp.resps.first() as RichProto.RichProtoResp.C2CPttDownResp
|
|
||||||
val url = StringBuilder()
|
|
||||||
url.append(pttDownResp.downloadUrl)
|
|
||||||
url.append("&client_proto=qq&client_appid=${MobileQQ.getMobileQQ().appId}&client_type=android&client_ver=${PlatformUtils.getQQVersion(MobileQQ.getContext())}&client_down_type=auto&client_aio_type=unk")
|
|
||||||
it.resume(url.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
richProtoReq.protoKey = RichProtoProc.C2C_PTT_DW
|
|
||||||
richProtoReq.reqs.add(pttDownReq)
|
|
||||||
richProtoReq.protoReqMgr = runtime.getRuntimeService(IProtoReqManager::class.java, "all")
|
|
||||||
RichProtoProc.procRichProtoReq(richProtoReq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getGroupPttDownUrl(
|
|
||||||
peerId: String,
|
|
||||||
md5: ByteArray,
|
|
||||||
groupFileKey: String
|
|
||||||
): String {
|
|
||||||
return suspendCancellableCoroutine {
|
|
||||||
val runtime = AppRuntimeFetcher.appRuntime
|
|
||||||
val richProtoReq = RichProto.RichProtoReq()
|
|
||||||
val groupPttDownReq: RichProto.RichProtoReq.GroupPttDownReq = RichProto.RichProtoReq.GroupPttDownReq()
|
|
||||||
groupPttDownReq.selfUin = runtime.currentAccountUin
|
|
||||||
groupPttDownReq.peerUin = peerId
|
|
||||||
groupPttDownReq.secondUin = peerId
|
|
||||||
groupPttDownReq.uinType = FileMsg.UIN_TROOP
|
|
||||||
groupPttDownReq.groupFileID = 0
|
|
||||||
groupPttDownReq.groupFileKey = groupFileKey
|
|
||||||
groupPttDownReq.md5 = md5
|
|
||||||
groupPttDownReq.voiceType = 1
|
|
||||||
groupPttDownReq.downType = 1
|
|
||||||
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
|
|
||||||
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
|
|
||||||
LogCenter.log("requestDownGroupVoice: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
|
|
||||||
it.resume("")
|
|
||||||
} else {
|
|
||||||
val pttDownResp = resp.resps.first() as RichProto.RichProtoResp.GroupPttDownResp
|
|
||||||
val url = StringBuilder()
|
|
||||||
url.append("http://")
|
|
||||||
url.append(pttDownResp.domainV4V6)
|
|
||||||
url.append(pttDownResp.urlPath)
|
|
||||||
url.append("&client_proto=qq&client_appid=${MobileQQ.getMobileQQ().appId}&client_type=android&client_ver=${
|
|
||||||
PlatformUtils.getQQVersion(
|
|
||||||
MobileQQ.getContext())}&client_down_type=auto&client_aio_type=unk")
|
|
||||||
it.resume(url.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
richProtoReq.protoKey = RichProtoProc.GRP_PTT_DW
|
|
||||||
richProtoReq.reqs.add(groupPttDownReq)
|
|
||||||
richProtoReq.protoReqMgr = runtime.getRuntimeService(IProtoReqManager::class.java, "all")
|
|
||||||
RichProtoProc.procRichProtoReq(richProtoReq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,142 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.transfile
|
|
||||||
|
|
||||||
import com.tencent.mobileqq.data.MessageForShortVideo
|
|
||||||
import com.tencent.mobileqq.data.MessageRecord
|
|
||||||
import com.tencent.mobileqq.transfile.FileMsg
|
|
||||||
import com.tencent.mobileqq.transfile.TransferRequest
|
|
||||||
import moe.fuqiuluo.shamrock.utils.MD5
|
|
||||||
import java.io.File
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.data.ResourceType.*
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.data.ContactType
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.data.PictureResource
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.data.Resource
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.data.ResourceType
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.data.TransTarget
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.data.VideoResource
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.transfile.data.VoiceResource
|
|
||||||
|
|
||||||
internal object Transfer: FileTransfer() {
|
|
||||||
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
|
|
||||||
ContactType.TROOP to mapOf(
|
|
||||||
Picture to { uploadGroupPic(id, (it as PictureResource).src, mRec) },
|
|
||||||
Voice to { uploadGroupVoice(id, (it as VoiceResource).src) },
|
|
||||||
Video to { uploadGroupVideo(id, (it as VideoResource).src, it.thumb) },
|
|
||||||
|
|
||||||
),
|
|
||||||
ContactType.PRIVATE to mapOf(
|
|
||||||
Picture to { uploadC2CPic(id, (it as PictureResource).src, mRec) },
|
|
||||||
Voice to { uploadC2CVoice(id, (it as VoiceResource).src) },
|
|
||||||
Video to { uploadC2CVideo(id, (it as VideoResource).src, it.thumb) },
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
suspend fun uploadC2CVideo(
|
|
||||||
userId: String,
|
|
||||||
file: File,
|
|
||||||
thumb: File,
|
|
||||||
wait: Boolean = true
|
|
||||||
): Boolean {
|
|
||||||
return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_C2C, BUSI_TYPE_SHORT_VIDEO, wait) {
|
|
||||||
it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4
|
|
||||||
it.mRec = MessageForShortVideo().also {
|
|
||||||
it.busiType = BUSI_TYPE_SHORT_VIDEO
|
|
||||||
}
|
|
||||||
it.mThumbPath = thumb.absolutePath
|
|
||||||
it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun uploadGroupVideo(
|
|
||||||
groupId: String,
|
|
||||||
file: File,
|
|
||||||
thumb: File,
|
|
||||||
wait: Boolean = true
|
|
||||||
): Boolean {
|
|
||||||
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_TROOP, BUSI_TYPE_SHORT_VIDEO, wait) {
|
|
||||||
it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4
|
|
||||||
it.mRec = MessageForShortVideo().also {
|
|
||||||
it.busiType = BUSI_TYPE_SHORT_VIDEO
|
|
||||||
}
|
|
||||||
it.mThumbPath = thumb.absolutePath
|
|
||||||
it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun uploadC2CVoice(
|
|
||||||
userId: String,
|
|
||||||
file: File,
|
|
||||||
wait: Boolean = true
|
|
||||||
): Boolean {
|
|
||||||
return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) {
|
|
||||||
it.mPttUploadPanel = 3
|
|
||||||
it.mPttCompressFinish = true
|
|
||||||
it.mIsPttPreSend = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun uploadGroupVoice(
|
|
||||||
groupId: String,
|
|
||||||
file: File,
|
|
||||||
wait: Boolean = true
|
|
||||||
): Boolean {
|
|
||||||
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) {
|
|
||||||
it.mPttUploadPanel = 3
|
|
||||||
it.mPttCompressFinish = true
|
|
||||||
it.mIsPttPreSend = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun uploadC2CPic(
|
|
||||||
peerId: String,
|
|
||||||
file: File,
|
|
||||||
record: MessageRecord? = null,
|
|
||||||
wait: Boolean = true
|
|
||||||
): Boolean {
|
|
||||||
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
|
|
||||||
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
|
|
||||||
picUpExtraInfo.mIsRaw = false
|
|
||||||
picUpExtraInfo.mUinType = FileMsg.UIN_BUDDY
|
|
||||||
it.mPicSendSource = 8
|
|
||||||
it.mExtraObj = picUpExtraInfo
|
|
||||||
it.mIsPresend = true
|
|
||||||
it.delayShowProgressTimeInMs = 2000
|
|
||||||
it.mRec = record
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun uploadGroupPic(
|
|
||||||
groupId: String,
|
|
||||||
file: File,
|
|
||||||
record: MessageRecord? = null,
|
|
||||||
wait: Boolean = true
|
|
||||||
): Boolean {
|
|
||||||
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
|
|
||||||
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
|
|
||||||
picUpExtraInfo.mIsRaw = false
|
|
||||||
picUpExtraInfo.mUinType = FileMsg.UIN_TROOP
|
|
||||||
it.mPicSendSource = 8
|
|
||||||
it.delayShowProgressTimeInMs = 2000
|
|
||||||
it.mExtraObj = picUpExtraInfo
|
|
||||||
it.mRec = record
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun get(contactType: ContactType, resourceType: ResourceType): suspend TransTarget.(Resource) -> Boolean {
|
|
||||||
return (ROUTE[contactType] ?: error("unsupported contact type: $contactType"))[resourceType]
|
|
||||||
?: error("Unsupported resource type: $resourceType")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend infix fun TransferTaskBuilder.trans(res: Resource): Boolean {
|
|
||||||
return Transfer[contact.type, res.type](contact, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class TransferTaskBuilder {
|
|
||||||
lateinit var contact: TransTarget
|
|
||||||
}
|
|
||||||
|
|
||||||
internal infix fun Transfer.with(contact: TransTarget): TransferTaskBuilder {
|
|
||||||
return TransferTaskBuilder().also {
|
|
||||||
it.contact = contact
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.transfile.data
|
|
||||||
|
|
||||||
import com.tencent.mobileqq.data.MessageRecord
|
|
||||||
|
|
||||||
internal enum class ContactType {
|
|
||||||
TROOP,
|
|
||||||
PRIVATE,
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface TransTarget {
|
|
||||||
val id: String
|
|
||||||
val type: ContactType
|
|
||||||
|
|
||||||
val mRec: MessageRecord?
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class Troop(
|
|
||||||
override val id: String,
|
|
||||||
override val mRec: MessageRecord? = null
|
|
||||||
): TransTarget {
|
|
||||||
override val type: ContactType = ContactType.TROOP
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class Private(
|
|
||||||
override val id: String,
|
|
||||||
override val mRec: MessageRecord? = null
|
|
||||||
): TransTarget {
|
|
||||||
override val type: ContactType = ContactType.PRIVATE
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.transfile.data
|
|
||||||
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
internal enum class ResourceType {
|
|
||||||
Picture,
|
|
||||||
Video,
|
|
||||||
Voice
|
|
||||||
}
|
|
||||||
|
|
||||||
internal interface Resource {
|
|
||||||
val type: ResourceType
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class PictureResource(
|
|
||||||
val src: File
|
|
||||||
): Resource {
|
|
||||||
override val type = ResourceType.Picture
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class VideoResource(
|
|
||||||
val src: File, val thumb: File
|
|
||||||
): Resource {
|
|
||||||
override val type = ResourceType.Video
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class VoiceResource(
|
|
||||||
val src: File
|
|
||||||
): Resource {
|
|
||||||
override val type = ResourceType.Voice
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
package moe.fuqiuluo.qqinterface.servlet.transfile.data
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class TryUpPicData(
|
|
||||||
@SerialName("ukey") val uKey: ByteArray,
|
|
||||||
@SerialName("exist") val exist: Boolean,
|
|
||||||
@SerialName("file_id") val fileId: ULong,
|
|
||||||
@SerialName("up_ip") var upIp: ArrayList<Long>? = null,
|
|
||||||
@SerialName("up_port") var upPort: ArrayList<Int>? = null,
|
|
||||||
)
|
|
@ -1,55 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.helper
|
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.FriendSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
|
|
||||||
internal object ContactHelper {
|
|
||||||
suspend fun getUinByUidAsync(uid: String): String {
|
|
||||||
if (uid.isBlank() || uid == "0") {
|
|
||||||
return "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
val kernelService = NTServiceFetcher.kernelService
|
|
||||||
val sessionService = kernelService.wrapperSession
|
|
||||||
|
|
||||||
return suspendCancellableCoroutine { continuation ->
|
|
||||||
sessionService.uixConvertService.getUin(hashSetOf(uid)) {
|
|
||||||
continuation.resume(it)
|
|
||||||
}
|
|
||||||
}[uid]?.toString() ?: "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getUidByUinAsync(peerId: Long): String {
|
|
||||||
val kernelService = NTServiceFetcher.kernelService
|
|
||||||
val sessionService = kernelService.wrapperSession
|
|
||||||
return suspendCancellableCoroutine { continuation ->
|
|
||||||
sessionService.uixConvertService.getUid(hashSetOf(peerId)) {
|
|
||||||
continuation.resume(it)
|
|
||||||
}
|
|
||||||
}[peerId]!!
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查联系人是否可用, 每次都刷新,性能有损耗
|
|
||||||
*/
|
|
||||||
suspend fun checkContactAvailable(chatType: Int, peerId: String): Boolean {
|
|
||||||
return when(chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> {
|
|
||||||
GroupSvc.getGroupList(true).getOrNull()?.find {
|
|
||||||
it.troopcode == peerId
|
|
||||||
} != null
|
|
||||||
}
|
|
||||||
|
|
||||||
MsgConstant.KCHATTYPEC2C -> {
|
|
||||||
FriendSvc.getFriendList(true).getOrNull()?.find {
|
|
||||||
it.uin == peerId
|
|
||||||
} != null
|
|
||||||
}
|
|
||||||
else -> error("unknown chat type: $chatType")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,431 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.helper
|
|
||||||
|
|
||||||
import com.tencent.mobileqq.qroute.QRoute
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
|
||||||
import com.tencent.qqnt.msg.api.IMsgService
|
|
||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import kotlinx.serialization.json.JsonArray
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.jsonObject
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.maker.ElemMaker
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.maker.NtMsgElementMaker
|
|
||||||
import moe.fuqiuluo.shamrock.helper.db.MessageDB
|
|
||||||
import moe.fuqiuluo.shamrock.helper.db.MessageMapping
|
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
|
|
||||||
import moe.fuqiuluo.shamrock.tools.*
|
|
||||||
import protobuf.message.Elem
|
|
||||||
import protobuf.message.RichText
|
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.time.Duration.Companion.seconds
|
|
||||||
|
|
||||||
internal object MessageHelper {
|
|
||||||
suspend fun sendMessageWithoutMsgId(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
message: String,
|
|
||||||
callback: IOperateCallback,
|
|
||||||
fromId: String = peerId
|
|
||||||
): SendMsgResult {
|
|
||||||
val uniseq = generateMsgId(chatType)
|
|
||||||
val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, decodeCQCode(message)).also {
|
|
||||||
if (it.second.isEmpty() && !it.first) {
|
|
||||||
error("消息合成失败,请查看日志或者检查输入。")
|
|
||||||
} else if (it.second.isEmpty()) {
|
|
||||||
return uniseq.copy(msgHashId = 0, msgTime = System.currentTimeMillis())
|
|
||||||
}
|
|
||||||
}.second.filter {
|
|
||||||
it.elementType != -1
|
|
||||||
} as ArrayList<MsgElement>
|
|
||||||
return sendMessageWithoutMsgId(chatType, peerId, msg, fromId, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun resendMsg(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
fromId: String,
|
|
||||||
msgId: Long,
|
|
||||||
retryCnt: Int,
|
|
||||||
msgHashId: Int
|
|
||||||
): Result<SendMsgResult> {
|
|
||||||
val contact = generateContact(chatType, peerId, fromId)
|
|
||||||
return resendMsg(contact, msgId, retryCnt, msgHashId)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun resendMsg(contact: Contact, msgId: Long, retryCnt: Int, msgHashId: Int): Result<SendMsgResult> {
|
|
||||||
if (retryCnt < 0) return Result.failure(SendMsgException("消息发送超时次数过多"))
|
|
||||||
val service = QRoute.api(IMsgService::class.java)
|
|
||||||
val result = withTimeoutOrNull(15.seconds) {
|
|
||||||
val resendRet = suspendCancellableCoroutine {
|
|
||||||
service.resendMsg(contact, msgId) { result, _ ->
|
|
||||||
it.resume(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (resendRet != 0 &&
|
|
||||||
resendRet != 4 // 使用OldBDH 100%触发
|
|
||||||
) {
|
|
||||||
resendMsg(contact, msgId, retryCnt - 1, msgHashId)
|
|
||||||
} else {
|
|
||||||
Result.success(SendMsgResult(msgHashId, msgId, System.currentTimeMillis()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result ?: resendMsg(contact, msgId, retryCnt - 1, msgHashId)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
|
||||||
suspend fun sendMessageWithoutMsgId(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
message: JsonArray,
|
|
||||||
fromId: String = peerId,
|
|
||||||
callback: IOperateCallback
|
|
||||||
): Result<SendMsgResult> {
|
|
||||||
val uniseq = generateMsgId(chatType)
|
|
||||||
val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also {
|
|
||||||
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
|
||||||
}.second.filter {
|
|
||||||
it.elementType != -1
|
|
||||||
} as ArrayList<MsgElement>
|
|
||||||
|
|
||||||
// ActionMsg No Care
|
|
||||||
if (msg.isEmpty()) {
|
|
||||||
return Result.success(uniseq.copy(msgTime = System.currentTimeMillis()))
|
|
||||||
}
|
|
||||||
|
|
||||||
val totalSize = msg.filter {
|
|
||||||
it.elementType == MsgConstant.KELEMTYPEPIC ||
|
|
||||||
it.elementType == MsgConstant.KELEMTYPEPTT ||
|
|
||||||
it.elementType == MsgConstant.KELEMTYPEVIDEO
|
|
||||||
}.map {
|
|
||||||
(it.picElement?.fileSize ?: 0) + (it.pttElement?.fileSize
|
|
||||||
?: 0) + (it.videoElement?.fileSize ?: 0)
|
|
||||||
}.reduceOrNull { a, b -> a + b } ?: 0
|
|
||||||
val estimateTime = (totalSize / (300 * 1024)) * 1000 + 2000
|
|
||||||
|
|
||||||
lateinit var sendResult: SendMsgResult // msgTime to msgHash
|
|
||||||
val sendRet = withTimeoutOrNull<Pair<Int, String>>(estimateTime) {
|
|
||||||
suspendCancellableCoroutine {
|
|
||||||
GlobalScope.launch {
|
|
||||||
sendResult = sendMessageWithoutMsgId(chatType, peerId, msg, fromId) { code, message ->
|
|
||||||
callback.onResult(code, message)
|
|
||||||
it.resume(code to message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sendRet?.first != 0) {
|
|
||||||
//return Result.failure(SendMsgException(sendRet?.second ?: "发送消息超时"))
|
|
||||||
return Result.success(uniseq.copy(isTimeout = true))
|
|
||||||
}
|
|
||||||
return Result.success(sendResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendMessageWithoutMsgId(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
message: ArrayList<MsgElement>,
|
|
||||||
fromId: String = peerId,
|
|
||||||
callback: IOperateCallback
|
|
||||||
): SendMsgResult {
|
|
||||||
return sendMessageWithoutMsgId(generateContact(chatType, peerId, fromId), message, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendMessageWithoutMsgId(
|
|
||||||
contact: Contact,
|
|
||||||
message: ArrayList<MsgElement>,
|
|
||||||
callback: IOperateCallback
|
|
||||||
): SendMsgResult {
|
|
||||||
val uniseq = generateMsgId(contact.chatType)
|
|
||||||
val nonMsg: Boolean = message.isEmpty()
|
|
||||||
return if (!nonMsg) {
|
|
||||||
val service = QRoute.api(IMsgService::class.java)
|
|
||||||
if (callback is MsgSvc.MessageCallback) {
|
|
||||||
callback.msgHash = uniseq.msgHashId
|
|
||||||
}
|
|
||||||
|
|
||||||
service.sendMsg(
|
|
||||||
contact,
|
|
||||||
uniseq.qqMsgId,
|
|
||||||
message,
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
|
|
||||||
uniseq.copy(msgTime = System.currentTimeMillis())
|
|
||||||
} else {
|
|
||||||
uniseq.copy(msgHashId = 0, msgTime = System.currentTimeMillis())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendMessageWithMsgId(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
message: JsonArray,
|
|
||||||
callback: IOperateCallback,
|
|
||||||
fromId: String = peerId
|
|
||||||
): SendMsgResult {
|
|
||||||
val uniseq = generateMsgId(chatType)
|
|
||||||
val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also {
|
|
||||||
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
|
||||||
}.second.filter {
|
|
||||||
it.elementType != -1
|
|
||||||
} as ArrayList<MsgElement>
|
|
||||||
val contact = generateContact(chatType, peerId, fromId)
|
|
||||||
val nonMsg: Boolean = message.isEmpty()
|
|
||||||
return if (!nonMsg) {
|
|
||||||
val service = QRoute.api(IMsgService::class.java)
|
|
||||||
if (callback is MsgSvc.MessageCallback) {
|
|
||||||
callback.msgHash = uniseq.msgHashId
|
|
||||||
}
|
|
||||||
|
|
||||||
service.sendMsg(
|
|
||||||
contact,
|
|
||||||
uniseq.qqMsgId,
|
|
||||||
msg,
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
uniseq.copy(msgTime = System.currentTimeMillis())
|
|
||||||
} else {
|
|
||||||
uniseq.copy(msgHashId = 0, msgTime = System.currentTimeMillis())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sendMessageWithMsgId(
|
|
||||||
contact: Contact,
|
|
||||||
message: ArrayList<MsgElement>,
|
|
||||||
uniseq: Long,
|
|
||||||
callback: IOperateCallback
|
|
||||||
): SendMsgResult {
|
|
||||||
val nonMsg: Boolean = message.isEmpty()
|
|
||||||
if (!nonMsg) {
|
|
||||||
val service = QRoute.api(IMsgService::class.java)
|
|
||||||
service.sendMsg(
|
|
||||||
contact,
|
|
||||||
uniseq,
|
|
||||||
message,
|
|
||||||
callback
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return SendMsgResult(
|
|
||||||
msgTime = if (nonMsg) 0 else System.currentTimeMillis(),
|
|
||||||
msgHashId = 0,
|
|
||||||
qqMsgId = uniseq
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun sendMessageNoCb(
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
message: JsonArray,
|
|
||||||
fromId: String = peerId
|
|
||||||
): SendMsgResult {
|
|
||||||
val uniseq = generateMsgId(chatType)
|
|
||||||
val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also {
|
|
||||||
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
|
||||||
}.second.filter {
|
|
||||||
it.elementType != -1
|
|
||||||
} as ArrayList<MsgElement>
|
|
||||||
val contact = generateContact(chatType, peerId, fromId)
|
|
||||||
return if (!message.isEmpty()) {
|
|
||||||
val service = QRoute.api(IMsgService::class.java)
|
|
||||||
return suspendCancellableCoroutine {
|
|
||||||
service.sendMsg(contact, uniseq.qqMsgId, msg) { _, _ ->
|
|
||||||
it.resume(uniseq.copy(msgTime = System.currentTimeMillis()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uniseq.copy(msgHashId = 0, msgTime = 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact {
|
|
||||||
val peerId = when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
|
|
||||||
ContactHelper.getUidByUinAsync(id.toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> id
|
|
||||||
}
|
|
||||||
return if (chatType == MsgConstant.KCHATTYPEGUILD) {
|
|
||||||
Contact(chatType, subId, peerId)
|
|
||||||
} else {
|
|
||||||
Contact(chatType, peerId, subId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun obtainMessageTypeByDetailType(detailType: String): Int {
|
|
||||||
return when (detailType) {
|
|
||||||
"troop", "group" -> MsgConstant.KCHATTYPEGROUP
|
|
||||||
"private" -> MsgConstant.KCHATTYPEC2C
|
|
||||||
"less" -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
|
|
||||||
"guild" -> MsgConstant.KCHATTYPEGUILD
|
|
||||||
else -> error("不支持的消息来源类型")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun obtainDetailTypeByMsgType(msgType: Int): String {
|
|
||||||
return when (msgType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> "group"
|
|
||||||
MsgConstant.KCHATTYPEC2C -> "private"
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> "guild"
|
|
||||||
MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN -> "less"
|
|
||||||
else -> error("不支持的消息来源类型")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun messageArrayToMsgElements(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
messageList: JsonArray
|
|
||||||
): Pair<Boolean, ArrayList<MsgElement>> {
|
|
||||||
val msgList = arrayListOf<MsgElement>()
|
|
||||||
var hasActionMsg = false
|
|
||||||
messageList.forEach {
|
|
||||||
val msg = it.jsonObject
|
|
||||||
val maker = NtMsgElementMaker[msg["type"].asString]
|
|
||||||
if (maker != null) {
|
|
||||||
try {
|
|
||||||
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
|
|
||||||
maker(chatType, msgId, peerId, data).onSuccess { msgElem ->
|
|
||||||
msgList.add(msgElem)
|
|
||||||
}.onFailure {
|
|
||||||
if (it.javaClass != ActionMsgException::class.java) {
|
|
||||||
throw it
|
|
||||||
} else {
|
|
||||||
hasActionMsg = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
LogCenter.log(e.stackTraceToString(), Level.ERROR)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR)
|
|
||||||
return false to arrayListOf()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hasActionMsg to msgList
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun messageArrayToRichText(
|
|
||||||
chatType: Int,
|
|
||||||
msgId: Long,
|
|
||||||
peerId: String,
|
|
||||||
messageList: JsonArray
|
|
||||||
): Result<Pair<String, RichText>> {
|
|
||||||
val elemMaker = ElemMaker()
|
|
||||||
messageList.forEach { element ->
|
|
||||||
val msg = element.asJsonObject
|
|
||||||
val maker = ElemMaker[msg["type"].asString]
|
|
||||||
if (maker != null) {
|
|
||||||
try {
|
|
||||||
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
|
|
||||||
maker(elemMaker, chatType, msgId, peerId, data)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
if (e.javaClass != ActionMsgException::class.java) {
|
|
||||||
LogCenter.log(e.stackTraceToString(), Level.ERROR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Result.success(elemMaker.getDesc() to elemMaker.getRich())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateMsgIdHash(chatType: Int, msgId: Long): Int {
|
|
||||||
val key = when (chatType) {
|
|
||||||
MsgConstant.KCHATTYPEGROUP -> "grp$msgId"
|
|
||||||
MsgConstant.KCHATTYPEC2C -> "c2c$msgId"
|
|
||||||
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> "tmpgrp$msgId"
|
|
||||||
MsgConstant.KCHATTYPEGUILD -> "guild$msgId"
|
|
||||||
else -> error("不支持的消息来源类型 | generateMsgIdHash: $chatType")
|
|
||||||
}
|
|
||||||
return abs(key.hashCode())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateMsgId(chatType: Int): SendMsgResult {
|
|
||||||
val msgId = createMessageUniseq(chatType, System.currentTimeMillis())
|
|
||||||
val hashCode: Int = generateMsgIdHash(chatType, msgId)
|
|
||||||
return SendMsgResult(hashCode, msgId, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMsgMappingByHash(hash: Int): MessageMapping? {
|
|
||||||
val db = MessageDB.getInstance()
|
|
||||||
return db.messageMappingDao().queryByMsgHashId(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMsgMappingBySeq(chatType: Int, peerId: String, msgSeq: Int): MessageMapping? {
|
|
||||||
val db = MessageDB.getInstance()
|
|
||||||
return db.messageMappingDao().queryByMsgSeq(chatType, peerId, msgSeq)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeMsgByHashCode(hashCode: Int) {
|
|
||||||
MessageDB.getInstance()
|
|
||||||
.messageMappingDao()
|
|
||||||
.deleteByMsgHash(hashCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveMsgMapping(
|
|
||||||
hash: Int,
|
|
||||||
qqMsgId: Long,
|
|
||||||
time: Long,
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeerId: String,
|
|
||||||
msgSeq: Int,
|
|
||||||
subChatType: Int = chatType
|
|
||||||
) {
|
|
||||||
val database = MessageDB.getInstance()
|
|
||||||
val mapping = MessageMapping(hash, qqMsgId, chatType, subChatType, peerId, time, msgSeq, subPeerId)
|
|
||||||
database.messageMappingDao().insert(mapping)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveMsgMappingNotExist(
|
|
||||||
hash: Int,
|
|
||||||
qqMsgId: Long,
|
|
||||||
time: Long,
|
|
||||||
chatType: Int,
|
|
||||||
peerId: String,
|
|
||||||
subPeerId: String,
|
|
||||||
msgSeq: Int,
|
|
||||||
subChatType: Int = chatType
|
|
||||||
) {
|
|
||||||
val database = MessageDB.getInstance()
|
|
||||||
val mapping = MessageMapping(hash, qqMsgId, chatType, subChatType, peerId, time, msgSeq, subPeerId)
|
|
||||||
database.messageMappingDao().insertNotExist(mapping)
|
|
||||||
}
|
|
||||||
|
|
||||||
external fun createMessageUniseq(chatType: Int, time: Long): Long
|
|
||||||
|
|
||||||
fun decodeCQCode(code: String): JsonArray {
|
|
||||||
val arrayList = ArrayList<JsonElement>()
|
|
||||||
val msgList = nativeDecodeCQCode(code)
|
|
||||||
msgList.forEach {
|
|
||||||
val params = hashMapOf<String, JsonElement>()
|
|
||||||
it.forEach { (key, value) ->
|
|
||||||
if (key != "_type") {
|
|
||||||
params[key] = value.json
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val data = mapOf(
|
|
||||||
"type" to it["_type"]!!.json,
|
|
||||||
"data" to JsonObject(params)
|
|
||||||
)
|
|
||||||
arrayList.add(data.json)
|
|
||||||
}
|
|
||||||
return arrayList.jsonArray
|
|
||||||
}
|
|
||||||
|
|
||||||
private external fun nativeDecodeCQCode(code: String): List<Map<String, String>>
|
|
||||||
external fun nativeEncodeCQCode(segment: List<Map<String, String>>): String
|
|
||||||
}
|
|
@ -1,97 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.helper
|
|
||||||
|
|
||||||
import io.ktor.client.request.get
|
|
||||||
import io.ktor.client.statement.bodyAsText
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.ark.ArkMsgSvc
|
|
||||||
import moe.fuqiuluo.shamrock.tools.GlobalClient
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asInt
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonArray
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asString
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asStringOrNull
|
|
||||||
import moe.fuqiuluo.shamrock.utils.MD5
|
|
||||||
|
|
||||||
internal object MusicHelper {
|
|
||||||
suspend fun tryShare163MusicById(chatType: Int, peerId: Long, msgId: Long, id: String): Boolean {
|
|
||||||
try {
|
|
||||||
val respond = GlobalClient.get("https://music.163.com/api/song/detail/?id=$id&ids=[$id]")
|
|
||||||
val songInfo = Json.parseToJsonElement(respond.bodyAsText()).asJsonObject["songs"].asJsonArray.first().asJsonObject
|
|
||||||
val name = songInfo["name"].asString
|
|
||||||
val title = songInfo["name"].asString
|
|
||||||
val singerName = songInfo["artists"].asJsonArray.first().asJsonObject["name"].asString
|
|
||||||
val previewUrl = songInfo["album"].asJsonObject["picUrl"].asString
|
|
||||||
val playUrl = "https://music.163.com/song/media/outer/url?id=$id.mp3"
|
|
||||||
val jumpUrl = "https://music.163.com/#/song?id=$id"
|
|
||||||
ArkMsgSvc.tryShareMusic(
|
|
||||||
chatType,
|
|
||||||
peerId,
|
|
||||||
msgId,
|
|
||||||
ArkAppInfo.NetEaseMusic,
|
|
||||||
title.ifBlank { name },
|
|
||||||
singerName,
|
|
||||||
jumpUrl,
|
|
||||||
previewUrl,
|
|
||||||
playUrl
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
LogCenter.log(e.stackTraceToString(), Level.ERROR)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun tryShareQQMusicById(chatType: Int, peerId: Long, msgId: Long, id: String): Boolean {
|
|
||||||
try {
|
|
||||||
val respond = GlobalClient.get("https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8¬ice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:$id},%22module%22:%22music.pf_song_detail_svr%22}}")
|
|
||||||
val songInfo = Json.parseToJsonElement(respond.bodyAsText()).asJsonObject["songinfo"].asJsonObject
|
|
||||||
if (songInfo["code"].asInt != 0) {
|
|
||||||
LogCenter.log("获取QQ音乐($id)的歌曲信息失败。")
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
val data = songInfo["data"].asJsonObject
|
|
||||||
val trackInfo = data["track_info"].asJsonObject
|
|
||||||
val mid = trackInfo["mid"].asString
|
|
||||||
val previewMid = trackInfo["album"].asJsonObject["mid"].asString
|
|
||||||
val singerMid = trackInfo["singer"].asJsonArrayOrNull?.let {
|
|
||||||
it[0].asJsonObject["mid"].asStringOrNull
|
|
||||||
} ?: ""
|
|
||||||
val name = trackInfo["name"].asString
|
|
||||||
val title = trackInfo["title"].asString
|
|
||||||
val singerName = trackInfo["singer"].asJsonArray.first().asJsonObject["name"].asString
|
|
||||||
val vs = trackInfo["vs"].asJsonArrayOrNull?.let {
|
|
||||||
it[0].asStringOrNull
|
|
||||||
} ?: ""
|
|
||||||
val code = MD5.getMd5Hex("${mid}q;z(&l~sdf2!nK".toByteArray()).substring(0 .. 4).uppercase()
|
|
||||||
val playUrl = "http://c6.y.qq.com/rsc/fcgi-bin/fcg_pyq_play.fcg?songid=&songmid=$mid&songtype=1&fromtag=50&uin=&code=$code"
|
|
||||||
val previewUrl = if (vs.isNotEmpty()) {
|
|
||||||
"http://y.gtimg.cn/music/photo_new/T062R150x150M000$vs}.jpg"
|
|
||||||
} else if (previewMid.isNotEmpty()) {
|
|
||||||
"http://y.gtimg.cn/music/photo_new/T002R150x150M000$previewMid.jpg"
|
|
||||||
} else if (singerMid.isNotEmpty()){
|
|
||||||
"http://y.gtimg.cn/music/photo_new/T001R150x150M000$singerMid.jpg"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
val jumpUrl = "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=${mid}&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare"
|
|
||||||
ArkMsgSvc.tryShareMusic(
|
|
||||||
chatType,
|
|
||||||
peerId,
|
|
||||||
msgId,
|
|
||||||
ArkAppInfo.QQMusic,
|
|
||||||
title.ifBlank { name },
|
|
||||||
singerName,
|
|
||||||
jumpUrl,
|
|
||||||
previewUrl,
|
|
||||||
playUrl
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
LogCenter.log(e.stackTraceToString(), Level.ERROR)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,174 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote
|
|
||||||
|
|
||||||
import io.ktor.server.application.Application
|
|
||||||
import io.ktor.server.application.install
|
|
||||||
import io.ktor.server.engine.ApplicationEngine
|
|
||||||
import io.ktor.server.engine.ApplicationEngineEnvironmentBuilder
|
|
||||||
import io.ktor.server.engine.applicationEngineEnvironment
|
|
||||||
import io.ktor.server.engine.connector
|
|
||||||
import io.ktor.server.engine.embeddedServer
|
|
||||||
import io.ktor.server.engine.sslConnector
|
|
||||||
import io.ktor.server.netty.Netty
|
|
||||||
import io.ktor.server.routing.routing
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.sync.Mutex
|
|
||||||
import kotlinx.coroutines.sync.withLock
|
|
||||||
import moe.fuqiuluo.shamrock.remote.api.*
|
|
||||||
import moe.fuqiuluo.shamrock.remote.config.*
|
|
||||||
import moe.fuqiuluo.shamrock.remote.plugin.Auth
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.internal.DataRequester
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
import java.security.KeyStore
|
|
||||||
|
|
||||||
|
|
||||||
internal object HTTPServer {
|
|
||||||
@JvmStatic
|
|
||||||
var isServiceStarted = false
|
|
||||||
internal var startTime = 0L
|
|
||||||
|
|
||||||
private val actionMutex = Mutex()
|
|
||||||
private lateinit var server: ApplicationEngine
|
|
||||||
internal var currServerPort: Int = 0
|
|
||||||
|
|
||||||
private fun Application.configModule() {
|
|
||||||
install(Auth).let {
|
|
||||||
val token = ShamrockConfig.getToken()
|
|
||||||
if (token.isBlank()) {
|
|
||||||
LogCenter.log("未配置Token,将不进行鉴权。", Level.WARN)
|
|
||||||
} else {
|
|
||||||
LogCenter.log("配置Token: $token", Level.INFO)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
contentNegotiation()
|
|
||||||
statusPages()
|
|
||||||
routing {
|
|
||||||
echoVersion()
|
|
||||||
obtainFrameworkInfo()
|
|
||||||
registerBDH()
|
|
||||||
userAction()
|
|
||||||
messageAction()
|
|
||||||
troopAction()
|
|
||||||
friendAction()
|
|
||||||
ticketActions()
|
|
||||||
fetchRes()
|
|
||||||
showLog()
|
|
||||||
profileRouter()
|
|
||||||
weatherAction()
|
|
||||||
otherAction()
|
|
||||||
guildAction()
|
|
||||||
testAction()
|
|
||||||
requestRouter()
|
|
||||||
fav()
|
|
||||||
if (ShamrockConfig.isDev()) {
|
|
||||||
qsign()
|
|
||||||
obtainProtocolData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// intercept(ApplicationCallPipeline.Plugins) {
|
|
||||||
// call.response.headers.appendIfAbsent("Content-Type", ContentType.Application.Json.toString())
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun ApplicationEngineEnvironmentBuilder.configSSL() {
|
|
||||||
try {
|
|
||||||
val keyStoreFile = ShamrockConfig.getKeyStorePath()
|
|
||||||
val pwd = ShamrockConfig.sslPwd().also {
|
|
||||||
if (it == null || it.isEmpty()) {
|
|
||||||
LogCenter.log("SSL 密码未填写。", Level.ERROR)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val privatePwd = ShamrockConfig.sslPrivatePwd().also {
|
|
||||||
if (it.isNullOrEmpty()) {
|
|
||||||
LogCenter.log("SSL Private密码未填写。", Level.ERROR)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val keyAlias = ShamrockConfig.sslAlias().also {
|
|
||||||
if (it.isNullOrEmpty()) {
|
|
||||||
LogCenter.log("SSL Alias未填写。", Level.ERROR)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val keyStore = KeyStore.getInstance("BKS")
|
|
||||||
keyStore.load(null, pwd)
|
|
||||||
|
|
||||||
keyStoreFile?.inputStream().use {
|
|
||||||
keyStore.load(it, pwd)
|
|
||||||
}
|
|
||||||
|
|
||||||
sslConnector(
|
|
||||||
keyStore = keyStore,
|
|
||||||
keyAlias = keyAlias!!,
|
|
||||||
keyStorePassword = { pwd!! },
|
|
||||||
privateKeyPassword = { privatePwd!!.toCharArray() }) {
|
|
||||||
this.port = ShamrockConfig.getSslPort()
|
|
||||||
this.keyStorePath = keyStoreFile
|
|
||||||
LogCenter.log("SSL 配置成功,端口: ${this.port}", Level.INFO)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LogCenter.log("SSL 配置错误: ${e.stackTraceToString()}", Level.ERROR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun start(port: Int) {
|
|
||||||
if (isServiceStarted) return
|
|
||||||
actionMutex.withLock {
|
|
||||||
val environment = applicationEngineEnvironment {
|
|
||||||
log = LoggerFactory.getLogger("ktor.application")
|
|
||||||
connector {
|
|
||||||
this.port = port
|
|
||||||
}
|
|
||||||
if (ShamrockConfig.ssl())
|
|
||||||
configSSL()
|
|
||||||
module { configModule() }
|
|
||||||
}
|
|
||||||
server = embeddedServer(Netty, environment)
|
|
||||||
server.start(wait = false)
|
|
||||||
}
|
|
||||||
startTime = System.currentTimeMillis()
|
|
||||||
isServiceStarted = true
|
|
||||||
currServerPort = port
|
|
||||||
LogCenter.log("Start HTTP Server: http://0.0.0.0:$currServerPort/")
|
|
||||||
DataRequester.request("success", values = mapOf(
|
|
||||||
"port" to currServerPort,
|
|
||||||
"voice" to NativeLoader.isVoiceLoaded
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isActive(): Boolean {
|
|
||||||
return server.application.isActive
|
|
||||||
}
|
|
||||||
|
|
||||||
fun changePort(port: Int) {
|
|
||||||
if (currServerPort == port && isServiceStarted) return
|
|
||||||
GlobalScope.launch {
|
|
||||||
stop()
|
|
||||||
start(port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun stop() {
|
|
||||||
actionMutex.withLock {
|
|
||||||
server.stop()
|
|
||||||
isServiceStarted = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restart() {
|
|
||||||
if(!isServiceStarted) return
|
|
||||||
val post = currServerPort
|
|
||||||
GlobalScope.launch {
|
|
||||||
stop()
|
|
||||||
start(post)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,177 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonArray
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import kotlinx.serialization.json.JsonPrimitive
|
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.EmptyObject
|
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.Status
|
|
||||||
import moe.fuqiuluo.shamrock.remote.structures.resultToString
|
|
||||||
import moe.fuqiuluo.shamrock.tools.*
|
|
||||||
import moe.fuqiuluo.shamrock.tools.json
|
|
||||||
|
|
||||||
internal object ActionManager {
|
|
||||||
val actionMap = mutableMapOf<String, IActionHandler>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
initManager()
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun get(action: String): IActionHandler? {
|
|
||||||
return actionMap[action]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal abstract class IActionHandler {
|
|
||||||
protected abstract suspend fun internalHandle(session: ActionSession): String
|
|
||||||
|
|
||||||
open val requiredParams: Array<String> = arrayOf()
|
|
||||||
|
|
||||||
suspend fun handle(session: ActionSession): String {
|
|
||||||
requiredParams.forEach {
|
|
||||||
if (!session.has(it)) {
|
|
||||||
return noParam(it, session.echo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return internalHandle(session)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun ok(
|
|
||||||
msg: String = "",
|
|
||||||
echo: JsonElement
|
|
||||||
): String {
|
|
||||||
return resultToString(true, Status.Ok, EmptyObject, msg, echo = echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected inline fun <reified T> ok(data: T, echo: JsonElement, msg: String = ""): String {
|
|
||||||
return resultToString(true, Status.Ok, data!!, msg, echo = echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun noParam(paramName: String, echo: JsonElement): String {
|
|
||||||
return failed(Status.BadParam, "lack of [$paramName]", echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun badParam(why: String, echo: JsonElement): String {
|
|
||||||
return failed(Status.BadParam, why, echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun error(why: String, echo: JsonElement, arrayResult: Boolean = false): String {
|
|
||||||
return failed(Status.InternalHandlerError, why, echo, arrayResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun logic(why: String, echo: JsonElement, arraayResult: Boolean = false): String {
|
|
||||||
return failed(Status.LogicError, why, echo, arraayResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun failed(status: Status, msg: String, echo: JsonElement, arrResult: Boolean = false): String {
|
|
||||||
return resultToString(false, status, if (arrResult) EmptyJsonArray else EmptyJsonObject, msg, echo = echo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class ActionSession {
|
|
||||||
private val params: JsonObject
|
|
||||||
internal val echo: JsonElement
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
values: Map<String, Any?>,
|
|
||||||
echo: JsonElement = EmptyJsonString
|
|
||||||
) {
|
|
||||||
val map = hashMapOf<String, JsonElement>()
|
|
||||||
values.forEach { (key, value) ->
|
|
||||||
if (value != null) {
|
|
||||||
when (value) {
|
|
||||||
is String -> map[key] = value.json
|
|
||||||
is Number -> map[key] = value.json
|
|
||||||
is Char -> map[key] = JsonPrimitive(value.code.toByte())
|
|
||||||
is Boolean -> map[key] = value.json
|
|
||||||
is JsonObject -> map[key] = value
|
|
||||||
is JsonArray -> map[key] = value
|
|
||||||
else -> error("unsupported type: ${value::class.java}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.echo = echo
|
|
||||||
this.params = JsonObject(map)
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
params: JsonObject,
|
|
||||||
echo: JsonElement
|
|
||||||
) {
|
|
||||||
this.echo = echo
|
|
||||||
this.params = params
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLong(key: String): Long {
|
|
||||||
return params[key].asLong
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getLongOrNull(key: String): Long? {
|
|
||||||
return params[key].asLongOrNull
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getInt(key: String): Int {
|
|
||||||
return params[key].asInt
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getIntOrNull(key: String): Int? {
|
|
||||||
return params[key].asIntOrNull
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isString(key: String): Boolean {
|
|
||||||
val element = params[key]
|
|
||||||
return element is JsonPrimitive && element.isString
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isArray(key: String): Boolean {
|
|
||||||
val element = params[key]
|
|
||||||
return element is JsonArray
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isObject(key: String): Boolean {
|
|
||||||
val element = params[key]
|
|
||||||
return element is JsonObject
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getString(key: String): String {
|
|
||||||
return params[key].asString
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getStringOrNull(key: String): String? {
|
|
||||||
return params[key].asStringOrNull
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBoolean(key: String): Boolean {
|
|
||||||
return params[key].asBoolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBooleanOrDefault(key: String, default: Boolean? = null): Boolean {
|
|
||||||
return params[key].asBooleanOrNull ?: default as Boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getJsonElement(key: String): JsonElement {
|
|
||||||
return params[key]!!
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getJsonElementOrNull(key: String): JsonElement? {
|
|
||||||
return params[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getObject(key: String): JsonObject {
|
|
||||||
return params[key].asJsonObject
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getObjectOrNull(key: String): JsonObject? {
|
|
||||||
return params[key].asJsonObjectOrNull
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getArray(key: String): JsonArray {
|
|
||||||
return params[key].asJsonArray
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getArrayOrNull(key: String): JsonArray? {
|
|
||||||
return params[key].asJsonArrayOrNull
|
|
||||||
}
|
|
||||||
|
|
||||||
fun has(key: String) = params.containsKey(key)
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.ark.LightAppSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.ark.data.ArkAppInfo
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("adapt_share_json")
|
|
||||||
internal object AdaptShareJson: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
//val json = if(session.isString("json")) session.getString("json")
|
|
||||||
//else session.getJsonElement("json").toString()
|
|
||||||
val cover = session.getString("cover")
|
|
||||||
val desc = session.getString("desc")
|
|
||||||
val url = session.getStringOrNull("url") ?: ""
|
|
||||||
return invoke(cover, desc, url, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(cover: String, desc: String, url: String, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
/*
|
|
||||||
ArkMsgSvc.tryShareJsonMessage(json).onSuccess {
|
|
||||||
return ok(SignArkMessageResult(it), echo = echo)
|
|
||||||
}.onFailure {
|
|
||||||
return error(it.message ?: it.toString(), echo)
|
|
||||||
}*/
|
|
||||||
LightAppSvc.adaptShareJumpUrl(ArkAppInfo.DanMaKu, cover, desc, url).onSuccess {
|
|
||||||
return ok(AdaptShareInfo(it), echo = echo)
|
|
||||||
}.onFailure {
|
|
||||||
return error(it.message ?: it.toString(), echo)
|
|
||||||
}
|
|
||||||
return logic("logic error", echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("cover", "desc")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class AdaptShareInfo(
|
|
||||||
val result: String
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("set_group_ban")
|
|
||||||
internal object BanTroopMember: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val groupId = session.getLong("group_id")
|
|
||||||
val userId = session.getLong("user_id")
|
|
||||||
val duration = session.getIntOrNull("duration") ?: (30 * 60)
|
|
||||||
|
|
||||||
return invoke(groupId, userId, duration, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun invoke(
|
|
||||||
groupId: Long,
|
|
||||||
userId: Long,
|
|
||||||
duration: Int = 30 * 60,
|
|
||||||
echo: JsonElement = EmptyJsonString
|
|
||||||
): String {
|
|
||||||
if (!GroupSvc.isAdmin(groupId)) {
|
|
||||||
return logic("You are not the administrator of the group.", echo)
|
|
||||||
}
|
|
||||||
GroupSvc.banMember(groupId, userId, duration)
|
|
||||||
return ok("成功", echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("group_id", "user_id")
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("clean_cache")
|
|
||||||
internal object CleanCache: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
return invoke(session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun invoke(echo: JsonElement = EmptyJsonString): String {
|
|
||||||
FileUtils.clearCache()
|
|
||||||
MMKVFetcher.mmkvWithId("hash2id")
|
|
||||||
.clear()
|
|
||||||
MMKVFetcher.mmkvWithId("id2id")
|
|
||||||
.clear()
|
|
||||||
MMKVFetcher.mmkvWithId("seq2id")
|
|
||||||
.clear()
|
|
||||||
MMKVFetcher.mmkvWithId("audio2silk")
|
|
||||||
.clear()
|
|
||||||
return ok("成功", echo)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.shamrock.helper.MessageHelper
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("clear_msgs", ["clear_messages"])
|
|
||||||
internal object ClearMsgs: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val msgType = session.getString("message_type")
|
|
||||||
val peerId = session.getString(if (msgType == "group") "group_id" else "user_id")
|
|
||||||
return invoke(msgType, peerId, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(
|
|
||||||
msgType: String,
|
|
||||||
peerId: String,
|
|
||||||
echo: JsonElement = EmptyJsonString
|
|
||||||
): String {
|
|
||||||
val chatType = MessageHelper.obtainMessageTypeByDetailType(msgType)
|
|
||||||
val contact = MessageHelper.generateContact(chatType, peerId, "")
|
|
||||||
NTServiceFetcher.kernelService.wrapperSession.msgService.clearMsgRecords(contact, null)
|
|
||||||
return ok(echo = echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String>
|
|
||||||
get() = arrayOf("message_type")
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.FileSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("create_group_file_folder")
|
|
||||||
internal object CreateGroupFileFolder: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val groupId = session.getLong("group_id")
|
|
||||||
val folderName = session.getString("name")
|
|
||||||
val echo = session.echo
|
|
||||||
return invoke(groupId, folderName, echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(groupId: Long, folderName: String, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
val result = FileSvc.createFileFolder(groupId, folderName)
|
|
||||||
if (result.isFailure) {
|
|
||||||
return ok(msg = result.exceptionOrNull()?.message ?: "无法创建群文件夹", echo = echo)
|
|
||||||
}
|
|
||||||
return ok(data = result.getOrThrow(), msg = "成功", echo = echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("group_id", "name")
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
@file:Suppress("UNCHECKED_CAST")
|
|
||||||
|
|
||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.GProSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("create_guild_role")
|
|
||||||
internal object CreateGuildRole: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val guildId = session.getString("guild_id").toULong()
|
|
||||||
val name = session.getString("name")
|
|
||||||
val color = session.getLong("color")
|
|
||||||
val initialUsers = session.getArray("initial_users").map {
|
|
||||||
it.asString.toULong()
|
|
||||||
}
|
|
||||||
return invoke(guildId, color, name, initialUsers, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(guildId: ULong, color: Long, name: String, initialUsers: List<ULong>, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
val result = GProSvc.createGuildRole(guildId, name, color, initialUsers as ArrayList<Long>).onFailure {
|
|
||||||
return error(it.message ?: "Unknown error", echo)
|
|
||||||
}.getOrThrow()
|
|
||||||
return ok(data = CreateGuildRoleResult(
|
|
||||||
result.roleId.toULong()
|
|
||||||
), echo = echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("guild_id", "color", "name", "initial_users")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CreateGuildRoleResult(
|
|
||||||
@SerialName("role_id") val roleId: ULong
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("delete_essence_msg", ["delete_essence_message"])
|
|
||||||
internal object DeleteEssenceMessage: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val messageId = session.getInt("message_id")
|
|
||||||
return invoke(messageId, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(messageId: Int, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
val msg = MsgSvc.getMsg(messageId).onFailure {
|
|
||||||
return logic("Obtain msg failed, please check your msg_id.", echo)
|
|
||||||
}.getOrThrow()
|
|
||||||
val (success, tip) = GroupSvc.deleteEssenceMessage(
|
|
||||||
if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0,
|
|
||||||
msg.msgSeq,
|
|
||||||
msg.msgRandom
|
|
||||||
)
|
|
||||||
return if (success) {
|
|
||||||
ok("成功", echo)
|
|
||||||
} else {
|
|
||||||
logic(tip, echo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.FileSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("delete_group_file")
|
|
||||||
internal object DeleteGroupFile: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val groupId = session.getLong("group_id")
|
|
||||||
val fileId = session.getString("file_id")
|
|
||||||
val busid = session.getInt("busid")
|
|
||||||
return invoke(groupId, fileId, busid, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(groupId: Long, fileId: String, bizId: Int, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
if(!FileSvc.deleteGroupFile(groupId, bizId, fileId)) {
|
|
||||||
return error("删除失败", echo = echo)
|
|
||||||
}
|
|
||||||
return ok("成功", echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("group_id", "file_id", "busid")
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.FileSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("delete_group_folder")
|
|
||||||
internal object DeleteGroupFolder: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val groupId = session.getLong("group_id")
|
|
||||||
val folderId = session.getString("folder_id")
|
|
||||||
return invoke(groupId, folderId, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(groupId: Long, folderId: String, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
if(!FileSvc.deleteGroupFolder(groupId, folderId)) {
|
|
||||||
return error(why = "删除群文件夹失败", echo = echo)
|
|
||||||
}
|
|
||||||
return ok(msg = "成功", echo = echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("group_id", "folder_id")
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.GProSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("delete_guild_role")
|
|
||||||
internal object DeleteGuildRole: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val guildId = session.getString("guild_id").toULong()
|
|
||||||
val roleId = session.getString("role_id").toULong()
|
|
||||||
return invoke(guildId, roleId, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun invoke(guildId: ULong, roleId: ULong, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
GProSvc.deleteGuildRole(guildId, roleId)
|
|
||||||
return ok("success", echo = echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("guild_id", "role_id")
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("delete_message", ["delete_msg"])
|
|
||||||
internal object DeleteMessage: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val hashCode = session.getString("message_id").toInt()
|
|
||||||
return invoke(hashCode, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(msgHash: Int, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
MsgSvc.recallMsg(msgHash)
|
|
||||||
return ok("成功", echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("message_id")
|
|
||||||
}
|
|
@ -1,140 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import android.util.Base64
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.shamrock.tools.asString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.DownloadUtils
|
|
||||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
|
||||||
import moe.fuqiuluo.shamrock.utils.MD5
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
@OneBotHandler("download_file")
|
|
||||||
internal object DownloadFile: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val url = session.getStringOrNull("url")
|
|
||||||
val name = session.getStringOrNull("name")
|
|
||||||
val b64 = session.getStringOrNull("base64")
|
|
||||||
val rootDir = session.getStringOrNull("root")
|
|
||||||
val threadCnt = session.getIntOrNull("thread_cnt") ?: 3
|
|
||||||
val headers = if (session.has("headers")) (if (session.isArray("headers")) {
|
|
||||||
session.getArray("headers").map {
|
|
||||||
it.asString
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
session.getString("headers").split("\r\n")
|
|
||||||
}) else emptyList()
|
|
||||||
return invoke(url, b64, threadCnt, headers, name, rootDir, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(
|
|
||||||
url: String?,
|
|
||||||
base64: String?,
|
|
||||||
threadCnt: Int,
|
|
||||||
headers: List<String>,
|
|
||||||
name: String?,
|
|
||||||
root: String?,
|
|
||||||
echo: JsonElement = EmptyJsonString
|
|
||||||
): String {
|
|
||||||
if (url != null) {
|
|
||||||
val headerMap = mutableMapOf(
|
|
||||||
"User-Agent" to "Shamrock"
|
|
||||||
)
|
|
||||||
headers.forEach {
|
|
||||||
val pair = it.split("=")
|
|
||||||
if (pair.size >= 2) {
|
|
||||||
val (k, v) = pair
|
|
||||||
headerMap[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return invoke(url, threadCnt, headerMap, name, root, echo)
|
|
||||||
} else if (base64 != null) {
|
|
||||||
return invoke(base64, name, root, echo)
|
|
||||||
} else {
|
|
||||||
return noParam("url/base64", echo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun invoke(
|
|
||||||
base64: String,
|
|
||||||
name: String?,
|
|
||||||
root: String?,
|
|
||||||
echo: JsonElement
|
|
||||||
): String {
|
|
||||||
kotlin.runCatching {
|
|
||||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
|
||||||
FileUtils.getTmpFile("cache").also {
|
|
||||||
it.writeBytes(bytes)
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
var tmp = if (name == null)
|
|
||||||
FileUtils.renameByMd5(it)
|
|
||||||
else it.parentFile!!.resolve(name).also { target ->
|
|
||||||
it.renameTo(target)
|
|
||||||
it.delete()
|
|
||||||
}
|
|
||||||
if (root != null) {
|
|
||||||
tmp = File(root).resolve(name ?: tmp.name).also {
|
|
||||||
tmp.renameTo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ok(data = DownloadResult(
|
|
||||||
file = tmp.absolutePath,
|
|
||||||
md5 = MD5.genFileMd5Hex(tmp.absolutePath)
|
|
||||||
), msg = "成功", echo = echo)
|
|
||||||
}.onFailure {
|
|
||||||
return logic("Base64格式错误", echo)
|
|
||||||
}
|
|
||||||
return logic("未知错误", echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(
|
|
||||||
url: String,
|
|
||||||
threadCnt: Int,
|
|
||||||
headers: Map<String, String>,
|
|
||||||
name: String?,
|
|
||||||
root: String?,
|
|
||||||
echo: JsonElement = EmptyJsonString
|
|
||||||
): String {
|
|
||||||
return kotlin.runCatching {
|
|
||||||
var tmp = FileUtils.getTmpFile("cache")
|
|
||||||
if(!DownloadUtils.download(
|
|
||||||
urlAdr = url,
|
|
||||||
dest = tmp,
|
|
||||||
headers = headers,
|
|
||||||
threadCount = threadCnt
|
|
||||||
)) {
|
|
||||||
return error("下载失败 (0x1)", echo)
|
|
||||||
}
|
|
||||||
tmp = if (name == null) {
|
|
||||||
FileUtils.renameByMd5(tmp)
|
|
||||||
} else {
|
|
||||||
val newFile = tmp.parentFile!!.resolve(name)
|
|
||||||
tmp.renameTo(newFile)
|
|
||||||
newFile
|
|
||||||
}
|
|
||||||
if (root != null) {
|
|
||||||
tmp = File(root).resolve(name ?: tmp.name).also {
|
|
||||||
tmp.renameTo(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ok(data = DownloadResult(
|
|
||||||
file = tmp.absolutePath,
|
|
||||||
md5 = MD5.genFileMd5Hex(tmp.absolutePath)
|
|
||||||
), msg = "成功", echo = echo)
|
|
||||||
}.getOrElse {
|
|
||||||
logic(it.stackTraceToString(), echo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class DownloadResult(
|
|
||||||
val file: String,
|
|
||||||
val md5: String
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,159 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import android.graphics.BitmapFactory
|
|
||||||
import kotlinx.io.core.ByteReadPacket
|
|
||||||
import kotlinx.io.core.discardExact
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromByteArray
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.QFavSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.CryptTools
|
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
|
||||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
|
||||||
import protobuf.fav.WeiyunComm
|
|
||||||
|
|
||||||
@OneBotHandler("fav.add_image_msg", ["fav.add_image_message"])
|
|
||||||
internal object FavAddImageMsg: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val uin = session.getLong("user_id")
|
|
||||||
val nickName = session.getString("nick")
|
|
||||||
val groupName = session.getStringOrNull("group_name") ?: ""
|
|
||||||
val groupId = session.getLongOrNull("group_id") ?: 0L
|
|
||||||
val file = session.getString("file")
|
|
||||||
return invoke(uin, nickName, file, groupName, groupId, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(
|
|
||||||
uin: Long,
|
|
||||||
nickName: String,
|
|
||||||
fileText: String,
|
|
||||||
groupName: String = "",
|
|
||||||
groupId: Long = 0L,
|
|
||||||
echo: JsonElement = EmptyJsonString
|
|
||||||
): String {
|
|
||||||
val image = fileText.let {
|
|
||||||
val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase()
|
|
||||||
if (md5.length == 32) {
|
|
||||||
FileUtils.getFileByMd5(it)
|
|
||||||
} else {
|
|
||||||
FileUtils.parseAndSave(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val options = BitmapFactory.Options()
|
|
||||||
BitmapFactory.decodeFile(image.absolutePath, options)
|
|
||||||
lateinit var picUrl: String
|
|
||||||
lateinit var picId: String
|
|
||||||
lateinit var itemId: String
|
|
||||||
lateinit var md5: String
|
|
||||||
|
|
||||||
QFavSvc.applyUpImageMsg(uin, nickName,
|
|
||||||
image = image,
|
|
||||||
groupName = groupName,
|
|
||||||
groupId = groupId,
|
|
||||||
width = options.outWidth,
|
|
||||||
height = options.outHeight
|
|
||||||
).onSuccess {
|
|
||||||
if (it.mHttpCode == 200 && it.mResult == 0) {
|
|
||||||
val readPacket = ByteReadPacket(DeflateTools.ungzip(it.mRespData))
|
|
||||||
readPacket.discardExact(6)
|
|
||||||
val allLength = readPacket.readInt()
|
|
||||||
val dataLength = readPacket.readInt()
|
|
||||||
val headLength = allLength - dataLength - 16
|
|
||||||
readPacket.discardExact(2)
|
|
||||||
ByteArray(headLength).also {
|
|
||||||
readPacket.readFully(it, 0, it.size)
|
|
||||||
}
|
|
||||||
val data = ByteArray(dataLength).also {
|
|
||||||
readPacket.readFully(it, 0, it.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
val resp = data.decodeProtobuf<WeiyunComm>()
|
|
||||||
.resp!!.fastUploadResourceResp!!.picResultList!!.first()
|
|
||||||
val picInfo = resp.picInfo!!
|
|
||||||
picUrl = picInfo.uri
|
|
||||||
picId = picInfo.picId
|
|
||||||
md5 = picInfo.name
|
|
||||||
} else {
|
|
||||||
return logic(it.mErrDesc, echo)
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
return error(it.message ?: it.toString(), echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
val sha = CryptTools
|
|
||||||
.getSHA1("/storage/emulated/0/Android/data/com.tencent.mobileqq/Tencent/QQ_Collection/pic/" + md5.uppercase() + "_0")
|
|
||||||
|
|
||||||
image.inputStream().use {
|
|
||||||
var offset = 0L
|
|
||||||
val block = ByteArray(131072)
|
|
||||||
var rest = image.length()
|
|
||||||
do {
|
|
||||||
val length = if (rest <= 131072) rest else 131072L
|
|
||||||
if(it.read(block, 0, length.toInt()) != -1) {
|
|
||||||
QFavSvc.sendPicUpBlock(
|
|
||||||
fileSize = image.length(),
|
|
||||||
offset = offset,
|
|
||||||
block = block,
|
|
||||||
blockSize = length,
|
|
||||||
pid = picId,
|
|
||||||
sha = sha
|
|
||||||
).onFailure {
|
|
||||||
return error(it.message ?: it.toString(), echo)
|
|
||||||
}
|
|
||||||
offset += length
|
|
||||||
rest -= length
|
|
||||||
} else {
|
|
||||||
rest = -1
|
|
||||||
}
|
|
||||||
} while (rest > 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
QFavSvc.addImageMsg(
|
|
||||||
uin, nickName, groupId, groupName, picUrl, picId, options.outWidth, options.outHeight, image.length(), md5.uppercase()
|
|
||||||
).onFailure {
|
|
||||||
return error(it.message ?: it.toString(), echo)
|
|
||||||
}.onSuccess {
|
|
||||||
if (it.mHttpCode == 200 && it.mResult == 0) {
|
|
||||||
val readPacket = ByteReadPacket(DeflateTools.ungzip(it.mRespData))
|
|
||||||
readPacket.discardExact(6)
|
|
||||||
val allLength = readPacket.readInt()
|
|
||||||
val dataLength = readPacket.readInt()
|
|
||||||
val headLength = allLength - dataLength - 16
|
|
||||||
readPacket.discardExact(2)
|
|
||||||
ByteArray(headLength).also {
|
|
||||||
readPacket.readFully(it, 0, it.size)
|
|
||||||
}
|
|
||||||
val data = ByteArray(dataLength).also {
|
|
||||||
readPacket.readFully(it, 0, it.size)
|
|
||||||
}
|
|
||||||
val resp = data.decodeProtobuf<WeiyunComm>().resp!!
|
|
||||||
itemId = resp.addRichMediaResp!!.cid
|
|
||||||
}
|
|
||||||
|
|
||||||
System.gc()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok(PicInfo(
|
|
||||||
picUrl = picUrl,
|
|
||||||
picId = picId,
|
|
||||||
id = itemId
|
|
||||||
), echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("user_id", "nick", "file")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class PicInfo(
|
|
||||||
@SerialName("pic_url") val picUrl: String,
|
|
||||||
@SerialName("pic_id") val picId: String,
|
|
||||||
@SerialName("id") val id: String
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.io.core.ByteReadPacket
|
|
||||||
import kotlinx.io.core.discardExact
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromByteArray
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.QFavSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
|
||||||
import protobuf.fav.WeiyunComm
|
|
||||||
|
|
||||||
@OneBotHandler("fav.add_text_msg", ["fav.add_text_message"])
|
|
||||||
internal object FavAddTextMsg: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val uin = session.getLong("user_id")
|
|
||||||
val nickName = session.getString("nick")
|
|
||||||
val groupName = session.getStringOrNull("group_name") ?: ""
|
|
||||||
val groupId = session.getLongOrNull("group_id") ?: 0L
|
|
||||||
val time = session.getLongOrNull("time") ?: System.currentTimeMillis()
|
|
||||||
val content = session.getString("content")
|
|
||||||
return invoke(uin, nickName, time, content, groupName, groupId, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(
|
|
||||||
uin: Long,
|
|
||||||
nickName: String,
|
|
||||||
time: Long = System.currentTimeMillis(),
|
|
||||||
content: String,
|
|
||||||
groupName: String = "",
|
|
||||||
groupId: Long = 0L,
|
|
||||||
echo: JsonElement = EmptyJsonString
|
|
||||||
): String {
|
|
||||||
QFavSvc.addRichMediaMsg(uin, nickName,
|
|
||||||
time = time,
|
|
||||||
content = content,
|
|
||||||
groupName = groupName,
|
|
||||||
groupId = groupId
|
|
||||||
).onSuccess {
|
|
||||||
return if (it.mHttpCode == 200 && it.mResult == 0) {
|
|
||||||
val readPacket = ByteReadPacket(DeflateTools.ungzip(it.mRespData))
|
|
||||||
readPacket.discardExact(6)
|
|
||||||
val allLength = readPacket.readInt()
|
|
||||||
val dataLength = readPacket.readInt()
|
|
||||||
val headLength = allLength - dataLength - 16
|
|
||||||
readPacket.discardExact(2)
|
|
||||||
ByteArray(headLength).also {
|
|
||||||
readPacket.readFully(it, 0, it.size)
|
|
||||||
}
|
|
||||||
val data = ByteArray(dataLength).also {
|
|
||||||
readPacket.readFully(it, 0, it.size)
|
|
||||||
}
|
|
||||||
val resp = data.decodeProtobuf<WeiyunComm>().resp!!.addRichMediaResp!!
|
|
||||||
ok(data = QFavItem(resp.cid), echo)
|
|
||||||
} else {
|
|
||||||
logic(it.mErrDesc, echo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ok("请求已提交", echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("user_id", "nick", "content")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class QFavItem(
|
|
||||||
@SerialName("id") val id: String
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
@file:OptIn(ExperimentalSerializationApi::class)
|
|
||||||
|
|
||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.io.core.ByteReadPacket
|
|
||||||
import kotlinx.io.core.discardExact
|
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromByteArray
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.QFavSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
|
||||||
import protobuf.fav.WeiyunComm
|
|
||||||
|
|
||||||
@OneBotHandler("fav.get_item_content")
|
|
||||||
internal object FavGetItemContent: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val id = session.getString("id")
|
|
||||||
return invoke(id, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(
|
|
||||||
id: String,
|
|
||||||
echo: JsonElement = EmptyJsonString
|
|
||||||
): String {
|
|
||||||
val respData = DeflateTools.ungzip(QFavSvc.getItemContent(id).onSuccess {
|
|
||||||
if (it.mHttpCode != 200 || it.mResult != 0) {
|
|
||||||
return logic(it.mErrDesc, echo)
|
|
||||||
}
|
|
||||||
}.getOrThrow().mRespData)
|
|
||||||
val readPacket = ByteReadPacket(respData)
|
|
||||||
readPacket.discardExact(6)
|
|
||||||
val allLength = readPacket.readInt()
|
|
||||||
val dataLength = readPacket.readInt()
|
|
||||||
val headLength = allLength - dataLength - 16
|
|
||||||
readPacket.discardExact(2)
|
|
||||||
ByteArray(headLength).also {
|
|
||||||
readPacket.readFully(it, 0, it.size)
|
|
||||||
}
|
|
||||||
val data = ByteArray(dataLength).also {
|
|
||||||
readPacket.readFully(it, 0, it.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
val resp = data.decodeProtobuf<WeiyunComm>().resp!!
|
|
||||||
return ok(ItemContent(
|
|
||||||
resp.getFavContentResp!!.content!!.joinToString("") {
|
|
||||||
String(it.richMedia!!.rawData!!)
|
|
||||||
}
|
|
||||||
), echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("id")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class ItemContent(
|
|
||||||
@SerialName("content") val content: String
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.io.core.ByteReadPacket
|
|
||||||
import kotlinx.io.core.discardExact
|
|
||||||
import kotlinx.serialization.SerialName
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.decodeFromByteArray
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.QFavSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.DeflateTools
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
import moe.fuqiuluo.symbols.decodeProtobuf
|
|
||||||
import protobuf.fav.WeiyunComm
|
|
||||||
|
|
||||||
@OneBotHandler("fav.get_item_list")
|
|
||||||
internal object FavGetItemList: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val category = session.getInt("category")
|
|
||||||
val startPos = session.getInt("start_pos")
|
|
||||||
val pageSize = session.getInt("page_size")
|
|
||||||
return invoke(category, startPos, pageSize, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(
|
|
||||||
category: Int,
|
|
||||||
startPos: Int,
|
|
||||||
pageSize: Int,
|
|
||||||
echo: JsonElement = EmptyJsonString
|
|
||||||
): String {
|
|
||||||
if (pageSize <= 1) {
|
|
||||||
return logic("page_size must be greater than 1", echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
val result = DeflateTools.ungzip(QFavSvc.getItemList(
|
|
||||||
category = category,
|
|
||||||
startPos = startPos,
|
|
||||||
pageSize = pageSize
|
|
||||||
).onSuccess {
|
|
||||||
if (it.mHttpCode != 200 || it.mResult != 0) {
|
|
||||||
return logic("fav.get_item_list failed", echo)
|
|
||||||
}
|
|
||||||
}.getOrThrow().mRespData)
|
|
||||||
val readPacket = ByteReadPacket(result)
|
|
||||||
readPacket.discardExact(6)
|
|
||||||
val allLength = readPacket.readInt()
|
|
||||||
val dataLength = readPacket.readInt()
|
|
||||||
val headLength = allLength - dataLength - 16
|
|
||||||
readPacket.discardExact(2)
|
|
||||||
ByteArray(headLength).also {
|
|
||||||
readPacket.readFully(it, 0, it.size)
|
|
||||||
}
|
|
||||||
val data = ByteArray(dataLength).also {
|
|
||||||
readPacket.readFully(it, 0, it.size)
|
|
||||||
}
|
|
||||||
val resp = data.decodeProtobuf<WeiyunComm>().resp!!.getFavListResp!!
|
|
||||||
|
|
||||||
val itemList = arrayListOf<Item>()
|
|
||||||
val rawItemList = resp.collections!!
|
|
||||||
rawItemList.forEach {
|
|
||||||
val itemId = it.cid
|
|
||||||
val author = it.author!!
|
|
||||||
val authorType = author.type.toInt()
|
|
||||||
val authorId = author.numId.toLong()
|
|
||||||
val authorName = author.strId
|
|
||||||
val groupName: String
|
|
||||||
val groupId: Long
|
|
||||||
if (authorType == 2) {
|
|
||||||
groupName = author.groupName
|
|
||||||
groupId = author.groupId.toLong()
|
|
||||||
} else {
|
|
||||||
groupName = ""
|
|
||||||
groupId = 0L
|
|
||||||
}
|
|
||||||
val clientVersion = it.srcAppVer
|
|
||||||
val time = it.createTime.toLong()
|
|
||||||
itemList.add(Item(
|
|
||||||
id = itemId,
|
|
||||||
authorType = authorType,
|
|
||||||
author = authorId,
|
|
||||||
authorName = authorName,
|
|
||||||
groupName = groupName,
|
|
||||||
groupId = groupId,
|
|
||||||
clientVersion = clientVersion,
|
|
||||||
time = time
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
return ok(ItemList(itemList), echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("category", "start_pos", "page_size")
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class ItemList(
|
|
||||||
val items: List<Item>
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class Item(
|
|
||||||
@SerialName("id") val id: String,
|
|
||||||
@SerialName("author_type") val authorType: Int,
|
|
||||||
@SerialName("author") val author: Long,
|
|
||||||
@SerialName("author_name") val authorName: String,
|
|
||||||
@SerialName("group_name") val groupName: String,
|
|
||||||
@SerialName("group_id") val groupId: Long,
|
|
||||||
@SerialName("client_version") val clientVersion: String,
|
|
||||||
@SerialName("time") val time: Long
|
|
||||||
)
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.Credentials
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("get_csrf_token")
|
|
||||||
internal object GetCSRF: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val domain = session.getStringOrNull("domain")
|
|
||||||
?: return invoke(session.echo)
|
|
||||||
return invoke(domain, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(domain: String, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
val uin = TicketSvc.getUin()
|
|
||||||
val pskey = TicketSvc.getPSKey(uin, domain)
|
|
||||||
?: return invoke(echo)
|
|
||||||
return ok(Credentials(bkn = TicketSvc.getCSRF(pskey)), echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun invoke(echo: JsonElement = EmptyJsonString): String {
|
|
||||||
val uin = TicketSvc.getUin()
|
|
||||||
val pskey = TicketSvc.getPSKey(uin)
|
|
||||||
return ok(Credentials(bkn = TicketSvc.getCSRF(pskey)), echo)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.Credentials
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("get_cookies", ["get_cookie"])
|
|
||||||
internal object GetCookies: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val domain = session.getStringOrNull("domain")
|
|
||||||
?: return invoke(session.echo)
|
|
||||||
return invoke(domain, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun invoke(echo: JsonElement = EmptyJsonString): String {
|
|
||||||
return ok(Credentials(
|
|
||||||
cookie = TicketSvc.getCookie(),
|
|
||||||
bigDataTicket = TicketSvc.getBigdataTicket()
|
|
||||||
), echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(domain: String, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
return ok(Credentials(
|
|
||||||
cookie = TicketSvc.getCookie(domain),
|
|
||||||
bigDataTicket = TicketSvc.getBigdataTicket()
|
|
||||||
), echo)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.Credentials
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("get_credentials")
|
|
||||||
internal object GetCredentials: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val domain = session.getStringOrNull("domain")
|
|
||||||
?: return invoke(session.echo)
|
|
||||||
return invoke(domain, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun invoke(echo: JsonElement = EmptyJsonString): String {
|
|
||||||
val uin = TicketSvc.getUin()
|
|
||||||
val skey = TicketSvc.getRealSkey(uin)
|
|
||||||
val pskey = TicketSvc.getPSKey(uin)
|
|
||||||
return ok(
|
|
||||||
Credentials(
|
|
||||||
bkn = TicketSvc.getCSRF(pskey),
|
|
||||||
cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey;"
|
|
||||||
), echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(domain: String, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
val uin = TicketSvc.getUin()
|
|
||||||
val skey = TicketSvc.getRealSkey(uin)
|
|
||||||
val pskey = TicketSvc.getPSKey(uin, domain) ?: ""
|
|
||||||
val pt4token = TicketSvc.getPt4Token(uin, domain) ?: ""
|
|
||||||
return ok(Credentials(
|
|
||||||
bkn = TicketSvc.getCSRF(pskey),
|
|
||||||
cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token;"
|
|
||||||
), echo)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("get_device_battery")
|
|
||||||
internal object GetDeviceBattery: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
return invoke(session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun invoke(echo: JsonElement = EmptyJsonString): String {
|
|
||||||
return ok(PlatformUtils.getDeviceBattery(), echo = echo)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("get_essence_msg_list", ["get_essence_message_list"])
|
|
||||||
internal object GetEssenceMessageList: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val groupId = session.getLong("group_id")
|
|
||||||
val page = session.getIntOrNull("page") ?: 0
|
|
||||||
val pageSize = session.getIntOrNull("page_size") ?: 20
|
|
||||||
return invoke(groupId, page, pageSize, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(groupId: Long, page: Int = 0, pageSize: Int = 20, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
if (page < 0 || pageSize > 50) {
|
|
||||||
return badParam("参数不正确:page_size不得大于50", echo)
|
|
||||||
}
|
|
||||||
val essenceMessageList = GroupSvc.getEssenceMessageList(groupId, page, pageSize)
|
|
||||||
if (essenceMessageList.isSuccess) {
|
|
||||||
return ok(essenceMessageList.getOrNull(), echo)
|
|
||||||
}
|
|
||||||
return logic(essenceMessageList.exceptionOrNull()?.message ?: "", echo)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.OutResourceByBase64
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.shamrock.utils.FileUtils
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
import java.io.ByteArrayOutputStream
|
|
||||||
import java.util.Base64
|
|
||||||
import java.util.zip.GZIPOutputStream
|
|
||||||
|
|
||||||
@OneBotHandler("get_file") internal object GetFile : IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val file = session.getString("file")
|
|
||||||
.replace(regex = "[{}\\-]".toRegex(), replacement = "")
|
|
||||||
.replace(" ", "")
|
|
||||||
.split(".")[0].lowercase()
|
|
||||||
val fileType = session.getStringOrNull("file_type") ?: "base64"
|
|
||||||
return invoke(file, fileType, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
operator fun invoke(file: String, fileType: String = "base64", echo: JsonElement = EmptyJsonString): String {
|
|
||||||
val targetFile = FileUtils.getFileByMd5(file)
|
|
||||||
return if (targetFile.exists()) {
|
|
||||||
when (fileType) {
|
|
||||||
"base64", "" -> ok(
|
|
||||||
OutResourceByBase64(
|
|
||||||
"/res/${targetFile.nameWithoutExtension}",
|
|
||||||
Base64.getEncoder()
|
|
||||||
.encodeToString(targetFile.readBytes()),
|
|
||||||
targetFile.nameWithoutExtension,
|
|
||||||
), echo
|
|
||||||
)
|
|
||||||
|
|
||||||
"gzip" -> ok(
|
|
||||||
OutResourceByBase64(
|
|
||||||
"/res/${targetFile.nameWithoutExtension}",
|
|
||||||
compressAndEncode(targetFile.readBytes()),
|
|
||||||
targetFile.nameWithoutExtension,
|
|
||||||
), echo
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> error("file_type error", echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
error("not found record file from md5", echo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun compressAndEncode(input: ByteArray): String {
|
|
||||||
// 压缩数据
|
|
||||||
val outputStream = ByteArrayOutputStream()
|
|
||||||
val gzip = GZIPOutputStream(outputStream)
|
|
||||||
gzip.write(input)
|
|
||||||
gzip.close()
|
|
||||||
val compressedBytes = outputStream.toByteArray()
|
|
||||||
|
|
||||||
// 编码为 Base64 字符串
|
|
||||||
return Base64.getEncoder()
|
|
||||||
.encodeToString(compressedBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("file")
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.GetForwardMsgResult
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("get_forward_msg", ["get_forward_message"])
|
|
||||||
internal object GetForwardMsg : IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val id = session.getString("id")
|
|
||||||
return invoke(id, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(
|
|
||||||
resId: String,
|
|
||||||
echo: JsonElement = EmptyJsonString
|
|
||||||
): String {
|
|
||||||
return ok(
|
|
||||||
data = GetForwardMsgResult(
|
|
||||||
msgs = MsgSvc.getForwardMsg(resId).getOrElse { return logic(it.toString(), echo = echo) }),
|
|
||||||
echo = echo
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override val requiredParams: Array<String> = arrayOf("id")
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
package moe.fuqiuluo.shamrock.remote.action.handlers
|
|
||||||
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
|
||||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.FriendEntry
|
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.PlatformType
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.FriendSvc
|
|
||||||
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
|
|
||||||
import moe.fuqiuluo.symbols.OneBotHandler
|
|
||||||
|
|
||||||
@OneBotHandler("get_friend_list")
|
|
||||||
internal object GetFriendList: IActionHandler() {
|
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
|
||||||
val refresh = session.getBooleanOrDefault("refresh", session.getBooleanOrDefault("no_cache", false))
|
|
||||||
return invoke(refresh, session.echo)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend operator fun invoke(refresh: Boolean, echo: JsonElement = EmptyJsonString): String {
|
|
||||||
val friendList = FriendSvc.getFriendList(refresh).onFailure {
|
|
||||||
return error(it.message ?: "unknown error", echo, arrayResult = true)
|
|
||||||
}.getOrThrow()
|
|
||||||
return ok(friendList.map { friend ->
|
|
||||||
FriendEntry(
|
|
||||||
id = friend.uin.toLong(),
|
|
||||||
name = friend.name,
|
|
||||||
displayName = friend.remark,
|
|
||||||
remark = friend.remark,
|
|
||||||
age = friend.age,
|
|
||||||
gender = friend.gender,
|
|
||||||
groupId = friend.groupid,
|
|
||||||
platformType = PlatformType.valueOf(friend.iTermType),
|
|
||||||
termType = friend.iTermType
|
|
||||||
)
|
|
||||||
}, echo)
|
|
||||||
}
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user