kritorをmasterブランチに設定する

kritorをmasterブランチに設定する
This commit is contained in:
fuqiuluo 2024-03-21 16:13:45 +08:00
parent 7782feb6ac
commit 680317da13
271 changed files with 295 additions and 25693 deletions

3
.gitmodules vendored Normal file
View File

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

View File

@ -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>
## 权限声明 ## 权限声明

View File

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

View 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,
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,6 @@ import android.content.ClipData
import android.content.ClipboardManager import android.content.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标准WebSocketShamrock作为Server。",
isSwitch = ShamrockConfig.isWs(ctx)
) {
ShamrockConfig.setWs(ctx, it)
return@Function true
}
Function(
title = "被动WebSocket",
desc = "OneBot标准WebSocketShamrock作为Client。",
isSwitch = ShamrockConfig.isWsClient(ctx)
) {
ShamrockConfig.setWsClient(ctx, it)
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
}*/
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,135 +0,0 @@
package moe.fuqiuluo.qqinterface.servlet
import VIP.GetCustomOnlineStatusReq
import VIP.GetCustomOnlineStatusRsp
import com.qq.jce.wup.UniPacket
import com.tencent.mobileqq.data.Card
import com.tencent.mobileqq.profilecard.api.IProfileDataService
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType.Application.Json
import io.ktor.http.contentType
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import mqq.app.Packet
import tencent.im.oidb.cmd0x11b2.oidb_0x11b2
import tencent.im.oidb.oidb_sso
import kotlin.coroutines.resume
internal object CardSvc: BaseSvc() {
private val GetModelShowLock by lazy {
Mutex()
}
private val refreshCardLock by lazy {
Mutex()
}
suspend fun getModelShow(uin: Long = app.longAccountUin): String {
return GetModelShowLock.withLock {
val uniPacket = UniPacket()
uniPacket.servantName = "VIP.CustomOnlineStatusServer.CustomOnlineStatusObj"
uniPacket.funcName = "GetCustomOnlineStatus"
val getCustomOnlineStatusReq = GetCustomOnlineStatusReq()
getCustomOnlineStatusReq.lUin = uin
getCustomOnlineStatusReq.sIMei = ""
uniPacket.put("req", getCustomOnlineStatusReq)
val resp = sendBufferAW("VipCustom.GetCustomOnlineStatus", false, uniPacket.encode())
?: error("unable to fetch contact model_show")
Packet.decodePacket(resp, "rsp", GetCustomOnlineStatusRsp()).sBuffer
}
}
suspend fun setModelShow(model: String, manu: String, modelShow: String, imei: String, show: Boolean) {
val cookie = TicketSvc.getCookie("vip.qq.com")
val csrf = TicketSvc.getCSRF(TicketSvc.getUin(), "vip.qq.com")
val p4token = TicketSvc.getPt4Token(TicketSvc.getUin(), "vip.qq.com") ?: ""
GlobalClient.post("https://club.vip.qq.com/srf-cgi-node?srfname=VIP.CustomOnlineStatusServer.CustomOnlineStatusObj.SetCustomOnlineStatus&ts=${System.currentTimeMillis()}&daid=18&g_tk=$csrf&pt4_token=$p4token") {
header("Cookie", cookie)
contentType(Json)
setBody(mapOf(
"servicesName" to "VIP.CustomOnlineStatusServer.CustomOnlineStatusObj",
"cmd" to "SetCustomOnlineStatus",
"args" to listOf(mapOf(
"sIMei" to imei,
"sModel" to model,
"sManu" to manu,
"lUin" to app.currentUin.toLong(),
"bShowInfo" to show,
"sModelShow" to modelShow
))
).json.toString())
}.bodyAsText().let {
LogCenter.log({ "setModelShow() => $it" }, Level.DEBUG)
}
}
suspend fun getSharePrivateArkMsg(peerId: Long): String {
val reqBody = oidb_0x11b2.BusinessCardV3Req()
reqBody.uin.set(peerId)
reqBody.jump_url.set("mqqapi://card/show_pslcard?src_type=internal&source=sharecard&version=1&uin=$peerId")
val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11ca_0", 4790, 0, reqBody.toByteArray())
?: error("unable to fetch contact ark_json_text")
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(buffer.slice(4))
val rsp = oidb_0x11b2.BusinessCardV3Rsp()
rsp.mergeFrom(body.bytes_bodybuffer.get().toByteArray())
return rsp.signed_ark_msg.get()
}
suspend fun getProfileCard(uin: Long): Result<Card> {
return getProfileCardFromCache(uin).onFailure {
return refreshAndGetProfileCard(uin)
}
}
fun getProfileCardFromCache(uin: Long): Result<Card> {
val profileDataService = app
.getRuntimeService(IProfileDataService::class.java, "all")
val card = profileDataService.getProfileCard(uin.toString(), true)
return if (card == null || card.strNick.isNullOrEmpty()) {
Result.failure(Exception("unable to fetch profile card"))
} else {
Result.success(card)
}
}
suspend fun refreshAndGetProfileCard(uin: Long): Result<Card> {
val dataService = app
.getRuntimeService(IProfileDataService::class.java, "all")
val card = refreshCardLock.withLock {
suspendCancellableCoroutine {
app.addObserver(object: ProfileCardObserver() {
override fun onGetProfileCard(success: Boolean, obj: Any) {
app.removeObserver(this)
if (!success || obj !is Card) {
it.resume(null)
} else {
dataService.saveProfileCard(obj)
it.resume(obj)
}
}
})
app.getRuntimeService(IProfileProtocolService::class.java, "all")
.requestProfileCard(app.currentUin, uin.toString(), 12, 0L, 0.toByte(), 0L, 0L, null, "", 0L, 10004, null, 0.toByte())
}
}
return if (card == null || card.strNick.isNullOrEmpty()) {
Result.failure(Exception("unable to fetch profile card"))
} else {
Result.success(card)
}
}
}

View File

@ -1,20 +0,0 @@
package moe.fuqiuluo.qqinterface.servlet
import kotlinx.serialization.encodeToByteArray
import protobuf.auto.toByteArray
import protobuf.oidb.cmd0x9082.Oidb0x9082
internal object ChatSvc: BaseSvc() {
fun setGroupMessageCommentFace(peer: Long, msgSeq: ULong, faceIndex: String, isSet: Boolean) {
val serviceId = if (isSet) 1 else 2
sendOidb("OidbSvcTrpcTcp.0x9082_$serviceId", 36994, serviceId, Oidb0x9082(
peer = peer.toULong(),
msgSeq = msgSeq,
faceIndex = faceIndex,
flag = 1u,
u1 = 0u,
u2 = 0u
).toByteArray())
}
}

View File

@ -1,248 +0,0 @@
package moe.fuqiuluo.qqinterface.servlet
import com.tencent.mobileqq.pb.ByteStringMicro
import moe.fuqiuluo.qqinterface.servlet.structures.*
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.oidb.cmd0x6d7.CreateFolderReq
import protobuf.oidb.cmd0x6d7.DeleteFolderReq
import protobuf.oidb.cmd0x6d7.MoveFolderReq
import protobuf.oidb.cmd0x6d7.Oidb0x6d7ReqBody
import protobuf.oidb.cmd0x6d7.Oidb0x6d7RespBody
import protobuf.oidb.cmd0x6d7.RenameFolderReq
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
import tencent.im.oidb.cmd0x6d8.oidb_0x6d8
import tencent.im.oidb.oidb_sso
import protobuf.group_file_common.FolderInfo as GroupFileCommonFolderInfo
import protobuf.auto.toByteArray
internal object FileSvc: BaseSvc() {
suspend fun createFileFolder(groupId: Long, folderName: String, parentFolderId: String = "/"): Result<GroupFileCommonFolderInfo> {
val data = Oidb0x6d7ReqBody(
createFolder = CreateFolderReq(
groupCode = groupId.toULong(),
appId = 3u,
parentFolderId = parentFolderId,
folderName = folderName
)
).toByteArray()
val resultBuffer = sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data)
?: return Result.failure(Exception("unable to fetch result"))
val oidbPkg = oidb_sso.OIDBSSOPkg()
oidbPkg.mergeFrom(resultBuffer.slice(4))
val rsp = oidbPkg.bytes_bodybuffer.get()
.toByteArray()
.decodeProtobuf<Oidb0x6d7RespBody>()
if (rsp.createFolder?.retCode != 0) {
return Result.failure(Exception("unable to create folder: ${rsp.createFolder?.retCode}"))
}
return Result.success(rsp.createFolder!!.folderInfo!!)
}
suspend fun deleteGroupFolder(groupId: Long, folderUid: String): Boolean {
val buffer = sendOidbAW("OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody(
deleteFolder = DeleteFolderReq(
groupCode = groupId.toULong(),
appId = 3u,
folderId = folderUid
)
).toByteArray()) ?: return false
val oidbPkg = oidb_sso.OIDBSSOPkg()
oidbPkg.mergeFrom(buffer.slice(4))
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
return rsp.deleteFolder?.retCode == 0
}
suspend fun moveGroupFolder(groupId: Long, folderUid: String, newParentFolderUid: String): Boolean {
val buffer = sendOidbAW("OidbSvc.0x6d7_2", 1751, 2, Oidb0x6d7ReqBody(
moveFolder = MoveFolderReq(
groupCode = groupId.toULong(),
appId = 3u,
folderId = folderUid,
parentFolderId = "/"
)
).toByteArray()) ?: return false
val oidbPkg = oidb_sso.OIDBSSOPkg()
oidbPkg.mergeFrom(buffer.slice(4))
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
return rsp.moveFolder?.retCode == 0
}
suspend fun renameFolder(groupId: Long, folderUid: String, name: String): Boolean {
val buffer = sendOidbAW("OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody(
renameFolder = RenameFolderReq(
groupCode = groupId.toULong(),
appId = 3u,
folderId = folderUid,
folderName = name
)
).toByteArray()) ?: return false
val oidbPkg = oidb_sso.OIDBSSOPkg()
oidbPkg.mergeFrom(buffer.slice(4))
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
return rsp.renameFolder?.retCode == 0
}
suspend fun deleteGroupFile(groupId: Long, bizId: Int, fileUid: String): Boolean {
val oidb0x6d6ReqBody = oidb_0x6d6.ReqBody().apply {
delete_file_req.set(oidb_0x6d6.DeleteFileReqBody().apply {
uint64_group_code.set(groupId)
uint32_app_id.set(3)
uint32_bus_id.set(bizId)
str_parent_folder_id.set("/")
str_file_id.set(fileUid)
})
}
val result = sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray())
?: return false
val oidbPkg = oidb_sso.OIDBSSOPkg()
oidbPkg.mergeFrom(result.slice(4))
val rsp = oidb_0x6d6.RspBody().apply {
mergeFrom(oidbPkg.bytes_bodybuffer.get().toByteArray())
}
return rsp.delete_file_rsp.int32_ret_code.get() == 0
}
suspend fun getGroupFileSystemInfo(groupId: Long): FileSystemInfo {
val rspGetFileCntBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 2, oidb_0x6d8.ReqBody().also {
it.group_file_cnt_req.set(oidb_0x6d8.GetFileCountReqBody().also {
it.uint64_group_code.set(groupId)
it.uint32_app_id.set(3)
it.uint32_bus_id.set(0)
})
}.toByteArray())
val fileCnt: Int
val limitCnt: Int
if (rspGetFileCntBuffer != null) {
oidb_0x6d8.RspBody().mergeFrom(oidb_sso.OIDBSSOPkg()
.mergeFrom(rspGetFileCntBuffer.slice(4))
.bytes_bodybuffer.get()
.toByteArray()
).group_file_cnt_rsp.apply {
fileCnt = uint32_all_file_count.get()
limitCnt = uint32_limit_count.get()
}
} else {
throw RuntimeException("获取群文件数量失败")
}
val rspGetFileSpaceBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 3, oidb_0x6d8.ReqBody().also {
it.group_space_req.set(oidb_0x6d8.GetSpaceReqBody().apply {
uint64_group_code.set(groupId)
uint32_app_id.set(3)
})
}.toByteArray())
val totalSpace: Long
val usedSpace: Long
if (rspGetFileSpaceBuffer != null) {
oidb_0x6d8.RspBody().mergeFrom(oidb_sso.OIDBSSOPkg()
.mergeFrom(rspGetFileSpaceBuffer.slice(4))
.bytes_bodybuffer.get()
.toByteArray()).group_space_rsp.apply {
totalSpace = uint64_total_space.get()
usedSpace = uint64_used_space.get()
}
} else {
throw RuntimeException("获取群文件空间失败")
}
return FileSystemInfo(
fileCnt, limitCnt, usedSpace, totalSpace
)
}
suspend fun getGroupRootFiles(groupId: Long): Result<GroupFileList> {
return getGroupFiles(groupId, "/")
}
suspend fun getGroupFileInfo(groupId: Long, fileId: String, busid: Int): FileUrl {
return FileUrl(RichProtoSvc.getGroupFileDownUrl(groupId, fileId, busid))
}
suspend fun getGroupFiles(groupId: Long, folderId: String): Result<GroupFileList> {
val fileSystemInfo = getGroupFileSystemInfo(groupId)
val rspGetFileListBuffer = sendOidbAW("OidbSvc.0x6d8_1", 1752, 1, oidb_0x6d8.ReqBody().also {
it.file_list_info_req.set(oidb_0x6d8.GetFileListReqBody().apply {
uint64_group_code.set(groupId)
uint32_app_id.set(3)
str_folder_id.set(folderId)
uint32_file_count.set(fileSystemInfo.fileCount)
uint32_all_file_count.set(0)
uint32_req_from.set(3)
uint32_sort_by.set(oidb_0x6d8.GetFileListReqBody.SORT_BY_TIMESTAMP)
uint32_filter_code.set(0)
uint64_uin.set(0)
uint32_start_index.set(0)
bytes_context.set(ByteStringMicro.copyFrom(EMPTY_BYTE_ARRAY))
uint32_show_onlinedoc_folder.set(0)
})
}.toByteArray(), timeout = 15_000L)
return kotlin.runCatching {
val files = arrayListOf<FileInfo>()
val dirs = arrayListOf<FolderInfo>()
if (rspGetFileListBuffer != null) {
val oidb = oidb_sso.OIDBSSOPkg().mergeFrom(rspGetFileListBuffer.slice(4).let {
if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it
})
oidb_0x6d8.RspBody().mergeFrom(oidb.bytes_bodybuffer.get().toByteArray())
.file_list_info_rsp.apply {
rpt_item_list.get().forEach { file ->
if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FILE) {
val fileInfo = file.file_info
files.add(FileInfo(
groupId = groupId,
fileId = fileInfo.str_file_id.get(),
fileName = fileInfo.str_file_name.get(),
fileSize = fileInfo.uint64_file_size.get(),
busid = fileInfo.uint32_bus_id.get(),
uploadTime = fileInfo.uint32_upload_time.get(),
deadTime = fileInfo.uint32_dead_time.get(),
modifyTime = fileInfo.uint32_modify_time.get(),
downloadTimes = fileInfo.uint32_download_times.get(),
uploadUin = fileInfo.uint64_uploader_uin.get(),
uploadNick = fileInfo.str_uploader_name.get(),
md5 = fileInfo.bytes_md5.get().toByteArray().toHexString(),
sha = fileInfo.bytes_sha.get().toByteArray().toHexString(),
// 根本没有
sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString(),
))
}
else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) {
val folderInfo = file.folder_info
dirs.add(FolderInfo(
groupId = groupId,
folderId = folderInfo.str_folder_id.get(),
folderName = folderInfo.str_folder_name.get(),
totalFileCount = folderInfo.uint32_total_file_count.get(),
createTime = folderInfo.uint32_create_time.get(),
creator = folderInfo.uint64_create_uin.get(),
creatorNick = folderInfo.str_creator_name.get()
))
} else {
LogCenter.log("未知文件类型: ${file.uint32_type.get()}", Level.WARN)
}
}
}
} else {
throw RuntimeException("获取群文件列表失败")
}
GroupFileList(files, dirs)
}.onFailure {
LogCenter.log(it.message + ", buffer: ${rspGetFileListBuffer.toHexString()}", Level.ERROR)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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&notice=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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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")
}

View File

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