mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 13:12:17 +08:00
Shamrock
: 完成消息推送
Signed-off-by: 白池 <whitechi73@outlook.com>
This commit is contained in:
parent
7bacea3288
commit
a95d8d85e8
@ -37,7 +37,6 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
|
||||
# List C/C++ source files with relative paths to this CMakeLists.txt.
|
||||
${SRC_DIR}
|
||||
md5.cpp
|
||||
cqcode.cpp
|
||||
silk.cpp
|
||||
message.cpp
|
||||
shamrock.cpp)
|
||||
|
@ -1,138 +0,0 @@
|
||||
#include <stdexcept>
|
||||
#include "cqcode.h"
|
||||
|
||||
inline void replace_string(std::string& str, const std::string& from, const std::string& to) {
|
||||
size_t startPos = 0;
|
||||
while ((startPos = str.find(from, startPos)) != std::string::npos) {
|
||||
str.replace(startPos, from.length(), to);
|
||||
startPos += to.length();
|
||||
}
|
||||
}
|
||||
|
||||
inline int utf8_next_len(const std::string& str, size_t offset)
|
||||
{
|
||||
uint8_t c = (uint8_t)str[offset];
|
||||
if (c >= 0xFC)
|
||||
return 6;
|
||||
else if (c >= 0xF8)
|
||||
return 5;
|
||||
else if (c >= 0xF0)
|
||||
return 4;
|
||||
else if (c >= 0xE0)
|
||||
return 3;
|
||||
else if (c >= 0xC0)
|
||||
return 2;
|
||||
else if (c > 0x00)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::string, std::string>>& dest) {
|
||||
std::string cache;
|
||||
bool is_start = false;
|
||||
std::string key_tmp;
|
||||
std::unordered_map<std::string, std::string> kv;
|
||||
for(size_t i = 0; i < code.size(); i++) {
|
||||
int utf8_char_len = utf8_next_len(code, i);
|
||||
if(utf8_char_len == 0) {
|
||||
continue;
|
||||
}
|
||||
std::string_view c(&code[i],utf8_char_len);
|
||||
if (c == "[") {
|
||||
if (is_start) {
|
||||
throw illegal_code();
|
||||
} else {
|
||||
if (!cache.empty()) {
|
||||
std::unordered_map<std::string, std::string> kv;
|
||||
replace_string(cache, "[", "[");
|
||||
replace_string(cache, "]", "]");
|
||||
replace_string(cache, "&", "&");
|
||||
kv.emplace("_type", "text");
|
||||
kv.emplace("text", cache);
|
||||
dest.push_back(kv);
|
||||
cache.clear();
|
||||
}
|
||||
std::string_view cq_flag(&code[i],4);
|
||||
if(cq_flag == "[CQ:"){
|
||||
is_start = true;
|
||||
i += 3;
|
||||
}else{
|
||||
cache += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (c == "=") {
|
||||
if (is_start) {
|
||||
if (cache.empty()) {
|
||||
throw illegal_code();
|
||||
} else {
|
||||
if (key_tmp.empty()) {
|
||||
key_tmp.append(cache);
|
||||
cache.clear();
|
||||
} else {
|
||||
cache += c;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cache += c;
|
||||
}
|
||||
}
|
||||
else if (c == ",") {
|
||||
if (is_start) {
|
||||
if (kv.count("_type") == 0 && !cache.empty()) {
|
||||
kv.emplace("_type", cache);
|
||||
cache.clear();
|
||||
} else {
|
||||
if (!key_tmp.empty()) {
|
||||
replace_string(cache, "[", "[");
|
||||
replace_string(cache, "]", "]");
|
||||
replace_string(cache, ",", ",");
|
||||
replace_string(cache, "&", "&");
|
||||
kv.emplace(key_tmp, cache);
|
||||
cache.clear();
|
||||
key_tmp.clear();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cache += c;
|
||||
}
|
||||
}
|
||||
else if (c == "]") {
|
||||
if (is_start) {
|
||||
if (!cache.empty()) {
|
||||
if (!key_tmp.empty()) {
|
||||
replace_string(cache, "[", "[");
|
||||
replace_string(cache, "]", "]");
|
||||
replace_string(cache, ",", ",");
|
||||
replace_string(cache, "&", "&");
|
||||
kv.emplace(key_tmp, cache);
|
||||
} else {
|
||||
kv.emplace("_type", cache);
|
||||
}
|
||||
dest.push_back(kv);
|
||||
kv.clear();
|
||||
key_tmp.clear();
|
||||
cache.clear();
|
||||
is_start = false;
|
||||
}
|
||||
} else {
|
||||
cache += c;
|
||||
}
|
||||
}
|
||||
else {
|
||||
cache += c;
|
||||
i += (utf8_char_len - 1);
|
||||
}
|
||||
}
|
||||
if (!cache.empty()) {
|
||||
std::unordered_map<std::string, std::string> kv;
|
||||
replace_string(cache, "[", "[");
|
||||
replace_string(cache, "]", "]");
|
||||
replace_string(cache, "&", "&");
|
||||
kv.emplace("_type", "text");
|
||||
kv.emplace("text", cache);
|
||||
dest.push_back(kv);
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
#include "jni.h"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
|
||||
struct Honor {
|
||||
int id;
|
||||
std::string name;
|
||||
std::string icon_url;
|
||||
int priority;
|
||||
};
|
||||
|
||||
int calc_honor_flag(int honor_id, char honor_flag);
|
||||
|
||||
jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor);
|
||||
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_moe_fuqiuluo_shamrock_remote_action_handlers_GetTroopHonor_nativeDecodeHonor(JNIEnv *env, jobject thiz,
|
||||
jstring user_id,
|
||||
jint honor_id,
|
||||
jbyte honor_flag) {
|
||||
static std::vector<Honor> honor_list = {
|
||||
Honor{1, "龙王", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150116_n4PxCiurbm.png", 1},
|
||||
Honor{2, "群聊之火", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190136_92JEGFKC5k.png", 3},
|
||||
Honor{3, "群聊炽焰", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190204_zgCTeSrMq1.png", 4},
|
||||
Honor{5, "冒尖小春笋", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150335_tUJCAtoKVP.png", 5},
|
||||
Honor{6, "快乐源泉", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150434_3tDmsJExCP.png", 7},
|
||||
Honor{7, "学术新星", "https://sola.gtimg.cn/aoi/sola/20200515140645_j0X6gbuHNP.png", 8},
|
||||
Honor{8, "顶尖学霸", "https://sola.gtimg.cn/aoi/sola/20200515140639_0CtWOpfVzK.png", 9},
|
||||
Honor{9, "至尊学神", "https://sola.gtimg.cn/aoi/sola/20200515140628_P8UEYBjMBT.png", 10},
|
||||
Honor{10, "一笔当先", "https://sola.gtimg.cn/aoi/sola/20200515140654_4r94tSCdaB.png", 11},
|
||||
Honor{11, "奋进小翠竹", "https://sola.gtimg.cn/aoi/sola/20200812151819_wbj6z2NGoB.png", 6},
|
||||
Honor{12, "氛围魔杖", "https://sola.gtimg.cn/aoi/sola/20200812151831_4ZJgQCaD1H.png", 2},
|
||||
Honor{13, "壕礼皇冠", "https://sola.gtimg.cn/aoi/sola/20200930154050_juZOAMg7pt.png", 12},
|
||||
};
|
||||
int flag = calc_honor_flag(honor_id, honor_flag);
|
||||
if ((honor_id != 1 && honor_id != 2 && honor_id != 3) || flag != 1) {
|
||||
auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) {
|
||||
return honor.id == honor_id;
|
||||
});
|
||||
return make_honor_object(env, user_id, honor);
|
||||
} else {
|
||||
auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) {
|
||||
return honor.id == honor_id;
|
||||
});
|
||||
std::string url = "https://static-res.qq.com/static-res/groupInteract/vas/a/" + std::to_string(honor_id) + "_1.png";
|
||||
honor = Honor{honor_id, honor.name, url, honor.priority};
|
||||
return make_honor_object(env, user_id, honor);
|
||||
}
|
||||
}
|
||||
|
||||
int calc_honor_flag(int honor_id, char honor_flag) {
|
||||
int flag;
|
||||
if (honor_flag == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (honor_id == 1) {
|
||||
flag = honor_flag;
|
||||
} else if (honor_id == 2 || honor_id == 3) {
|
||||
flag = honor_flag >> 2;
|
||||
} else if (honor_id != 4) {
|
||||
return 0;
|
||||
} else {
|
||||
flag = honor_flag >> 4;
|
||||
}
|
||||
return flag & 3;
|
||||
}
|
||||
|
||||
jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor) {
|
||||
jclass GroupMemberHonor = env->FindClass("moe/fuqiuluo/shamrock/remote/service/data/GroupMemberHonor");
|
||||
jmethodID GroupMemberHonor_init = env->GetMethodID(GroupMemberHonor, "<init>",
|
||||
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;)V");
|
||||
auto user_id_str = (jstring) user_id;
|
||||
jstring honor_desc = env->NewStringUTF(honor.name.c_str());
|
||||
jstring uin_name = env->NewStringUTF("");
|
||||
jstring honor_icon_url = env->NewStringUTF(honor.icon_url.c_str());
|
||||
jobject ret = env->NewObject(GroupMemberHonor, GroupMemberHonor_init, user_id_str, uin_name, honor_icon_url, 0, honor.id, honor_desc);
|
||||
|
||||
env->DeleteLocalRef(GroupMemberHonor);
|
||||
env->DeleteLocalRef(user_id_str);
|
||||
env->DeleteLocalRef(honor_desc);
|
||||
env->DeleteLocalRef(honor_icon_url);
|
||||
|
||||
return ret;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
#ifndef UNTITLED_CQCODE_H
|
||||
#define UNTITLED_CQCODE_H
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <exception>
|
||||
|
||||
class illegal_code: std::exception {
|
||||
public:
|
||||
[[nodiscard]] const char * what() const noexcept override {
|
||||
return "Error cq code.";
|
||||
}
|
||||
};
|
||||
|
||||
void decode_cqcode(const std::string& code, std::vector<std::unordered_map<std::string, std::string>>& dest);
|
||||
|
||||
void encode_cqcode(const std::vector<std::unordered_map<std::string, std::string>>& segment, std::string& dest);
|
||||
|
||||
#endif //UNTITLED_CQCODE_H
|
@ -1,5 +1,4 @@
|
||||
#include "jni.h"
|
||||
#include "cqcode.h"
|
||||
#include <random>
|
||||
|
||||
inline void replace_string(std::string& str, const std::string& from, const std::string& to) {
|
||||
@ -12,7 +11,7 @@ inline void replace_string(std::string& str, const std::string& from, const std:
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz,
|
||||
Java_qq_service_msg_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz,
|
||||
jint chat_type,
|
||||
jlong time) {
|
||||
static std::random_device rd;
|
||||
@ -32,123 +31,6 @@ Java_moe_fuqiuluo_shamrock_helper_MessageHelper_getChatType(JNIEnv *env, jobject
|
||||
return (int32_t) ((int64_t) msg_id & 0xffL);
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jobject JNICALL
|
||||
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeDecodeCQCode(JNIEnv *env, jobject thiz,
|
||||
jstring code) {
|
||||
jclass ArrayList = env->FindClass("java/util/ArrayList");
|
||||
jmethodID NewArrayList = env->GetMethodID(ArrayList, "<init>", "()V");
|
||||
jmethodID ArrayListAdd = env->GetMethodID(ArrayList, "add", "(Ljava/lang/Object;)Z");
|
||||
jobject arrayList = env->NewObject(ArrayList, NewArrayList);
|
||||
|
||||
const char* cCode = env->GetStringUTFChars(code, nullptr);
|
||||
std::string cppCode = cCode;
|
||||
std::vector<std::unordered_map<std::string, std::string>> dest;
|
||||
try {
|
||||
decode_cqcode(cppCode, dest);
|
||||
} catch (illegal_code& code) {
|
||||
return arrayList;
|
||||
}
|
||||
|
||||
jclass HashMap = env->FindClass("java/util/HashMap");
|
||||
jmethodID NewHashMap = env->GetMethodID(HashMap, "<init>", "()V");
|
||||
jclass String = env->FindClass("java/lang/String");
|
||||
jmethodID NewString = env->GetMethodID(String, "<init>", "([BLjava/lang/String;)V");
|
||||
jstring charset = env->NewStringUTF("UTF-8");
|
||||
jmethodID put = env->GetMethodID(HashMap, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
for (auto& map : dest) {
|
||||
jobject hashMap = env->NewObject(HashMap, NewHashMap);
|
||||
for (const auto& pair : map) {
|
||||
jbyteArray keyArray = env->NewByteArray((int) pair.first.size());
|
||||
jbyteArray valueArray = env->NewByteArray((int) pair.second.size());
|
||||
env->SetByteArrayRegion(keyArray, 0, (int) pair.first.size(), (jbyte*)pair.first.c_str());
|
||||
env->SetByteArrayRegion(valueArray, 0, (int) pair.second.size(), (jbyte*)pair.second.c_str());
|
||||
auto key = (jstring) env->NewObject(String, NewString, keyArray, charset);
|
||||
auto value = (jstring) env->NewObject(String, NewString, valueArray, charset);
|
||||
env->CallObjectMethod(hashMap, put, key, value);
|
||||
}
|
||||
env->CallBooleanMethod(arrayList, ArrayListAdd, hashMap);
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(ArrayList);
|
||||
env->DeleteLocalRef(HashMap);
|
||||
env->DeleteLocalRef(String);
|
||||
env->DeleteLocalRef(charset);
|
||||
env->ReleaseStringUTFChars(code, cCode);
|
||||
|
||||
return arrayList;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jstring JNICALL
|
||||
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeEncodeCQCode(JNIEnv *env, jobject thiz,
|
||||
jobject segment_list) {
|
||||
jclass List = env->FindClass("java/util/List");
|
||||
jmethodID ListSize = env->GetMethodID(List, "size", "()I");
|
||||
jmethodID ListGet = env->GetMethodID(List, "get", "(I)Ljava/lang/Object;");
|
||||
jclass Map = env->FindClass("java/util/Map");
|
||||
jmethodID MapGet = env->GetMethodID(Map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
|
||||
jmethodID entrySetMethod = env->GetMethodID(Map, "entrySet", "()Ljava/util/Set;");
|
||||
jclass setClass = env->FindClass("java/util/Set");
|
||||
jmethodID iteratorMethod = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;");
|
||||
jclass entryClass = env->FindClass("java/util/Map$Entry");
|
||||
jmethodID getKeyMethod = env->GetMethodID(entryClass, "getKey", "()Ljava/lang/Object;");
|
||||
jmethodID getValueMethod = env->GetMethodID(entryClass, "getValue", "()Ljava/lang/Object;");
|
||||
|
||||
std::string result;
|
||||
jint size = env->CallIntMethod(segment_list, ListSize);
|
||||
for (int i = 0; i < size; i++ ) {
|
||||
jobject segment = env->CallObjectMethod(segment_list, ListGet, i);
|
||||
jobject entrySet = env->CallObjectMethod(segment, entrySetMethod);
|
||||
jobject iterator = env->CallObjectMethod(entrySet, iteratorMethod);
|
||||
auto type = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("_type"));
|
||||
auto typeString = env->GetStringUTFChars(type, nullptr);
|
||||
if (strcmp(typeString, "text") == 0) {
|
||||
auto text = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("text"));
|
||||
auto textString = env->GetStringUTFChars(text, nullptr);
|
||||
std::string tmpValue = textString;
|
||||
replace_string(tmpValue, "&", "&");
|
||||
replace_string(tmpValue, "[", "[");
|
||||
replace_string(tmpValue, "]", "]");
|
||||
replace_string(tmpValue, ",", ",");
|
||||
result.append(tmpValue);
|
||||
env->ReleaseStringUTFChars(text, textString);
|
||||
} else {
|
||||
result.append("[CQ:");
|
||||
result.append(typeString);
|
||||
while (env->CallBooleanMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "hasNext", "()Z"))) {
|
||||
jobject entry = env->CallObjectMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "next", "()Ljava/lang/Object;"));
|
||||
auto key = (jstring) env->CallObjectMethod(entry, getKeyMethod);
|
||||
auto value = (jstring) env->CallObjectMethod(entry, getValueMethod);
|
||||
auto keyString = env->GetStringUTFChars(key, nullptr);
|
||||
auto valueString = env->GetStringUTFChars(value, nullptr);
|
||||
if (strcmp(keyString, "_type") != 0) {
|
||||
std::string tmpValue = valueString;
|
||||
replace_string(tmpValue, "&", "&");
|
||||
replace_string(tmpValue, "[", "[");
|
||||
replace_string(tmpValue, "]", "]");
|
||||
replace_string(tmpValue, ",", ",");
|
||||
result.append(",").append(keyString).append("=").append(tmpValue);
|
||||
}
|
||||
env->ReleaseStringUTFChars(key, keyString);
|
||||
env->ReleaseStringUTFChars(value, valueString);
|
||||
env->DeleteLocalRef(entry);
|
||||
env->DeleteLocalRef(key);
|
||||
env->DeleteLocalRef(value);
|
||||
}
|
||||
result.append("]");
|
||||
}
|
||||
env->ReleaseStringUTFChars(type, typeString);
|
||||
}
|
||||
|
||||
env->DeleteLocalRef(List);
|
||||
env->DeleteLocalRef(Map);
|
||||
env->DeleteLocalRef(setClass);
|
||||
env->DeleteLocalRef(entryClass);
|
||||
return env->NewStringUTF(result.c_str());
|
||||
}
|
||||
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz,
|
||||
|
@ -9,7 +9,7 @@ import moe.fuqiuluo.symbols.Protobuf
|
||||
|
||||
@Serializable
|
||||
data class NtV2RichMediaRsp(
|
||||
@ProtoNumber(1) val head: RspHead,
|
||||
@ProtoNumber(1) val head: RspHead?,
|
||||
@ProtoNumber(2) val upload: UploadRsp?,
|
||||
@ProtoNumber(3) val download: DownloadRsp?,
|
||||
@ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?,
|
||||
|
@ -18,6 +18,11 @@ public class MsgService {
|
||||
public void addMsgListener(IKernelMsgListener listener) {
|
||||
}
|
||||
|
||||
public void removeMsgListener(@NotNull IKernelMsgListener iKernelMsgListener) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
public String getRichMediaFilePathForGuild(@NotNull RichMediaFilePathInfo richMediaFilePathInfo) {
|
||||
return null;
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
|
||||
).toByteArray()
|
||||
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data)
|
||||
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||
if (!fromServiceMsg.isSuccess) {
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||
}
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
@ -57,7 +57,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
|
||||
folderId = request.folderId
|
||||
)
|
||||
).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||
if (!fromServiceMsg.isSuccess) {
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||
}
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
@ -82,7 +82,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
|
||||
}
|
||||
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray())
|
||||
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||
if (!fromServiceMsg.isSuccess) {
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||
}
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
@ -106,7 +106,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
|
||||
folderName = request.name
|
||||
)
|
||||
).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||
if (!fromServiceMsg.isSuccess) {
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||
}
|
||||
val oidbPkg = oidb_sso.OIDBSSOPkg()
|
||||
|
@ -1,18 +1,22 @@
|
||||
@file:OptIn(DelicateCoroutinesApi::class)
|
||||
|
||||
package moe.fuqiuluo.shamrock.internals
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||
import io.kritor.Scene
|
||||
import io.kritor.contact
|
||||
import io.kritor.event.MessageEvent
|
||||
import io.kritor.event.Scene
|
||||
import io.kritor.event.contact
|
||||
import io.kritor.event.messageEvent
|
||||
import io.kritor.sender
|
||||
import io.kritor.event.sender
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.FlowCollector
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.io.core.BytePacketBuilder
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.msg.toKritorMessages
|
||||
|
||||
internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
private val messageEventFlow by lazy {
|
||||
@ -51,6 +55,7 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
this.uid = record.senderUid
|
||||
this.nick = record.sendNickName
|
||||
}
|
||||
this.elements.addAll(elements.toKritorMessages(record))
|
||||
})
|
||||
return true
|
||||
}
|
||||
@ -59,9 +64,23 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
record: MsgRecord,
|
||||
elements: ArrayList<MsgElement>,
|
||||
): Boolean {
|
||||
val botUin = app.longAccountUin
|
||||
var nickName = record.sendNickName
|
||||
|
||||
transMessageEvent(record, messageEvent {
|
||||
this.time = record.msgTime.toInt()
|
||||
this.scene = Scene.FRIEND
|
||||
this.messageId = record.msgId
|
||||
this.messageSeq = record.msgSeq
|
||||
this.contact = contact {
|
||||
this.scene = scene
|
||||
this.peer = record.senderUin.toString()
|
||||
this.subPeer = record.senderUid
|
||||
}
|
||||
this.sender = sender {
|
||||
this.uin = record.senderUin
|
||||
this.uid = record.senderUid
|
||||
this.nick = record.sendNickName
|
||||
}
|
||||
this.elements.addAll(elements.toKritorMessages(record))
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
@ -71,9 +90,23 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
groupCode: Long,
|
||||
fromNick: String,
|
||||
): Boolean {
|
||||
val botUin = app.longAccountUin
|
||||
var nickName = record.sendNickName
|
||||
|
||||
transMessageEvent(record, messageEvent {
|
||||
this.time = record.msgTime.toInt()
|
||||
this.scene = Scene.FRIEND
|
||||
this.messageId = record.msgId
|
||||
this.messageSeq = record.msgSeq
|
||||
this.contact = contact {
|
||||
this.scene = scene
|
||||
this.peer = record.senderUin.toString()
|
||||
this.subPeer = groupCode.toString()
|
||||
}
|
||||
this.sender = sender {
|
||||
this.uin = record.senderUin
|
||||
this.uid = record.senderUid
|
||||
this.nick = record.sendNickName
|
||||
}
|
||||
this.elements.addAll(elements.toKritorMessages(record))
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
@ -81,9 +114,23 @@ internal object GlobalEventTransmitter: QQInterfaces() {
|
||||
record: MsgRecord,
|
||||
elements: ArrayList<MsgElement>,
|
||||
): Boolean {
|
||||
val botUin = app.longAccountUin
|
||||
var nickName = record.sendNickName
|
||||
|
||||
transMessageEvent(record, messageEvent {
|
||||
this.time = record.msgTime.toInt()
|
||||
this.scene = Scene.GUILD
|
||||
this.messageId = record.msgId
|
||||
this.messageSeq = record.msgSeq
|
||||
this.contact = contact {
|
||||
this.scene = scene
|
||||
this.peer = record.channelId.toString()
|
||||
this.subPeer = record.guildId
|
||||
}
|
||||
this.sender = sender {
|
||||
this.uin = record.senderUin
|
||||
this.uid = record.senderUid
|
||||
this.nick = record.sendNickName
|
||||
}
|
||||
this.elements.addAll(elements.toKritorMessages(record))
|
||||
})
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import android.content.pm.PackageManager
|
||||
import android.content.pm.VersionedPackage
|
||||
import android.os.Build
|
||||
import android.os.Looper
|
||||
import com.tencent.qphone.base.remote.ToServiceMsg
|
||||
import de.robv.android.xposed.XC_MethodReplacement
|
||||
import de.robv.android.xposed.XSharedPreferences
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
@ -21,6 +22,8 @@ import moe.fuqiuluo.shamrock.xposed.XposedEntry
|
||||
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
|
||||
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
|
||||
import moe.fuqiuluo.symbols.XposedHook
|
||||
import mqq.app.MobileQQ
|
||||
import qq.service.QQInterfaces
|
||||
|
||||
@XposedHook(priority = 0)
|
||||
class AntiDetection: IAction {
|
||||
@ -36,6 +39,17 @@ class AntiDetection: IAction {
|
||||
if (ShamrockConfig[AntiJvmTrace])
|
||||
antiTrace()
|
||||
antiMemoryWalking()
|
||||
antiO3Report()
|
||||
}
|
||||
|
||||
private fun antiO3Report() {
|
||||
QQInterfaces.app.javaClass.hookMethod("sendToService").before {
|
||||
val toServiceMsg = it.args[0] as ToServiceMsg?
|
||||
if (toServiceMsg != null && toServiceMsg.serviceCmd.startsWith("trpc.o3")) {
|
||||
LogCenter.log("拦截trpc.o3环境上报包", Level.WARN)
|
||||
it.result = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun antiGetPackageGidsDetection(ctx: Context) {
|
||||
|
190
xposed/src/main/java/qq/service/bdh/FileTransfer.kt
Normal file
190
xposed/src/main/java/qq/service/bdh/FileTransfer.kt
Normal file
@ -0,0 +1,190 @@
|
||||
@file:OptIn(DelicateCoroutinesApi::class)
|
||||
|
||||
package qq.service.bdh
|
||||
|
||||
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 mqq.app.AppRuntime
|
||||
import qq.service.QQInterfaces
|
||||
import java.io.File
|
||||
import java.lang.Math.abs
|
||||
import kotlin.coroutines.resume
|
||||
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 = QQInterfaces.app
|
||||
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 = QQInterfaces.app
|
||||
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 kotlin.math.abs(Random.nextInt()).toLong()
|
||||
return uniseq
|
||||
}
|
||||
}
|
||||
}
|
495
xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt
Normal file
495
xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt
Normal file
@ -0,0 +1,495 @@
|
||||
package qq.service.bdh
|
||||
|
||||
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.FileTransNotifyInfo
|
||||
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 com.tencent.qqnt.msg.api.IMsgService
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import moe.fuqiuluo.shamrock.config.ResourceGroup
|
||||
import moe.fuqiuluo.shamrock.config.ShamrockConfig
|
||||
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
||||
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||
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.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 qq.service.QQInterfaces
|
||||
import qq.service.internals.NTServiceFetcher
|
||||
import qq.service.internals.msgService
|
||||
import qq.service.kernel.SimpleKernelMsgListener
|
||||
import qq.service.msg.MessageHelper
|
||||
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
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
internal object NtV2RichMediaSvc: QQInterfaces() {
|
||||
private val requestIdSeq = atomic(1L)
|
||||
|
||||
private fun fetchGroupResUploadTo(): String {
|
||||
return ShamrockConfig[ResourceGroup].ifNullOrEmpty { "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 = ArrayList(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
|
||||
}
|
||||
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, app.currentAccountUin)
|
||||
else -> Contact(chatType, fetchGroupResUploadTo(), null)
|
||||
}
|
||||
val result = mutableListOf<CommonFileInfo>()
|
||||
val msgService = NTServiceFetcher.kernelService.msgService
|
||||
?: return Result.failure(Exception("kernelService.msgService is null"))
|
||||
withTimeoutOrNull(timeout) {
|
||||
val uniseq = MessageHelper.generateMsgId(chatType)
|
||||
suspendCancellableCoroutine {
|
||||
val listener = object: SimpleKernelMsgListener() {
|
||||
override fun onRichMediaUploadComplete(fileTransNotifyInfo: FileTransNotifyInfo) {
|
||||
if (fileTransNotifyInfo.msgId == uniseq) {
|
||||
result.add(fileTransNotifyInfo.commonFileInfo)
|
||||
}
|
||||
if (result.size == resources.size) {
|
||||
msgService.removeMsgListener(this)
|
||||
it.resume(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
msgService.addMsgListener(listener)
|
||||
|
||||
QRoute.api(IMsgService::class.java).sendMsg(contact, uniseq, messages) { _, _ ->
|
||||
if (contact.chatType == MsgConstant.KCHATTYPEGROUP && contact.peerUid == "100000000") {
|
||||
val kernelService = NTServiceFetcher.kernelService
|
||||
val sessionService = kernelService.wrapperSession
|
||||
val service = sessionService.msgService
|
||||
service.deleteMsg(contact, arrayListOf(uniseq), null)
|
||||
}
|
||||
}
|
||||
|
||||
it.invokeOnCancellation {
|
||||
msgService.removeMsgListener(listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11c5_200", 4549, 200, req, true)
|
||||
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||
return Result.failure(Exception("unable to get multimedia pic info: ${fromServiceMsg?.wupBuffer}"))
|
||||
}
|
||||
fromServiceMsg.wupBuffer.slice(4).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,
|
||||
sceneBuilder: suspend SceneInfo.() -> Unit
|
||||
): Result<UploadRsp> {
|
||||
return runCatching {
|
||||
requestUploadNtPic(file, md5, sha, name, width, height, sceneBuilder).getOrThrow()
|
||||
}.onFailure {
|
||||
if (retryCnt > 0) {
|
||||
return requestUploadNtPic(file, md5, sha, name, width, height, retryCnt - 1, sceneBuilder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun requestUploadNtPic(
|
||||
file: File,
|
||||
md5: String,
|
||||
sha: String,
|
||||
name: String,
|
||||
width: UInt,
|
||||
height: UInt,
|
||||
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 = 1u,
|
||||
clientSeq = Random.nextUInt(),
|
||||
noNeedCompatMsg = false
|
||||
)
|
||||
).toByteArray()
|
||||
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true, timeout = 3.seconds)
|
||||
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||
return Result.failure(Exception("unable to request upload nt pic"))
|
||||
}
|
||||
val rspBuffer = fromServiceMsg.wupBuffer.slice(4).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 fromServiceMsg = sendBufferAW("ImgStore.GroupPicUp", true, Cmd0x388ReqBody(
|
||||
netType = 3,
|
||||
subCmd = 1,
|
||||
msgTryUpImg = arrayListOf(
|
||||
TryUpImgReq(
|
||||
groupCode = groupId.toLong(),
|
||||
srcUin = app.longAccountUin,
|
||||
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())
|
||||
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||
return Result.failure(Exception("unable to request upload group pic"))
|
||||
}
|
||||
val rsp = fromServiceMsg.wupBuffer.slice(4).decodeProtobuf<Cmd0x388RspBody>()
|
||||
.msgTryUpImgRsp!!.first()
|
||||
TryUpPicData(
|
||||
uKey = rsp.ukey,
|
||||
exist = rsp.fileExist,
|
||||
fileId = rsp.fileId.toULong(),
|
||||
upIp = rsp.upIp,
|
||||
upPort = rsp.upPort
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
429
xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt
Normal file
429
xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt
Normal file
@ -0,0 +1,429 @@
|
||||
@file:OptIn(ExperimentalSerializationApi::class)
|
||||
package qq.service.bdh
|
||||
|
||||
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.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.tools.slice
|
||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||
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 qq.service.QQInterfaces
|
||||
import qq.service.contact.ContactHelper
|
||||
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: QQInterfaces() {
|
||||
suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String {
|
||||
val fromServiceMsg = 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())
|
||||
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||
return ""
|
||||
}
|
||||
val body = oidb_sso.OIDBSSOPkg()
|
||||
body.mergeFrom(fromServiceMsg.wupBuffer.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 fromServiceMsg = 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())
|
||||
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||
return ""
|
||||
}
|
||||
val body = oidb_sso.OIDBSSOPkg()
|
||||
body.mergeFrom(fromServiceMsg.wupBuffer.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 fromServiceMsg = 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 (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
|
||||
if (retryCnt < 5) {
|
||||
return getC2CFileDownUrl(fileId, subId, retryCnt + 1)
|
||||
}
|
||||
return ""
|
||||
} else {
|
||||
val body = oidb_sso.OIDBSSOPkg()
|
||||
body.mergeFrom(fromServiceMsg.wupBuffer.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=")) {
|
||||
NtV2RichMediaSvc.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()) NtV2RichMediaSvc.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=")) {
|
||||
NtV2RichMediaSvc.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 richProtoReq = RichProto.RichProtoReq()
|
||||
val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq()
|
||||
downReq.selfUin = app.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 = app.getRuntimeService(IProtoReqManager::class.java, "all")
|
||||
RichProtoProc.procRichProtoReq(richProtoReq)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGroupVideoDownUrl(
|
||||
peerId: String,
|
||||
md5: ByteArray,
|
||||
fileUUId: String
|
||||
): String {
|
||||
return suspendCancellableCoroutine {
|
||||
val richProtoReq = RichProto.RichProtoReq()
|
||||
val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq()
|
||||
downReq.selfUin = app.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 = app.getRuntimeService(IProtoReqManager::class.java, "all")
|
||||
RichProtoProc.procRichProtoReq(richProtoReq)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getC2CPttDownUrl(
|
||||
peerId: String,
|
||||
fileUUId: String
|
||||
): String {
|
||||
return suspendCancellableCoroutine {
|
||||
val richProtoReq = RichProto.RichProtoReq()
|
||||
val pttDownReq: RichProto.RichProtoReq.C2CPttDownReq = RichProto.RichProtoReq.C2CPttDownReq()
|
||||
pttDownReq.selfUin = app.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 = app.getRuntimeService(IProtoReqManager::class.java, "all")
|
||||
RichProtoProc.procRichProtoReq(richProtoReq)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getGroupPttDownUrl(
|
||||
peerId: String,
|
||||
md5: ByteArray,
|
||||
groupFileKey: String
|
||||
): String {
|
||||
return suspendCancellableCoroutine {
|
||||
val richProtoReq = RichProto.RichProtoReq()
|
||||
val groupPttDownReq: RichProto.RichProtoReq.GroupPttDownReq = RichProto.RichProtoReq.GroupPttDownReq()
|
||||
groupPttDownReq.selfUin = app.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 = app.getRuntimeService(IProtoReqManager::class.java, "all")
|
||||
RichProtoProc.procRichProtoReq(richProtoReq)
|
||||
}
|
||||
}
|
||||
}
|
13
xposed/src/main/java/qq/service/bdh/TryUpPicData.kt
Normal file
13
xposed/src/main/java/qq/service/bdh/TryUpPicData.kt
Normal file
@ -0,0 +1,13 @@
|
||||
package qq.service.bdh
|
||||
|
||||
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,
|
||||
)
|
@ -33,7 +33,7 @@ internal object GroupFileHelper: QQInterfaces() {
|
||||
it.uint32_bus_id.set(0)
|
||||
})
|
||||
}.toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||
if (!fromServiceMsg.isSuccess) {
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||
}
|
||||
val fileCnt: Int
|
||||
@ -104,7 +104,7 @@ internal object GroupFileHelper: QQInterfaces() {
|
||||
uint32_show_onlinedoc_folder.set(0)
|
||||
})
|
||||
}.toByteArray(), timeout = 15.seconds) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
|
||||
if (!fromServiceMsg.isSuccess) {
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
|
||||
}
|
||||
val files = arrayListOf<File>()
|
||||
|
@ -269,7 +269,7 @@ internal object GroupHelper: QQInterfaces() {
|
||||
uint32_shutup_timestap.set(0)
|
||||
})
|
||||
}.toByteArray()) ?: return Result.failure(RuntimeException("[oidb] timeout"))
|
||||
if (!fromServiceMsg.isSuccess) {
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
return Result.failure(RuntimeException("[oidb] failed"))
|
||||
}
|
||||
val body = oidb_sso.OIDBSSOPkg()
|
||||
@ -291,7 +291,7 @@ internal object GroupHelper: QQInterfaces() {
|
||||
uint64_uin.set(app.longAccountUin)
|
||||
uint64_group_code.set(groupId)
|
||||
}.toByteArray(), trpc = true) ?: return Result.failure(RuntimeException("[oidb] timeout"))
|
||||
if (!fromServiceMsg.isSuccess) {
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
return Result.failure(RuntimeException("[oidb] failed"))
|
||||
}
|
||||
val body = oidb_sso.OIDBSSOPkg()
|
||||
@ -311,7 +311,7 @@ internal object GroupHelper: QQInterfaces() {
|
||||
toServiceMsg.extraData.putBoolean("is_admin", false)
|
||||
toServiceMsg.extraData.putInt("from", 0)
|
||||
val fromServiceMsg = sendToServiceMsgAW(toServiceMsg) ?: return@timeout Result.failure(Exception("获取群信息超时"))
|
||||
if (!fromServiceMsg.isSuccess) {
|
||||
if (fromServiceMsg.wupBuffer == null) {
|
||||
return@timeout Result.failure(Exception("获取群信息失败"))
|
||||
}
|
||||
val uniPacket = UniPacket(true)
|
||||
@ -393,7 +393,7 @@ internal object GroupHelper: QQInterfaces() {
|
||||
req.uint32_client_type.set(1)
|
||||
req.uint32_rich_card_name_ver.set(1)
|
||||
val fromServiceMsg = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray())
|
||||
if (fromServiceMsg != null && fromServiceMsg.isSuccess) {
|
||||
if (fromServiceMsg != null && fromServiceMsg.wupBuffer != null) {
|
||||
val rsp = group_member_info.RspBody()
|
||||
rsp.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
|
||||
if (rsp.msg_meminfo.str_location.has()) {
|
||||
|
@ -1,49 +1,21 @@
|
||||
@file:OptIn(DelicateCoroutinesApi::class)
|
||||
|
||||
package qq.service.internals
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.BroadcastHelperTransNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.ContactMsgBoxInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.CustomWithdrawConfig
|
||||
import com.tencent.qqnt.kernel.nativeinterface.DevInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.DownloadRelateEmojiResultInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.EmojiNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.EmojiResourceInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.FirstViewDirectMsgNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.FirstViewGroupGuildInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.FreqLimitInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GroupFileListResult
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GroupGuildNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GroupItem
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GuildInteractiveNotificationItem
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GuildMsgAbFlag
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GuildNotificationAbstractInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.HitRelatedEmojiWordsResult
|
||||
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener
|
||||
import com.tencent.qqnt.kernel.nativeinterface.ImportOldDbMsgNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.InputStatusInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.KickedInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgAbstract
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgSetting
|
||||
import com.tencent.qqnt.kernel.nativeinterface.RecvdOrder
|
||||
import com.tencent.qqnt.kernel.nativeinterface.RelatedWordEmojiInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.SearchGroupFileResult
|
||||
import com.tencent.qqnt.kernel.nativeinterface.TabStatusInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.UnreadCntInfo
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
|
||||
import qq.service.kernel.SimpleKernelMsgListener
|
||||
import qq.service.msg.MessageHelper
|
||||
|
||||
object AioListener: IKernelMsgListener {
|
||||
override fun onRecvMsg(msgs: ArrayList<MsgRecord>) {
|
||||
msgs.forEach {
|
||||
object AioListener: SimpleKernelMsgListener() {
|
||||
override fun onRecvMsg(records: ArrayList<MsgRecord>) {
|
||||
records.forEach {
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
onMsg(it)
|
||||
@ -103,275 +75,4 @@ object AioListener: IKernelMsgListener {
|
||||
else -> LogCenter.log("不支持PUSH事件: ${record.chatType}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMsgRecall(chatType: Int, peerId: String, msgId: Long) {
|
||||
LogCenter.log("onMsgRecall($chatType, $peerId, $msgId)")
|
||||
}
|
||||
|
||||
override fun onAddSendMsg(record: MsgRecord) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgInfoListUpdate(msgList: ArrayList<MsgRecord>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onTempChatInfoUpdate(tempChatInfo: TempChatInfo) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgAbstractUpdate(arrayList: ArrayList<MsgAbstract>?) {
|
||||
//arrayList?.forEach {
|
||||
// LogCenter.log("onMsgAbstractUpdate($it)", Level.WARN)
|
||||
//}
|
||||
}
|
||||
|
||||
override fun onRecvMsgSvrRspTransInfo(
|
||||
j2: Long,
|
||||
contact: Contact?,
|
||||
i2: Int,
|
||||
i3: Int,
|
||||
str: String?,
|
||||
bArr: ByteArray?
|
||||
) {
|
||||
LogCenter.log("onRecvMsgSvrRspTransInfo($j2, $contact, $i2, $i3, $str)", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onRecvS2CMsg(arrayList: ArrayList<Byte>?) {
|
||||
LogCenter.log("onRecvS2CMsg(${arrayList.toString()})", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onRecvSysMsg(arrayList: ArrayList<Byte>?) {
|
||||
LogCenter.log("onRecvSysMsg(${arrayList.toString()})", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {}
|
||||
|
||||
override fun onBroadcastHelperProgerssUpdate(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {}
|
||||
|
||||
override fun onChannelFreqLimitInfoUpdate(
|
||||
contact: Contact?,
|
||||
z: Boolean,
|
||||
freqLimitInfo: FreqLimitInfo?
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
override fun onContactUnreadCntUpdate(unreadMap: HashMap<Int, HashMap<String, UnreadCntInfo>>) {
|
||||
// 推送未读消息数量
|
||||
}
|
||||
|
||||
override fun onCustomWithdrawConfigUpdate(customWithdrawConfig: CustomWithdrawConfig?) {
|
||||
LogCenter.log("onCustomWithdrawConfigUpdate: " + customWithdrawConfig.toString(), Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onDraftUpdate(contact: Contact?, arrayList: ArrayList<MsgElement>?, j2: Long) {
|
||||
LogCenter.log("onDraftUpdate: " + contact.toString() + "|" + arrayList + "|" + j2.toString(), Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onEmojiDownloadComplete(emojiNotifyInfo: EmojiNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onEmojiResourceUpdate(emojiResourceInfo: EmojiResourceInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onFileMsgCome(arrayList: ArrayList<MsgRecord>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onFirstViewGroupGuildMapping(arrayList: ArrayList<FirstViewGroupGuildInfo>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGrabPasswordRedBag(
|
||||
i2: Int,
|
||||
str: String?,
|
||||
i3: Int,
|
||||
recvdOrder: RecvdOrder?,
|
||||
msgRecord: MsgRecord?
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
override fun onKickedOffLine(kickedInfo: KickedInfo?) {
|
||||
LogCenter.log("onKickedOffLine($kickedInfo)")
|
||||
}
|
||||
|
||||
override fun onRichMediaUploadComplete(notifyInfo: FileTransNotifyInfo) {
|
||||
LogCenter.log({ "[BDH] 资源上传完成(${notifyInfo.trasferStatus}, ${notifyInfo.fileId}, ${notifyInfo.msgId}, ${notifyInfo.commonFileInfo})" }, Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onRecvOnlineFileMsg(arrayList: ArrayList<MsgRecord>?) {
|
||||
LogCenter.log(("onRecvOnlineFileMsg" + arrayList?.joinToString { ", " }), Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onRichMediaDownloadComplete(fileTransNotifyInfo: FileTransNotifyInfo) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRichMediaProgerssUpdate(fileTransNotifyInfo: FileTransNotifyInfo) {
|
||||
|
||||
}
|
||||
|
||||
override fun onSearchGroupFileInfoUpdate(searchGroupFileResult: SearchGroupFileResult?) {
|
||||
LogCenter.log("onSearchGroupFileInfoUpdate($searchGroupFileResult)", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onGroupFileInfoAdd(groupItem: GroupItem?) {
|
||||
LogCenter.log("onGroupFileInfoAdd: " + groupItem.toString(), Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onGroupFileInfoUpdate(groupFileListResult: GroupFileListResult?) {
|
||||
LogCenter.log("onGroupFileInfoUpdate: " + groupFileListResult.toString(), Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onGroupGuildUpdate(groupGuildNotifyInfo: GroupGuildNotifyInfo?) {
|
||||
LogCenter.log("onGroupGuildUpdate: " + groupGuildNotifyInfo.toString(), Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onGroupTransferInfoAdd(groupItem: GroupItem?) {
|
||||
LogCenter.log("onGroupTransferInfoAdd: " + groupItem.toString(), Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onGroupTransferInfoUpdate(groupFileListResult: GroupFileListResult?) {
|
||||
LogCenter.log("onGroupTransferInfoUpdate: " + groupFileListResult.toString(), Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onGuildInteractiveUpdate(guildInteractiveNotificationItem: GuildInteractiveNotificationItem?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: DownloadRelateEmojiResultInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: HitRelatedEmojiWordsResult?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onHitRelatedEmojiResult(relatedWordEmojiInfo: RelatedWordEmojiInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: ImportOldDbMsgNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onInputStatusPush(inputStatusInfo: InputStatusInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onLineDev(devList: ArrayList<DevInfo>?) {
|
||||
//LogCenter.log("onLineDev($arrayList)")
|
||||
}
|
||||
|
||||
override fun onLogLevelChanged(newLevel: Long) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgBoxChanged(arrayList: ArrayList<ContactMsgBoxInfo>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgDelete(contact: Contact?, arrayList: ArrayList<Long>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgEventListUpdate(hashMap: HashMap<String, ArrayList<Long>>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgInfoListAdd(arrayList: ArrayList<MsgRecord>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgQRCodeStatusChanged(i2: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgSecurityNotify(msgRecord: MsgRecord?) {
|
||||
LogCenter.log("onMsgSecurityNotify($msgRecord)")
|
||||
}
|
||||
|
||||
override fun onMsgSettingUpdate(msgSetting: MsgSetting?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onNtFirstViewMsgSyncEnd() {
|
||||
|
||||
}
|
||||
|
||||
override fun onNtMsgSyncEnd() {
|
||||
LogCenter.log("NTKernel同步消息完成", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onNtMsgSyncStart() {
|
||||
LogCenter.log("NTKernel同步消息开始", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRecvGroupGuildFlag(i2: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRecvUDCFlag(i2: Int) {
|
||||
LogCenter.log("onRecvUDCFlag($i2)", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onSendMsgError(j2: Long, contact: Contact?, i2: Int, str: String?) {
|
||||
LogCenter.log("onSendMsgError($j2, $contact, $j2, $str)", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onSysMsgNotification(i2: Int, j2: Long, j3: Long, arrayList: ArrayList<Byte>?) {
|
||||
LogCenter.log("onSysMsgNotification($i2, $j2, $j3, $arrayList)", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onUnreadCntAfterFirstView(hashMap: HashMap<Int, ArrayList<UnreadCntInfo>>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onUnreadCntUpdate(hashMap: HashMap<Int, ArrayList<UnreadCntInfo>>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onUserChannelTabStatusChanged(z: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun onUserOnlineStatusChanged(z: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun onUserTabStatusChanged(arrayList: ArrayList<TabStatusInfo>?) {
|
||||
LogCenter.log("onUserTabStatusChanged($arrayList)", Level.DEBUG)
|
||||
}
|
||||
|
||||
override fun onlineStatusBigIconDownloadPush(i2: Int, j2: Long, str: String?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onlineStatusSmallIconDownloadPush(i2: Int, j2: Long, str: String?) {
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,316 @@
|
||||
package qq.service.kernel
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.BroadcastHelperTransNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.ContactMsgBoxInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.CustomWithdrawConfig
|
||||
import com.tencent.qqnt.kernel.nativeinterface.DevInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.DownloadRelateEmojiResultInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.EmojiNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.EmojiResourceInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.FirstViewDirectMsgNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.FirstViewGroupGuildInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.FreqLimitInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GroupFileListResult
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GroupGuildNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GroupItem
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GuildInteractiveNotificationItem
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GuildMsgAbFlag
|
||||
import com.tencent.qqnt.kernel.nativeinterface.GuildNotificationAbstractInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.HitRelatedEmojiWordsResult
|
||||
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener
|
||||
import com.tencent.qqnt.kernel.nativeinterface.ImportOldDbMsgNotifyInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.InputStatusInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.KickedInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgAbstract
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgSetting
|
||||
import com.tencent.qqnt.kernel.nativeinterface.RecvdOrder
|
||||
import com.tencent.qqnt.kernel.nativeinterface.RelatedWordEmojiInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.SearchGroupFileResult
|
||||
import com.tencent.qqnt.kernel.nativeinterface.TabStatusInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
|
||||
import com.tencent.qqnt.kernel.nativeinterface.UnreadCntInfo
|
||||
import java.util.ArrayList
|
||||
import java.util.HashMap
|
||||
|
||||
abstract class SimpleKernelMsgListener: IKernelMsgListener {
|
||||
override fun onAddSendMsg(msgRecord: MsgRecord?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onBroadcastHelperProgerssUpdate(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onChannelFreqLimitInfoUpdate(
|
||||
contact: Contact?,
|
||||
z: Boolean,
|
||||
freqLimitInfo: FreqLimitInfo?
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
override fun onContactUnreadCntUpdate(hashMap: HashMap<Int, HashMap<String, UnreadCntInfo>>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onCustomWithdrawConfigUpdate(customWithdrawConfig: CustomWithdrawConfig?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onDraftUpdate(contact: Contact?, arrayList: ArrayList<MsgElement>?, j2: Long) {
|
||||
|
||||
}
|
||||
|
||||
override fun onEmojiDownloadComplete(emojiNotifyInfo: EmojiNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onEmojiResourceUpdate(emojiResourceInfo: EmojiResourceInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onFileMsgCome(arrayList: ArrayList<MsgRecord>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onFirstViewGroupGuildMapping(arrayList: ArrayList<FirstViewGroupGuildInfo>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGrabPasswordRedBag(
|
||||
i2: Int,
|
||||
str: String?,
|
||||
i3: Int,
|
||||
recvdOrder: RecvdOrder?,
|
||||
msgRecord: MsgRecord?
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGroupFileInfoAdd(groupItem: GroupItem?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGroupFileInfoUpdate(groupFileListResult: GroupFileListResult?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGroupGuildUpdate(groupGuildNotifyInfo: GroupGuildNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGroupTransferInfoAdd(groupItem: GroupItem?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGroupTransferInfoUpdate(groupFileListResult: GroupFileListResult?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGuildInteractiveUpdate(guildInteractiveNotificationItem: GuildInteractiveNotificationItem?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: DownloadRelateEmojiResultInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: HitRelatedEmojiWordsResult?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onHitRelatedEmojiResult(relatedWordEmojiInfo: RelatedWordEmojiInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: ImportOldDbMsgNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onInputStatusPush(inputStatusInfo: InputStatusInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onKickedOffLine(kickedInfo: KickedInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onLineDev(arrayList: ArrayList<DevInfo>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onLogLevelChanged(j2: Long) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgAbstractUpdate(arrayList: ArrayList<MsgAbstract>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgBoxChanged(arrayList: ArrayList<ContactMsgBoxInfo>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgDelete(contact: Contact?, arrayList: ArrayList<Long>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgEventListUpdate(hashMap: HashMap<String, ArrayList<Long>>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgInfoListAdd(arrayList: ArrayList<MsgRecord>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgInfoListUpdate(arrayList: ArrayList<MsgRecord>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgQRCodeStatusChanged(i2: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgRecall(i2: Int, str: String?, j2: Long) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgSecurityNotify(msgRecord: MsgRecord?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onMsgSettingUpdate(msgSetting: MsgSetting?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onNtFirstViewMsgSyncEnd() {
|
||||
|
||||
}
|
||||
|
||||
override fun onNtMsgSyncEnd() {
|
||||
|
||||
}
|
||||
|
||||
override fun onNtMsgSyncStart() {
|
||||
|
||||
}
|
||||
|
||||
override fun onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRecvGroupGuildFlag(i2: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRecvMsg(records: ArrayList<MsgRecord>) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRecvMsgSvrRspTransInfo(
|
||||
j2: Long,
|
||||
contact: Contact?,
|
||||
i2: Int,
|
||||
i3: Int,
|
||||
str: String?,
|
||||
bArr: ByteArray?
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRecvOnlineFileMsg(arrayList: ArrayList<MsgRecord>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRecvS2CMsg(arrayList: ArrayList<Byte>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRecvSysMsg(arrayList: ArrayList<Byte>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRecvUDCFlag(i2: Int) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRichMediaDownloadComplete(fileTransNotifyInfo: FileTransNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRichMediaProgerssUpdate(fileTransNotifyInfo: FileTransNotifyInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onRichMediaUploadComplete(fileTransNotifyInfo: FileTransNotifyInfo) {
|
||||
|
||||
}
|
||||
|
||||
override fun onSearchGroupFileInfoUpdate(searchGroupFileResult: SearchGroupFileResult?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onSendMsgError(j2: Long, contact: Contact?, i2: Int, str: String?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onSysMsgNotification(i2: Int, j2: Long, j3: Long, arrayList: ArrayList<Byte>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onTempChatInfoUpdate(tempChatInfo: TempChatInfo?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onUnreadCntAfterFirstView(hashMap: HashMap<Int, ArrayList<UnreadCntInfo>>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onUnreadCntUpdate(hashMap: HashMap<Int, ArrayList<UnreadCntInfo>>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onUserChannelTabStatusChanged(z: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun onUserOnlineStatusChanged(z: Boolean) {
|
||||
|
||||
}
|
||||
|
||||
override fun onUserTabStatusChanged(arrayList: ArrayList<TabStatusInfo>?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onlineStatusBigIconDownloadPush(i2: Int, j2: Long, str: String?) {
|
||||
|
||||
}
|
||||
|
||||
override fun onlineStatusSmallIconDownloadPush(i2: Int, j2: Long, str: String?) {
|
||||
|
||||
}
|
||||
}
|
@ -1,12 +1,15 @@
|
||||
package qq.service.msg
|
||||
|
||||
import com.tencent.qqnt.kernel.api.IKernelService
|
||||
import com.tencent.qqnt.kernel.nativeinterface.Contact
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||
import qq.service.QQInterfaces
|
||||
import qq.service.contact.ContactHelper
|
||||
import qq.service.internals.msgService
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
@ -28,4 +31,25 @@ internal object MessageHelper: QQInterfaces() {
|
||||
} ?: return Result.failure(Exception("获取临时会话信息失败"))
|
||||
return Result.success(info)
|
||||
}
|
||||
|
||||
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 generateMsgId(chatType: Int): Long {
|
||||
return createMessageUniseq(chatType, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
external fun createMessageUniseq(chatType: Int, time: Long): Long
|
||||
}
|
405
xposed/src/main/java/qq/service/msg/MsgConvertor.kt
Normal file
405
xposed/src/main/java/qq/service/msg/MsgConvertor.kt
Normal file
@ -0,0 +1,405 @@
|
||||
package qq.service.msg
|
||||
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
|
||||
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
|
||||
import io.kritor.event.AtElement
|
||||
import io.kritor.event.Element
|
||||
import io.kritor.event.ElementKt
|
||||
import io.kritor.event.ImageType
|
||||
import io.kritor.event.Scene
|
||||
import io.kritor.event.atElement
|
||||
import io.kritor.event.basketballElement
|
||||
import io.kritor.event.buttonAction
|
||||
import io.kritor.event.buttonActionPermission
|
||||
import io.kritor.event.buttonRender
|
||||
import io.kritor.event.contactElement
|
||||
import io.kritor.event.diceElement
|
||||
import io.kritor.event.faceElement
|
||||
import io.kritor.event.forwardElement
|
||||
import io.kritor.event.imageElement
|
||||
import io.kritor.event.jsonElement
|
||||
import io.kritor.event.locationElement
|
||||
import io.kritor.event.pokeElement
|
||||
import io.kritor.event.rpsElement
|
||||
import io.kritor.event.textElement
|
||||
import io.kritor.event.videoElement
|
||||
import io.kritor.event.voiceElement
|
||||
import moe.fuqiuluo.shamrock.helper.ActionMsgException
|
||||
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.tools.asJsonArray
|
||||
import moe.fuqiuluo.shamrock.tools.asJsonObject
|
||||
import moe.fuqiuluo.shamrock.tools.asString
|
||||
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
|
||||
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
|
||||
import moe.fuqiuluo.shamrock.tools.toHexString
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils.QQ_9_0_8_VER
|
||||
import qq.service.bdh.RichProtoSvc
|
||||
import qq.service.contact.ContactHelper
|
||||
|
||||
typealias NtMessages = ArrayList<MsgElement>
|
||||
typealias Convertor = suspend (MsgRecord, MsgElement) -> Result<Element>
|
||||
|
||||
suspend fun NtMessages.toKritorMessages(record: MsgRecord): ArrayList<Element> {
|
||||
val result = arrayListOf<Element>()
|
||||
forEach {
|
||||
MsgConvertor[it.elementType]?.invoke(record, it)?.onSuccess {
|
||||
result.add(it)
|
||||
}?.onFailure {
|
||||
if (it !is ActionMsgException) {
|
||||
LogCenter.log("消息转换异常: " + it.stackTraceToString(), Level.WARN)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private object MsgConvertor {
|
||||
private val convertorMap = hashMapOf(
|
||||
MsgConstant.KELEMTYPETEXT to ::convertText,
|
||||
MsgConstant.KELEMTYPEFACE to ::convertFace,
|
||||
MsgConstant.KELEMTYPEPIC to ::convertImage,
|
||||
MsgConstant.KELEMTYPEPTT to ::convertVoice,
|
||||
MsgConstant.KELEMTYPEVIDEO to ::convertVideo,
|
||||
MsgConstant.KELEMTYPEMARKETFACE to ::convertMarketFace,
|
||||
MsgConstant.KELEMTYPEARKSTRUCT to ::convertStructJson,
|
||||
MsgConstant.KELEMTYPEREPLY to ::convertReply,
|
||||
//MsgConstant.KELEMTYPEGRAYTIP to ::convertGrayTips,
|
||||
MsgConstant.KELEMTYPEFILE to ::convertFile,
|
||||
MsgConstant.KELEMTYPEMARKDOWN to ::convertMarkdown,
|
||||
//MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem,
|
||||
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem,
|
||||
MsgConstant.KELEMTYPEFACEBUBBLE to ::convertBubbleFace,
|
||||
MsgConstant.KELEMTYPEINLINEKEYBOARD to ::convertInlineKeyboard
|
||||
)
|
||||
|
||||
suspend fun convertText(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val text = element.textElement
|
||||
val elem = Element.newBuilder()
|
||||
if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
|
||||
elem.setAt(atElement {
|
||||
this.uid = text.atNtUid
|
||||
this.uin = ContactHelper.getUinByUidAsync(text.atNtUid).toLong()
|
||||
})
|
||||
} else {
|
||||
elem.setText(textElement {
|
||||
this.text = text.content
|
||||
})
|
||||
}
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertFace(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val face = element.faceElement
|
||||
val elem = Element.newBuilder()
|
||||
if (face.faceType == 5) {
|
||||
elem.setPoke(pokeElement {
|
||||
this.id = face.vaspokeId
|
||||
this.type = face.pokeType
|
||||
this.strength = face.pokeStrength
|
||||
})
|
||||
} else {
|
||||
when(face.faceIndex) {
|
||||
114 -> elem.setBasketball(basketballElement {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
358 -> elem.setDice(diceElement {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
359 -> elem.setRps(rpsElement {
|
||||
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
|
||||
})
|
||||
394 -> elem.setFace(faceElement {
|
||||
this.id = face.faceIndex
|
||||
this.isBig = face.faceType == 3
|
||||
this.result = face.resultId.ifNullOrEmpty { "1" }?.toInt() ?: 1
|
||||
})
|
||||
else -> elem.setFace(faceElement {
|
||||
this.id = face.faceIndex
|
||||
this.isBig = face.faceType == 3
|
||||
})
|
||||
}
|
||||
}
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertImage(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
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 = record.chatType,
|
||||
size = image.fileSize,
|
||||
sha = "",
|
||||
fileId = image.fileUuid,
|
||||
storeId = storeId,
|
||||
)
|
||||
)
|
||||
|
||||
val originalUrl = image.originImageUrl ?: ""
|
||||
LogCenter.log({ "receive image: $image" }, Level.DEBUG)
|
||||
|
||||
val elem = Element.newBuilder()
|
||||
elem.setImage(imageElement {
|
||||
this.file = md5
|
||||
this.url = when (record.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 = record.peerUin.toString()
|
||||
)
|
||||
|
||||
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 = record.senderUin.toString(),
|
||||
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 = record.channelId.ifNullOrEmpty { record.peerUin.toString() } ?: "0",
|
||||
subPeer = record.guildId ?: "0"
|
||||
)
|
||||
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}")
|
||||
}
|
||||
this.type = if (image.isFlashPic == true) ImageType.FLASH else if (image.original) ImageType.ORIGIN else ImageType.COMMON
|
||||
this.subType = image.picSubType
|
||||
})
|
||||
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertVoice(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val ptt = element.pttElement
|
||||
val elem = Element.newBuilder()
|
||||
|
||||
val md5 = if (ptt.fileName.startsWith("silk"))
|
||||
ptt.fileName.substring(5)
|
||||
else ptt.md5HexStr
|
||||
|
||||
elem.setVoice(voiceElement {
|
||||
this.url = when (record.chatType) {
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt.fileUuid)
|
||||
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl("0", md5.hex2ByteArray(), ptt.fileUuid)
|
||||
|
||||
else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}")
|
||||
}
|
||||
this.file = md5
|
||||
this.magic = ptt.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE
|
||||
})
|
||||
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertVideo(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val video = element.videoElement
|
||||
val elem = Element.newBuilder()
|
||||
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()
|
||||
elem.setVideo(videoElement {
|
||||
this.file = md5.toHexString()
|
||||
this.url = when (record.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: ${record.chatType}")
|
||||
}
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertMarketFace(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val marketFace = element.marketFaceElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setMarketFace(io.kritor.event.marketFaceElement {
|
||||
this.id = marketFace.emojiId.lowercase()
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertStructJson(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val data = element.arkElement.bytesData.asJsonObject
|
||||
val elem = Element.newBuilder()
|
||||
when (data["app"].asString) {
|
||||
"com.tencent.multimsg" -> {
|
||||
val info = data["meta"].asJsonObject["detail"].asJsonObject
|
||||
elem.setForward(forwardElement {
|
||||
this.id = info["resid"].asString
|
||||
this.uniseq = info["uniseq"].asString
|
||||
this.summary = info["summary"].asString
|
||||
this.description = info["news"].asJsonArray.joinToString("\n") {
|
||||
it.asJsonObject["text"].asString
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
"com.tencent.troopsharecard" -> {
|
||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||||
elem.setContact(contactElement {
|
||||
this.scene = Scene.GROUP
|
||||
this.peer = info["jumpUrl"].asString.split("group_code=")[1]
|
||||
})
|
||||
}
|
||||
|
||||
"com.tencent.contact.lua" -> {
|
||||
val info = data["meta"].asJsonObject["contact"].asJsonObject
|
||||
elem.setContact(contactElement {
|
||||
this.scene = Scene.FRIEND
|
||||
this.peer = info["jumpUrl"].asString.split("uin=")[1]
|
||||
})
|
||||
}
|
||||
|
||||
"com.tencent.map" -> {
|
||||
val info = data["meta"].asJsonObject["Location.Search"].asJsonObject
|
||||
elem.setLocation(locationElement {
|
||||
this.lat = info["lat"].asString.toFloat()
|
||||
this.lon = info["lng"].asString.toFloat()
|
||||
this.address = info["address"].asString
|
||||
this.title = info["name"].asString
|
||||
})
|
||||
}
|
||||
|
||||
else -> elem.setJson(jsonElement {
|
||||
this.json = data.toString()
|
||||
})
|
||||
}
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertReply(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val reply = element.replyElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setReply(io.kritor.event.replyElement {
|
||||
this.messageId = reply.replayMsgId
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertFile(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
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 (record.chatType) {
|
||||
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
|
||||
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(record.guildId, record.channelId, fileId, bizId)
|
||||
else -> RichProtoSvc.getGroupFileDownUrl(record.peerUin, fileId, bizId)
|
||||
}
|
||||
val elem = Element.newBuilder()
|
||||
elem.setFile(io.kritor.event.fileElement {
|
||||
this.name = fileName
|
||||
this.size = fileSize
|
||||
this.url = url
|
||||
this.expireTime = expireTime
|
||||
this.id = fileId
|
||||
this.subId = fileSubId
|
||||
this.biz = bizId
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertMarkdown(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val markdown = element.markdownElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setMarkdown(io.kritor.event.markdownElement {
|
||||
this.markdown = markdown.content
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertBubbleFace(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val bubbleFace = element.faceBubbleElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setBubbleFace(io.kritor.event.bubbleFaceElement {
|
||||
this.id = bubbleFace.yellowFaceInfo.index
|
||||
this.count = bubbleFace.faceCount ?: 1
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
suspend fun convertInlineKeyboard(record: MsgRecord, element: MsgElement): Result<Element> {
|
||||
val inlineKeyboard = element.inlineKeyboardElement
|
||||
val elem = Element.newBuilder()
|
||||
elem.setButton(io.kritor.event.buttonElement {
|
||||
inlineKeyboard.rows.forEach { row ->
|
||||
this.rows.add(io.kritor.event.row {
|
||||
row.buttons.forEach buttonsLoop@ { button ->
|
||||
if (button == null) return@buttonsLoop
|
||||
this.buttons.add(io.kritor.event.button {
|
||||
this.id = button.id
|
||||
this.action = buttonAction {
|
||||
this.type = button.type
|
||||
this.permission = buttonActionPermission {
|
||||
this.type = button.permissionType
|
||||
button.specifyRoleIds?.let {
|
||||
this.roleIds.addAll(it)
|
||||
}
|
||||
button.specifyTinyids?.let {
|
||||
this.userIds.addAll(it)
|
||||
}
|
||||
}
|
||||
this.unsupportedTips = button.unsupportTips ?: ""
|
||||
this.data = button.data ?: ""
|
||||
this.reply = button.isReply
|
||||
this.enter = button.enter
|
||||
}
|
||||
this.renderData = buttonRender {
|
||||
this.label = button.label ?: ""
|
||||
this.visitedLabel = button.visitedLabel ?: ""
|
||||
this.style = button.style
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return Result.success(elem.build())
|
||||
}
|
||||
|
||||
operator fun get(case: Int): Convertor? {
|
||||
return convertorMap[case]
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user