diff --git a/app/src/main/cpp/CMakeLists.txt b/app/src/main/cpp/CMakeLists.txt index a69e8ff..ea53a89 100644 --- a/app/src/main/cpp/CMakeLists.txt +++ b/app/src/main/cpp/CMakeLists.txt @@ -37,7 +37,6 @@ add_library(${CMAKE_PROJECT_NAME} SHARED # List C/C++ source files with relative paths to this CMakeLists.txt. ${SRC_DIR} md5.cpp - cqcode.cpp silk.cpp message.cpp shamrock.cpp) diff --git a/app/src/main/cpp/cqcode.cpp b/app/src/main/cpp/cqcode.cpp deleted file mode 100644 index 60a5682..0000000 --- a/app/src/main/cpp/cqcode.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include -#include "cqcode.h" - -inline void replace_string(std::string& str, const std::string& from, const std::string& to) { - size_t startPos = 0; - while ((startPos = str.find(from, startPos)) != std::string::npos) { - str.replace(startPos, from.length(), to); - startPos += to.length(); - } -} - -inline int utf8_next_len(const std::string& str, size_t offset) -{ - uint8_t c = (uint8_t)str[offset]; - if (c >= 0xFC) - return 6; - else if (c >= 0xF8) - return 5; - else if (c >= 0xF0) - return 4; - else if (c >= 0xE0) - return 3; - else if (c >= 0xC0) - return 2; - else if (c > 0x00) - return 1; - else - return 0; -} - - -void decode_cqcode(const std::string& code, std::vector>& dest) { - std::string cache; - bool is_start = false; - std::string key_tmp; - std::unordered_map kv; - for(size_t i = 0; i < code.size(); i++) { - int utf8_char_len = utf8_next_len(code, i); - if(utf8_char_len == 0) { - continue; - } - std::string_view c(&code[i],utf8_char_len); - if (c == "[") { - if (is_start) { - throw illegal_code(); - } else { - if (!cache.empty()) { - std::unordered_map kv; - replace_string(cache, "[", "["); - replace_string(cache, "]", "]"); - replace_string(cache, "&", "&"); - kv.emplace("_type", "text"); - kv.emplace("text", cache); - dest.push_back(kv); - cache.clear(); - } - std::string_view cq_flag(&code[i],4); - if(cq_flag == "[CQ:"){ - is_start = true; - i += 3; - }else{ - cache += c; - } - } - } - else if (c == "=") { - if (is_start) { - if (cache.empty()) { - throw illegal_code(); - } else { - if (key_tmp.empty()) { - key_tmp.append(cache); - cache.clear(); - } else { - cache += c; - } - } - } else { - cache += c; - } - } - else if (c == ",") { - if (is_start) { - if (kv.count("_type") == 0 && !cache.empty()) { - kv.emplace("_type", cache); - cache.clear(); - } else { - if (!key_tmp.empty()) { - replace_string(cache, "[", "["); - replace_string(cache, "]", "]"); - replace_string(cache, ",", ","); - replace_string(cache, "&", "&"); - kv.emplace(key_tmp, cache); - cache.clear(); - key_tmp.clear(); - } - } - } else { - cache += c; - } - } - else if (c == "]") { - if (is_start) { - if (!cache.empty()) { - if (!key_tmp.empty()) { - replace_string(cache, "[", "["); - replace_string(cache, "]", "]"); - replace_string(cache, ",", ","); - replace_string(cache, "&", "&"); - kv.emplace(key_tmp, cache); - } else { - kv.emplace("_type", cache); - } - dest.push_back(kv); - kv.clear(); - key_tmp.clear(); - cache.clear(); - is_start = false; - } - } else { - cache += c; - } - } - else { - cache += c; - i += (utf8_char_len - 1); - } - } - if (!cache.empty()) { - std::unordered_map kv; - replace_string(cache, "[", "["); - replace_string(cache, "]", "]"); - replace_string(cache, "&", "&"); - kv.emplace("_type", "text"); - kv.emplace("text", cache); - dest.push_back(kv); - } -} diff --git a/app/src/main/cpp/group_honor.cpp b/app/src/main/cpp/group_honor.cpp deleted file mode 100644 index ebae11f..0000000 --- a/app/src/main/cpp/group_honor.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "jni.h" -#include -#include -#include - -struct Honor { - int id; - std::string name; - std::string icon_url; - int priority; -}; - -int calc_honor_flag(int honor_id, char honor_flag); - -jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor); - - -extern "C" -JNIEXPORT jobject JNICALL -Java_moe_fuqiuluo_shamrock_remote_action_handlers_GetTroopHonor_nativeDecodeHonor(JNIEnv *env, jobject thiz, - jstring user_id, - jint honor_id, - jbyte honor_flag) { - static std::vector honor_list = { - Honor{1, "龙王", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150116_n4PxCiurbm.png", 1}, - Honor{2, "群聊之火", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190136_92JEGFKC5k.png", 3}, - Honor{3, "群聊炽焰", "https://qzonestyle.gtimg.cn/aoi/sola/20200217190204_zgCTeSrMq1.png", 4}, - Honor{5, "冒尖小春笋", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150335_tUJCAtoKVP.png", 5}, - Honor{6, "快乐源泉", "https://qzonestyle.gtimg.cn/aoi/sola/20200213150434_3tDmsJExCP.png", 7}, - Honor{7, "学术新星", "https://sola.gtimg.cn/aoi/sola/20200515140645_j0X6gbuHNP.png", 8}, - Honor{8, "顶尖学霸", "https://sola.gtimg.cn/aoi/sola/20200515140639_0CtWOpfVzK.png", 9}, - Honor{9, "至尊学神", "https://sola.gtimg.cn/aoi/sola/20200515140628_P8UEYBjMBT.png", 10}, - Honor{10, "一笔当先", "https://sola.gtimg.cn/aoi/sola/20200515140654_4r94tSCdaB.png", 11}, - Honor{11, "奋进小翠竹", "https://sola.gtimg.cn/aoi/sola/20200812151819_wbj6z2NGoB.png", 6}, - Honor{12, "氛围魔杖", "https://sola.gtimg.cn/aoi/sola/20200812151831_4ZJgQCaD1H.png", 2}, - Honor{13, "壕礼皇冠", "https://sola.gtimg.cn/aoi/sola/20200930154050_juZOAMg7pt.png", 12}, - }; - int flag = calc_honor_flag(honor_id, honor_flag); - if ((honor_id != 1 && honor_id != 2 && honor_id != 3) || flag != 1) { - auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) { - return honor.id == honor_id; - }); - return make_honor_object(env, user_id, honor); - } else { - auto honor = *std::find_if(honor_list.begin(), honor_list.end(), [&honor_id](auto &honor) { - return honor.id == honor_id; - }); - std::string url = "https://static-res.qq.com/static-res/groupInteract/vas/a/" + std::to_string(honor_id) + "_1.png"; - honor = Honor{honor_id, honor.name, url, honor.priority}; - return make_honor_object(env, user_id, honor); - } -} - -int calc_honor_flag(int honor_id, char honor_flag) { - int flag; - if (honor_flag == 0) { - return 0; - } - if (honor_id == 1) { - flag = honor_flag; - } else if (honor_id == 2 || honor_id == 3) { - flag = honor_flag >> 2; - } else if (honor_id != 4) { - return 0; - } else { - flag = honor_flag >> 4; - } - return flag & 3; -} - -jobject make_honor_object(JNIEnv *env, jobject user_id, const Honor& honor) { - jclass GroupMemberHonor = env->FindClass("moe/fuqiuluo/shamrock/remote/service/data/GroupMemberHonor"); - jmethodID GroupMemberHonor_init = env->GetMethodID(GroupMemberHonor, "", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;)V"); - auto user_id_str = (jstring) user_id; - jstring honor_desc = env->NewStringUTF(honor.name.c_str()); - jstring uin_name = env->NewStringUTF(""); - jstring honor_icon_url = env->NewStringUTF(honor.icon_url.c_str()); - jobject ret = env->NewObject(GroupMemberHonor, GroupMemberHonor_init, user_id_str, uin_name, honor_icon_url, 0, honor.id, honor_desc); - - env->DeleteLocalRef(GroupMemberHonor); - env->DeleteLocalRef(user_id_str); - env->DeleteLocalRef(honor_desc); - env->DeleteLocalRef(honor_icon_url); - - return ret; -} diff --git a/app/src/main/cpp/interface/cqcode.h b/app/src/main/cpp/interface/cqcode.h deleted file mode 100644 index aed24f9..0000000 --- a/app/src/main/cpp/interface/cqcode.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef UNTITLED_CQCODE_H -#define UNTITLED_CQCODE_H - -#include -#include -#include -#include - -class illegal_code: std::exception { -public: - [[nodiscard]] const char * what() const noexcept override { - return "Error cq code."; - } -}; - -void decode_cqcode(const std::string& code, std::vector>& dest); - -void encode_cqcode(const std::vector>& segment, std::string& dest); - -#endif //UNTITLED_CQCODE_H diff --git a/app/src/main/cpp/message.cpp b/app/src/main/cpp/message.cpp index 97fec4a..09284d4 100644 --- a/app/src/main/cpp/message.cpp +++ b/app/src/main/cpp/message.cpp @@ -1,5 +1,4 @@ #include "jni.h" -#include "cqcode.h" #include inline void replace_string(std::string& str, const std::string& from, const std::string& to) { @@ -12,7 +11,7 @@ inline void replace_string(std::string& str, const std::string& from, const std: extern "C" JNIEXPORT jlong JNICALL -Java_moe_fuqiuluo_shamrock_helper_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz, +Java_qq_service_msg_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz, jint chat_type, jlong time) { static std::random_device rd; @@ -32,123 +31,6 @@ Java_moe_fuqiuluo_shamrock_helper_MessageHelper_getChatType(JNIEnv *env, jobject return (int32_t) ((int64_t) msg_id & 0xffL); } -extern "C" -JNIEXPORT jobject JNICALL -Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeDecodeCQCode(JNIEnv *env, jobject thiz, - jstring code) { - jclass ArrayList = env->FindClass("java/util/ArrayList"); - jmethodID NewArrayList = env->GetMethodID(ArrayList, "", "()V"); - jmethodID ArrayListAdd = env->GetMethodID(ArrayList, "add", "(Ljava/lang/Object;)Z"); - jobject arrayList = env->NewObject(ArrayList, NewArrayList); - - const char* cCode = env->GetStringUTFChars(code, nullptr); - std::string cppCode = cCode; - std::vector> dest; - try { - decode_cqcode(cppCode, dest); - } catch (illegal_code& code) { - return arrayList; - } - - jclass HashMap = env->FindClass("java/util/HashMap"); - jmethodID NewHashMap = env->GetMethodID(HashMap, "", "()V"); - jclass String = env->FindClass("java/lang/String"); - jmethodID NewString = env->GetMethodID(String, "", "([BLjava/lang/String;)V"); - jstring charset = env->NewStringUTF("UTF-8"); - jmethodID put = env->GetMethodID(HashMap, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"); - for (auto& map : dest) { - jobject hashMap = env->NewObject(HashMap, NewHashMap); - for (const auto& pair : map) { - jbyteArray keyArray = env->NewByteArray((int) pair.first.size()); - jbyteArray valueArray = env->NewByteArray((int) pair.second.size()); - env->SetByteArrayRegion(keyArray, 0, (int) pair.first.size(), (jbyte*)pair.first.c_str()); - env->SetByteArrayRegion(valueArray, 0, (int) pair.second.size(), (jbyte*)pair.second.c_str()); - auto key = (jstring) env->NewObject(String, NewString, keyArray, charset); - auto value = (jstring) env->NewObject(String, NewString, valueArray, charset); - env->CallObjectMethod(hashMap, put, key, value); - } - env->CallBooleanMethod(arrayList, ArrayListAdd, hashMap); - } - - env->DeleteLocalRef(ArrayList); - env->DeleteLocalRef(HashMap); - env->DeleteLocalRef(String); - env->DeleteLocalRef(charset); - env->ReleaseStringUTFChars(code, cCode); - - return arrayList; -} - -extern "C" -JNIEXPORT jstring JNICALL -Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeEncodeCQCode(JNIEnv *env, jobject thiz, - jobject segment_list) { - jclass List = env->FindClass("java/util/List"); - jmethodID ListSize = env->GetMethodID(List, "size", "()I"); - jmethodID ListGet = env->GetMethodID(List, "get", "(I)Ljava/lang/Object;"); - jclass Map = env->FindClass("java/util/Map"); - jmethodID MapGet = env->GetMethodID(Map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); - jmethodID entrySetMethod = env->GetMethodID(Map, "entrySet", "()Ljava/util/Set;"); - jclass setClass = env->FindClass("java/util/Set"); - jmethodID iteratorMethod = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;"); - jclass entryClass = env->FindClass("java/util/Map$Entry"); - jmethodID getKeyMethod = env->GetMethodID(entryClass, "getKey", "()Ljava/lang/Object;"); - jmethodID getValueMethod = env->GetMethodID(entryClass, "getValue", "()Ljava/lang/Object;"); - - std::string result; - jint size = env->CallIntMethod(segment_list, ListSize); - for (int i = 0; i < size; i++ ) { - jobject segment = env->CallObjectMethod(segment_list, ListGet, i); - jobject entrySet = env->CallObjectMethod(segment, entrySetMethod); - jobject iterator = env->CallObjectMethod(entrySet, iteratorMethod); - auto type = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("_type")); - auto typeString = env->GetStringUTFChars(type, nullptr); - if (strcmp(typeString, "text") == 0) { - auto text = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("text")); - auto textString = env->GetStringUTFChars(text, nullptr); - std::string tmpValue = textString; - replace_string(tmpValue, "&", "&"); - replace_string(tmpValue, "[", "["); - replace_string(tmpValue, "]", "]"); - replace_string(tmpValue, ",", ","); - result.append(tmpValue); - env->ReleaseStringUTFChars(text, textString); - } else { - result.append("[CQ:"); - result.append(typeString); - while (env->CallBooleanMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "hasNext", "()Z"))) { - jobject entry = env->CallObjectMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "next", "()Ljava/lang/Object;")); - auto key = (jstring) env->CallObjectMethod(entry, getKeyMethod); - auto value = (jstring) env->CallObjectMethod(entry, getValueMethod); - auto keyString = env->GetStringUTFChars(key, nullptr); - auto valueString = env->GetStringUTFChars(value, nullptr); - if (strcmp(keyString, "_type") != 0) { - std::string tmpValue = valueString; - replace_string(tmpValue, "&", "&"); - replace_string(tmpValue, "[", "["); - replace_string(tmpValue, "]", "]"); - replace_string(tmpValue, ",", ","); - result.append(",").append(keyString).append("=").append(tmpValue); - } - env->ReleaseStringUTFChars(key, keyString); - env->ReleaseStringUTFChars(value, valueString); - env->DeleteLocalRef(entry); - env->DeleteLocalRef(key); - env->DeleteLocalRef(value); - } - result.append("]"); - } - env->ReleaseStringUTFChars(type, typeString); - } - - env->DeleteLocalRef(List); - env->DeleteLocalRef(Map); - env->DeleteLocalRef(setClass); - env->DeleteLocalRef(entryClass); - return env->NewStringUTF(result.c_str()); -} - - extern "C" JNIEXPORT jlong JNICALL Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz, diff --git a/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt index 96b8f28..e12c009 100644 --- a/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt +++ b/protobuf/src/main/java/protobuf/oidb/cmd0x11c5/NtV2RichMediaRsp.kt @@ -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?, diff --git a/qqinterface/src/main/java/com/tencent/qqnt/kernel/api/impl/MsgService.java b/qqinterface/src/main/java/com/tencent/qqnt/kernel/api/impl/MsgService.java index 3fc02e1..4fa64c0 100644 --- a/qqinterface/src/main/java/com/tencent/qqnt/kernel/api/impl/MsgService.java +++ b/qqinterface/src/main/java/com/tencent/qqnt/kernel/api/impl/MsgService.java @@ -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; } diff --git a/xposed/src/main/java/kritor/service/GroupFileService.kt b/xposed/src/main/java/kritor/service/GroupFileService.kt index 3dc2121..1daaa63 100644 --- a/xposed/src/main/java/kritor/service/GroupFileService.kt +++ b/xposed/src/main/java/kritor/service/GroupFileService.kt @@ -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() diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt index cc72ed2..eb2de00 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/internals/GlobalEventTransmitter.kt @@ -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, ): 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, ): 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 } } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt index 878cc10..e402c6f 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt @@ -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) { diff --git a/xposed/src/main/java/qq/service/bdh/FileTransfer.kt b/xposed/src/main/java/qq/service/bdh/FileTransfer.kt new file mode 100644 index 0000000..4e44bcb --- /dev/null +++ b/xposed/src/main/java/qq/service/bdh/FileTransfer.kt @@ -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 + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt b/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt new file mode 100644 index 0000000..233a55e --- /dev/null +++ b/xposed/src/main/java/qq/service/bdh/NtV2RichMediaSvc.kt @@ -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, + timeout: Duration, + retryCnt: Int = 5 + ): Result> { + 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, + timeout: Duration + ): Result> { + 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() + 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 { + 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().buffer.decodeProtobuf().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 { + 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 { + 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().buffer + val rsp = rspBuffer.decodeProtobuf() + 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 { + 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() + .msgTryUpImgRsp!!.first() + TryUpPicData( + uKey = rsp.ukey, + exist = rsp.fileExist, + fileId = rsp.fileId.toULong(), + upIp = rsp.upIp, + upPort = rsp.upPort + ) + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt b/xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt new file mode 100644 index 0000000..f8ac3a2 --- /dev/null +++ b/xposed/src/main/java/qq/service/bdh/RichProtoSvc.kt @@ -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() + .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) + } + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/bdh/TryUpPicData.kt b/xposed/src/main/java/qq/service/bdh/TryUpPicData.kt new file mode 100644 index 0000000..aa6a2c4 --- /dev/null +++ b/xposed/src/main/java/qq/service/bdh/TryUpPicData.kt @@ -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? = null, + @SerialName("up_port") var upPort: ArrayList? = null, +) \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/file/GroupFileHelper.kt b/xposed/src/main/java/qq/service/file/GroupFileHelper.kt index bf1131b..61076ce 100644 --- a/xposed/src/main/java/qq/service/file/GroupFileHelper.kt +++ b/xposed/src/main/java/qq/service/file/GroupFileHelper.kt @@ -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() diff --git a/xposed/src/main/java/qq/service/group/GroupHelper.kt b/xposed/src/main/java/qq/service/group/GroupHelper.kt index e6dc318..0189399 100644 --- a/xposed/src/main/java/qq/service/group/GroupHelper.kt +++ b/xposed/src/main/java/qq/service/group/GroupHelper.kt @@ -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()) { diff --git a/xposed/src/main/java/qq/service/internals/AioListener.kt b/xposed/src/main/java/qq/service/internals/AioListener.kt index 86e7a27..d8d9ade 100644 --- a/xposed/src/main/java/qq/service/internals/AioListener.kt +++ b/xposed/src/main/java/qq/service/internals/AioListener.kt @@ -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) { - msgs.forEach { +object AioListener: SimpleKernelMsgListener() { + override fun onRecvMsg(records: ArrayList) { + 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?) { - - } - - override fun onTempChatInfoUpdate(tempChatInfo: TempChatInfo) { - - } - - override fun onMsgAbstractUpdate(arrayList: ArrayList?) { - //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?) { - LogCenter.log("onRecvS2CMsg(${arrayList.toString()})", Level.DEBUG) - } - - override fun onRecvSysMsg(arrayList: ArrayList?) { - 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>) { - // 推送未读消息数量 - } - - override fun onCustomWithdrawConfigUpdate(customWithdrawConfig: CustomWithdrawConfig?) { - LogCenter.log("onCustomWithdrawConfigUpdate: " + customWithdrawConfig.toString(), Level.DEBUG) - } - - override fun onDraftUpdate(contact: Contact?, arrayList: ArrayList?, 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?) { - - } - - override fun onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { - - } - - override fun onFirstViewGroupGuildMapping(arrayList: ArrayList?) { - - } - - 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?) { - 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?) { - //LogCenter.log("onLineDev($arrayList)") - } - - override fun onLogLevelChanged(newLevel: Long) { - - } - - override fun onMsgBoxChanged(arrayList: ArrayList?) { - - } - - override fun onMsgDelete(contact: Contact?, arrayList: ArrayList?) { - - } - - override fun onMsgEventListUpdate(hashMap: HashMap>?) { - - } - - override fun onMsgInfoListAdd(arrayList: ArrayList?) { - - } - - 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?) { - LogCenter.log("onSysMsgNotification($i2, $j2, $j3, $arrayList)", Level.DEBUG) - } - - override fun onUnreadCntAfterFirstView(hashMap: HashMap>?) { - - } - - override fun onUnreadCntUpdate(hashMap: HashMap>?) { - - } - - override fun onUserChannelTabStatusChanged(z: Boolean) { - - } - - override fun onUserOnlineStatusChanged(z: Boolean) { - - } - - override fun onUserTabStatusChanged(arrayList: ArrayList?) { - LogCenter.log("onUserTabStatusChanged($arrayList)", Level.DEBUG) - } - - override fun onlineStatusBigIconDownloadPush(i2: Int, j2: Long, str: String?) { - - } - - override fun onlineStatusSmallIconDownloadPush(i2: Int, j2: Long, str: String?) { - - } } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt b/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt new file mode 100644 index 0000000..47c3c56 --- /dev/null +++ b/xposed/src/main/java/qq/service/kernel/SimpleKernelMsgListener.kt @@ -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>?) { + + } + + override fun onCustomWithdrawConfigUpdate(customWithdrawConfig: CustomWithdrawConfig?) { + + } + + override fun onDraftUpdate(contact: Contact?, arrayList: ArrayList?, j2: Long) { + + } + + override fun onEmojiDownloadComplete(emojiNotifyInfo: EmojiNotifyInfo?) { + + } + + override fun onEmojiResourceUpdate(emojiResourceInfo: EmojiResourceInfo?) { + + } + + override fun onFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { + + } + + override fun onFileMsgCome(arrayList: ArrayList?) { + + } + + override fun onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) { + + } + + override fun onFirstViewGroupGuildMapping(arrayList: ArrayList?) { + + } + + 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?) { + + } + + override fun onLogLevelChanged(j2: Long) { + + } + + override fun onMsgAbstractUpdate(arrayList: ArrayList?) { + + } + + override fun onMsgBoxChanged(arrayList: ArrayList?) { + + } + + override fun onMsgDelete(contact: Contact?, arrayList: ArrayList?) { + + } + + override fun onMsgEventListUpdate(hashMap: HashMap>?) { + + } + + override fun onMsgInfoListAdd(arrayList: ArrayList?) { + + } + + override fun onMsgInfoListUpdate(arrayList: ArrayList?) { + + } + + 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) { + + } + + override fun onRecvMsgSvrRspTransInfo( + j2: Long, + contact: Contact?, + i2: Int, + i3: Int, + str: String?, + bArr: ByteArray? + ) { + + } + + override fun onRecvOnlineFileMsg(arrayList: ArrayList?) { + + } + + override fun onRecvS2CMsg(arrayList: ArrayList?) { + + } + + override fun onRecvSysMsg(arrayList: ArrayList?) { + + } + + 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?) { + + } + + override fun onTempChatInfoUpdate(tempChatInfo: TempChatInfo?) { + + } + + override fun onUnreadCntAfterFirstView(hashMap: HashMap>?) { + + } + + override fun onUnreadCntUpdate(hashMap: HashMap>?) { + + } + + override fun onUserChannelTabStatusChanged(z: Boolean) { + + } + + override fun onUserOnlineStatusChanged(z: Boolean) { + + } + + override fun onUserTabStatusChanged(arrayList: ArrayList?) { + + } + + override fun onlineStatusBigIconDownloadPush(i2: Int, j2: Long, str: String?) { + + } + + override fun onlineStatusSmallIconDownloadPush(i2: Int, j2: Long, str: String?) { + + } +} \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/MessageHelper.kt b/xposed/src/main/java/qq/service/msg/MessageHelper.kt index 29c5c28..8249ac9 100644 --- a/xposed/src/main/java/qq/service/msg/MessageHelper.kt +++ b/xposed/src/main/java/qq/service/msg/MessageHelper.kt @@ -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 } \ No newline at end of file diff --git a/xposed/src/main/java/qq/service/msg/MsgConvertor.kt b/xposed/src/main/java/qq/service/msg/MsgConvertor.kt new file mode 100644 index 0000000..28016f9 --- /dev/null +++ b/xposed/src/main/java/qq/service/msg/MsgConvertor.kt @@ -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 +typealias Convertor = suspend (MsgRecord, MsgElement) -> Result + +suspend fun NtMessages.toKritorMessages(record: MsgRecord): ArrayList { + val result = arrayListOf() + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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] + } +} +