Shamrock: 完成消息推送

Signed-off-by: 白池 <whitechi73@outlook.com>
This commit is contained in:
白池 2024-03-14 20:33:30 +08:00
parent 7bacea3288
commit a95d8d85e8
20 changed files with 1969 additions and 694 deletions

View File

@ -37,7 +37,6 @@ add_library(${CMAKE_PROJECT_NAME} SHARED
# List C/C++ source files with relative paths to this CMakeLists.txt. # List C/C++ source files with relative paths to this CMakeLists.txt.
${SRC_DIR} ${SRC_DIR}
md5.cpp md5.cpp
cqcode.cpp
silk.cpp silk.cpp
message.cpp message.cpp
shamrock.cpp) shamrock.cpp)

View File

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

View File

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

View File

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

View File

@ -1,5 +1,4 @@
#include "jni.h" #include "jni.h"
#include "cqcode.h"
#include <random> #include <random>
inline void replace_string(std::string& str, const std::string& from, const std::string& to) { inline void replace_string(std::string& str, const std::string& from, const std::string& to) {
@ -12,7 +11,7 @@ inline void replace_string(std::string& str, const std::string& from, const std:
extern "C" extern "C"
JNIEXPORT jlong JNICALL JNIEXPORT jlong JNICALL
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz, Java_qq_service_msg_MessageHelper_createMessageUniseq(JNIEnv *env, jobject thiz,
jint chat_type, jint chat_type,
jlong time) { jlong time) {
static std::random_device rd; static std::random_device rd;
@ -32,123 +31,6 @@ Java_moe_fuqiuluo_shamrock_helper_MessageHelper_getChatType(JNIEnv *env, jobject
return (int32_t) ((int64_t) msg_id & 0xffL); return (int32_t) ((int64_t) msg_id & 0xffL);
} }
extern "C"
JNIEXPORT jobject JNICALL
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeDecodeCQCode(JNIEnv *env, jobject thiz,
jstring code) {
jclass ArrayList = env->FindClass("java/util/ArrayList");
jmethodID NewArrayList = env->GetMethodID(ArrayList, "<init>", "()V");
jmethodID ArrayListAdd = env->GetMethodID(ArrayList, "add", "(Ljava/lang/Object;)Z");
jobject arrayList = env->NewObject(ArrayList, NewArrayList);
const char* cCode = env->GetStringUTFChars(code, nullptr);
std::string cppCode = cCode;
std::vector<std::unordered_map<std::string, std::string>> dest;
try {
decode_cqcode(cppCode, dest);
} catch (illegal_code& code) {
return arrayList;
}
jclass HashMap = env->FindClass("java/util/HashMap");
jmethodID NewHashMap = env->GetMethodID(HashMap, "<init>", "()V");
jclass String = env->FindClass("java/lang/String");
jmethodID NewString = env->GetMethodID(String, "<init>", "([BLjava/lang/String;)V");
jstring charset = env->NewStringUTF("UTF-8");
jmethodID put = env->GetMethodID(HashMap, "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
for (auto& map : dest) {
jobject hashMap = env->NewObject(HashMap, NewHashMap);
for (const auto& pair : map) {
jbyteArray keyArray = env->NewByteArray((int) pair.first.size());
jbyteArray valueArray = env->NewByteArray((int) pair.second.size());
env->SetByteArrayRegion(keyArray, 0, (int) pair.first.size(), (jbyte*)pair.first.c_str());
env->SetByteArrayRegion(valueArray, 0, (int) pair.second.size(), (jbyte*)pair.second.c_str());
auto key = (jstring) env->NewObject(String, NewString, keyArray, charset);
auto value = (jstring) env->NewObject(String, NewString, valueArray, charset);
env->CallObjectMethod(hashMap, put, key, value);
}
env->CallBooleanMethod(arrayList, ArrayListAdd, hashMap);
}
env->DeleteLocalRef(ArrayList);
env->DeleteLocalRef(HashMap);
env->DeleteLocalRef(String);
env->DeleteLocalRef(charset);
env->ReleaseStringUTFChars(code, cCode);
return arrayList;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_nativeEncodeCQCode(JNIEnv *env, jobject thiz,
jobject segment_list) {
jclass List = env->FindClass("java/util/List");
jmethodID ListSize = env->GetMethodID(List, "size", "()I");
jmethodID ListGet = env->GetMethodID(List, "get", "(I)Ljava/lang/Object;");
jclass Map = env->FindClass("java/util/Map");
jmethodID MapGet = env->GetMethodID(Map, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
jmethodID entrySetMethod = env->GetMethodID(Map, "entrySet", "()Ljava/util/Set;");
jclass setClass = env->FindClass("java/util/Set");
jmethodID iteratorMethod = env->GetMethodID(setClass, "iterator", "()Ljava/util/Iterator;");
jclass entryClass = env->FindClass("java/util/Map$Entry");
jmethodID getKeyMethod = env->GetMethodID(entryClass, "getKey", "()Ljava/lang/Object;");
jmethodID getValueMethod = env->GetMethodID(entryClass, "getValue", "()Ljava/lang/Object;");
std::string result;
jint size = env->CallIntMethod(segment_list, ListSize);
for (int i = 0; i < size; i++ ) {
jobject segment = env->CallObjectMethod(segment_list, ListGet, i);
jobject entrySet = env->CallObjectMethod(segment, entrySetMethod);
jobject iterator = env->CallObjectMethod(entrySet, iteratorMethod);
auto type = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("_type"));
auto typeString = env->GetStringUTFChars(type, nullptr);
if (strcmp(typeString, "text") == 0) {
auto text = (jstring) env->CallObjectMethod(segment, MapGet, env->NewStringUTF("text"));
auto textString = env->GetStringUTFChars(text, nullptr);
std::string tmpValue = textString;
replace_string(tmpValue, "&", "&amp;");
replace_string(tmpValue, "[", "&#91;");
replace_string(tmpValue, "]", "&#93;");
replace_string(tmpValue, ",", "&#44;");
result.append(tmpValue);
env->ReleaseStringUTFChars(text, textString);
} else {
result.append("[CQ:");
result.append(typeString);
while (env->CallBooleanMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "hasNext", "()Z"))) {
jobject entry = env->CallObjectMethod(iterator, env->GetMethodID(env->GetObjectClass(iterator), "next", "()Ljava/lang/Object;"));
auto key = (jstring) env->CallObjectMethod(entry, getKeyMethod);
auto value = (jstring) env->CallObjectMethod(entry, getValueMethod);
auto keyString = env->GetStringUTFChars(key, nullptr);
auto valueString = env->GetStringUTFChars(value, nullptr);
if (strcmp(keyString, "_type") != 0) {
std::string tmpValue = valueString;
replace_string(tmpValue, "&", "&amp;");
replace_string(tmpValue, "[", "&#91;");
replace_string(tmpValue, "]", "&#93;");
replace_string(tmpValue, ",", "&#44;");
result.append(",").append(keyString).append("=").append(tmpValue);
}
env->ReleaseStringUTFChars(key, keyString);
env->ReleaseStringUTFChars(value, valueString);
env->DeleteLocalRef(entry);
env->DeleteLocalRef(key);
env->DeleteLocalRef(value);
}
result.append("]");
}
env->ReleaseStringUTFChars(type, typeString);
}
env->DeleteLocalRef(List);
env->DeleteLocalRef(Map);
env->DeleteLocalRef(setClass);
env->DeleteLocalRef(entryClass);
return env->NewStringUTF(result.c_str());
}
extern "C" extern "C"
JNIEXPORT jlong JNICALL JNIEXPORT jlong JNICALL
Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz, Java_moe_fuqiuluo_shamrock_helper_MessageHelper_insertChatTypeToMsgId(JNIEnv *env, jobject thiz,

View File

@ -9,7 +9,7 @@ import moe.fuqiuluo.symbols.Protobuf
@Serializable @Serializable
data class NtV2RichMediaRsp( data class NtV2RichMediaRsp(
@ProtoNumber(1) val head: RspHead, @ProtoNumber(1) val head: RspHead?,
@ProtoNumber(2) val upload: UploadRsp?, @ProtoNumber(2) val upload: UploadRsp?,
@ProtoNumber(3) val download: DownloadRsp?, @ProtoNumber(3) val download: DownloadRsp?,
@ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?, @ProtoNumber(4) val downloadRkeyRsp: DownloadRkeyRsp?,

View File

@ -18,6 +18,11 @@ public class MsgService {
public void addMsgListener(IKernelMsgListener listener) { public void addMsgListener(IKernelMsgListener listener) {
} }
public void removeMsgListener(@NotNull IKernelMsgListener iKernelMsgListener) {
}
public String getRichMediaFilePathForGuild(@NotNull RichMediaFilePathInfo richMediaFilePathInfo) { public String getRichMediaFilePathForGuild(@NotNull RichMediaFilePathInfo richMediaFilePathInfo) {
return null; return null;
} }

View File

@ -31,7 +31,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
).toByteArray() ).toByteArray()
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data) val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data)
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) ?: 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")) throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
} }
val oidbPkg = oidb_sso.OIDBSSOPkg() val oidbPkg = oidb_sso.OIDBSSOPkg()
@ -57,7 +57,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
folderId = request.folderId folderId = request.folderId
) )
).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) ).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")) throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
} }
val oidbPkg = oidb_sso.OIDBSSOPkg() 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()) val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray())
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) ?: 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")) throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
} }
val oidbPkg = oidb_sso.OIDBSSOPkg() val oidbPkg = oidb_sso.OIDBSSOPkg()
@ -106,7 +106,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
folderName = request.name folderName = request.name
) )
).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) ).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")) throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
} }
val oidbPkg = oidb_sso.OIDBSSOPkg() val oidbPkg = oidb_sso.OIDBSSOPkg()

View File

@ -1,18 +1,22 @@
@file:OptIn(DelicateCoroutinesApi::class)
package moe.fuqiuluo.shamrock.internals package moe.fuqiuluo.shamrock.internals
import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import io.kritor.Scene
import io.kritor.contact
import io.kritor.event.MessageEvent import io.kritor.event.MessageEvent
import io.kritor.event.Scene
import io.kritor.event.contact
import io.kritor.event.messageEvent 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.GlobalScope
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.io.core.BytePacketBuilder import kotlinx.io.core.BytePacketBuilder
import qq.service.QQInterfaces import qq.service.QQInterfaces
import qq.service.msg.toKritorMessages
internal object GlobalEventTransmitter: QQInterfaces() { internal object GlobalEventTransmitter: QQInterfaces() {
private val messageEventFlow by lazy { private val messageEventFlow by lazy {
@ -51,6 +55,7 @@ internal object GlobalEventTransmitter: QQInterfaces() {
this.uid = record.senderUid this.uid = record.senderUid
this.nick = record.sendNickName this.nick = record.sendNickName
} }
this.elements.addAll(elements.toKritorMessages(record))
}) })
return true return true
} }
@ -59,9 +64,23 @@ internal object GlobalEventTransmitter: QQInterfaces() {
record: MsgRecord, record: MsgRecord,
elements: ArrayList<MsgElement>, elements: ArrayList<MsgElement>,
): Boolean { ): Boolean {
val botUin = app.longAccountUin transMessageEvent(record, messageEvent {
var nickName = record.sendNickName 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 return true
} }
@ -71,9 +90,23 @@ internal object GlobalEventTransmitter: QQInterfaces() {
groupCode: Long, groupCode: Long,
fromNick: String, fromNick: String,
): Boolean { ): Boolean {
val botUin = app.longAccountUin transMessageEvent(record, messageEvent {
var nickName = record.sendNickName 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 return true
} }
@ -81,9 +114,23 @@ internal object GlobalEventTransmitter: QQInterfaces() {
record: MsgRecord, record: MsgRecord,
elements: ArrayList<MsgElement>, elements: ArrayList<MsgElement>,
): Boolean { ): Boolean {
val botUin = app.longAccountUin transMessageEvent(record, messageEvent {
var nickName = record.sendNickName 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 return true
} }
} }

View File

@ -7,6 +7,7 @@ import android.content.pm.PackageManager
import android.content.pm.VersionedPackage import android.content.pm.VersionedPackage
import android.os.Build import android.os.Build
import android.os.Looper import android.os.Looper
import com.tencent.qphone.base.remote.ToServiceMsg
import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XSharedPreferences import de.robv.android.xposed.XSharedPreferences
import de.robv.android.xposed.XposedHelpers 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.LuoClassloader
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
import moe.fuqiuluo.symbols.XposedHook import moe.fuqiuluo.symbols.XposedHook
import mqq.app.MobileQQ
import qq.service.QQInterfaces
@XposedHook(priority = 0) @XposedHook(priority = 0)
class AntiDetection: IAction { class AntiDetection: IAction {
@ -36,6 +39,17 @@ class AntiDetection: IAction {
if (ShamrockConfig[AntiJvmTrace]) if (ShamrockConfig[AntiJvmTrace])
antiTrace() antiTrace()
antiMemoryWalking() 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) { private fun antiGetPackageGidsDetection(ctx: Context) {

View File

@ -0,0 +1,190 @@
@file:OptIn(DelicateCoroutinesApi::class)
package qq.service.bdh
import com.tencent.mobileqq.transfile.BaseTransProcessor
import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.TransferRequest
import com.tencent.mobileqq.transfile.api.ITransFileController
import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.utils.MD5
import mqq.app.AppRuntime
import qq.service.QQInterfaces
import java.io.File
import java.lang.Math.abs
import kotlin.coroutines.resume
import kotlin.random.Random
internal abstract class FileTransfer {
suspend fun transC2CResource(
peerId: String,
file: File,
fileType: Int, busiType: Int,
wait: Boolean = true,
builder: (TransferRequest) -> Unit
): Boolean {
val runtime = QQInterfaces.app
val transferRequest = TransferRequest()
transferRequest.needSendMsg = false
transferRequest.mSelfUin = runtime.account
transferRequest.mPeerUin = peerId
transferRequest.mSecondId = runtime.currentAccountUin
transferRequest.mUinType = FileMsg.UIN_BUDDY
transferRequest.mFileType = fileType
transferRequest.mUniseq = createMessageUniseq()
transferRequest.mIsUp = true
builder(transferRequest)
transferRequest.mBusiType = busiType
transferRequest.mMd5 = MD5.genFileMd5Hex(file.absolutePath)
transferRequest.mLocalPath = file.absolutePath
return transAndWait(runtime, transferRequest, wait)
}
suspend fun transTroopResource(
groupId: String,
file: File,
fileType: Int, busiType: Int,
wait: Boolean = true,
builder: (TransferRequest) -> Unit
): Boolean {
val runtime = QQInterfaces.app
val transferRequest = TransferRequest()
transferRequest.needSendMsg = false
transferRequest.mSelfUin = runtime.account
transferRequest.mPeerUin = groupId
transferRequest.mSecondId = runtime.currentAccountUin
transferRequest.mUinType = FileMsg.UIN_TROOP
transferRequest.mFileType = fileType
transferRequest.mUniseq = createMessageUniseq()
transferRequest.mIsUp = true
builder(transferRequest)
transferRequest.mBusiType = busiType
transferRequest.mMd5 = MD5.genFileMd5Hex(file.absolutePath)
transferRequest.mLocalPath = file.absolutePath
return transAndWait(runtime, transferRequest, wait)
}
private suspend fun transAndWait(
runtime: AppRuntime,
transferRequest: TransferRequest,
wait: Boolean
): Boolean {
return withTimeoutOrNull(60_000) {
val service = runtime.getRuntimeService(ITransFileController::class.java, "all")
if(service.transferAsync(transferRequest)) {
if (!wait) { // 如果无需等待直接返回
return@withTimeoutOrNull true
}
suspendCancellableCoroutine { continuation ->
GlobalScope.launch {
lateinit var processor: IHttpCommunicatorListener
while (
//service.findProcessor(
// transferRequest.keyForTransfer // uin + uniseq
//) != null
service.containsProcessor(runtime.currentAccountUin, transferRequest.mUniseq)
// 如果上传处理器依旧存在,说明没有上传成功
&& service.isWorking.get()
) {
processor = service.findProcessor(runtime.currentAccountUin, transferRequest.mUniseq)
delay(100)
}
if (processor is BaseTransProcessor && processor.file != null) {
val fileMsg = processor.file
LogCenter.log("[OldBDH] 资源上传结束(fileId = ${fileMsg.fileID}, fileKey = ${fileMsg.fileKey}, path = ${fileMsg.filePath})")
}
continuation.resume(true)
}
// 实现取消上传器
// 目前没什么用
continuation.invokeOnCancellation {
continuation.resume(false)
}
}
} else true
} ?: false
}
companion object {
const val SEND_MSG_BUSINESS_TYPE_AIO_ALBUM_PIC = 1031
const val SEND_MSG_BUSINESS_TYPE_AIO_KEY_WORD_PIC = 1046
const val SEND_MSG_BUSINESS_TYPE_AIO_QZONE_PIC = 1045
const val SEND_MSG_BUSINESS_TYPE_ALBUM_PIC = 1007
const val SEND_MSG_BUSINESS_TYPE_BLESS = 1056
const val SEND_MSG_BUSINESS_TYPE_CAPTURE_PIC = 1008
const val SEND_MSG_BUSINESS_TYPE_COMMEN_FALSH_PIC = 1040
const val SEND_MSG_BUSINESS_TYPE_CUSTOM = 1006
const val SEND_MSG_BUSINESS_TYPE_DOUTU_PIC = 1044
const val SEND_MSG_BUSINESS_TYPE_FALSH_PIC = 1039
const val SEND_MSG_BUSINESS_TYPE_FAST_IMAGE = 1037
const val SEND_MSG_BUSINESS_TYPE_FORWARD_EDIT = 1048
const val SEND_MSG_BUSINESS_TYPE_FORWARD_PIC = 1009
const val SEND_MSG_BUSINESS_TYPE_FULL_SCREEN_ESSENCE = 1057
const val SEND_MSG_BUSINESS_TYPE_GALEERY_PIC = 1041
const val SEND_MSG_BUSINESS_TYPE_GAME_CENTER_STRATEGY = 1058
const val SEND_MSG_BUSINESS_TYPE_HOT_PIC = 1042
const val SEND_MSG_BUSINESS_TYPE_MIXED_PICS = 1043
const val SEND_MSG_BUSINESS_TYPE_PIC_AIO_ALBUM = 1052
const val SEND_MSG_BUSINESS_TYPE_PIC_CAMERA = 1050
const val SEND_MSG_BUSINESS_TYPE_PIC_FAV = 1053
const val SEND_MSG_BUSINESS_TYPE_PIC_SCREEN = 1027
const val SEND_MSG_BUSINESS_TYPE_PIC_SHARE = 1030
const val SEND_MSG_BUSINESS_TYPE_PIC_TAB_CAMERA = 1051
const val SEND_MSG_BUSINESS_TYPE_QQPINYIN_SEND_PIC = 1038
const val SEND_MSG_BUSINESS_TYPE_RECOMMENDED_STICKER = 1047
const val SEND_MSG_BUSINESS_TYPE_RELATED_EMOTION = 1054
const val SEND_MSG_BUSINESS_TYPE_SHOWLOVE = 1036
const val SEND_MSG_BUSINESS_TYPE_SOGOU_SEND_PIC = 1034
const val SEND_MSG_BUSINESS_TYPE_TROOP_BAR = 1035
const val SEND_MSG_BUSINESS_TYPE_WLAN_RECV_NOTIFY = 1055
const val SEND_MSG_BUSINESS_TYPE_ZHITU_PIC = 1049
const val SEND_MSG_BUSINESS_TYPE_ZPLAN_EMOTICON_GIF = 1060
const val SEND_MSG_BUSINESS_TYPE_ZPLAN_PIC = 1059
const val VIDEO_FORMAT_AFS = 7
const val VIDEO_FORMAT_AVI = 1
const val VIDEO_FORMAT_MKV = 4
const val VIDEO_FORMAT_MOD = 9
const val VIDEO_FORMAT_MOV = 8
const val VIDEO_FORMAT_MP4 = 2
const val VIDEO_FORMAT_MTS = 11
const val VIDEO_FORMAT_RM = 6
const val VIDEO_FORMAT_RMVB = 5
const val VIDEO_FORMAT_TS = 10
const val VIDEO_FORMAT_WMV = 3
const val BUSI_TYPE_GUILD_VIDEO = 4601
const val BUSI_TYPE_MULTI_FORWARD_VIDEO = 1010
const val BUSI_TYPE_PUBACCOUNT_PERM_VIDEO = 1009
const val BUSI_TYPE_PUBACCOUNT_TEMP_VIDEO = 1007
const val BUSI_TYPE_SHORT_VIDEO = 1
const val BUSI_TYPE_SHORT_VIDEO_PTV = 2
const val BUSI_TYPE_VIDEO = 0
const val BUSI_TYPE_VIDEO_EMOTICON_PIC = 1022
const val BUSI_TYPE_VIDEO_EMOTICON_VIDEO = 1021
const val TRANSFILE_TYPE_PIC = 1
const val TRANSFILE_TYPE_PIC_EMO = 65538
const val TRANSFILE_TYPE_PIC_THUMB = 65537
const val TRANSFILE_TYPE_PISMA = 49
const val TRANSFILE_TYPE_RAWPIC = 131075
const val TRANSFILE_TYPE_PROFILE_COVER = 35
const val TRANSFILE_TYPE_PTT = 2
const val TRANSFILE_TYPE_PTT_SLICE_TO_TEXT = 327696
const val TRANSFILE_TYPE_QQHEAD_PIC = 131074
internal fun createMessageUniseq(time: Long = System.currentTimeMillis()): Long {
var uniseq = (time / 1000).toInt().toLong()
uniseq = uniseq shl 32 or kotlin.math.abs(Random.nextInt()).toLong()
return uniseq
}
}
}

View File

@ -0,0 +1,495 @@
package qq.service.bdh
import android.graphics.BitmapFactory
import androidx.exifinterface.media.ExifInterface
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
import com.tencent.qqnt.kernel.nativeinterface.CommonFileInfo
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.PicElement
import com.tencent.qqnt.kernel.nativeinterface.PttElement
import com.tencent.qqnt.kernel.nativeinterface.QQNTWrapperUtil
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo
import com.tencent.qqnt.kernel.nativeinterface.VideoElement
import com.tencent.qqnt.msg.api.IMsgService
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.config.ResourceGroup
import moe.fuqiuluo.shamrock.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.utils.AudioUtils
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MediaType
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.auto.toByteArray
import protobuf.oidb.TrpcOidb
import protobuf.oidb.cmd0x11c5.ClientMeta
import protobuf.oidb.cmd0x11c5.CodecConfigReq
import protobuf.oidb.cmd0x11c5.CommonHead
import protobuf.oidb.cmd0x11c5.DownloadExt
import protobuf.oidb.cmd0x11c5.DownloadReq
import protobuf.oidb.cmd0x11c5.FileInfo
import protobuf.oidb.cmd0x11c5.FileType
import protobuf.oidb.cmd0x11c5.IndexNode
import protobuf.oidb.cmd0x11c5.MultiMediaReqHead
import protobuf.oidb.cmd0x11c5.NtV2RichMediaReq
import protobuf.oidb.cmd0x11c5.NtV2RichMediaRsp
import protobuf.oidb.cmd0x11c5.SceneInfo
import protobuf.oidb.cmd0x11c5.UploadInfo
import protobuf.oidb.cmd0x11c5.UploadReq
import protobuf.oidb.cmd0x11c5.UploadRsp
import protobuf.oidb.cmd0x11c5.VideoDownloadExt
import protobuf.oidb.cmd0x388.Cmd0x388ReqBody
import protobuf.oidb.cmd0x388.Cmd0x388RspBody
import protobuf.oidb.cmd0x388.TryUpImgReq
import qq.service.QQInterfaces
import qq.service.internals.NTServiceFetcher
import qq.service.internals.msgService
import qq.service.kernel.SimpleKernelMsgListener
import qq.service.msg.MessageHelper
import java.io.File
import kotlin.coroutines.resume
import kotlin.math.roundToInt
import kotlin.random.Random
import kotlin.random.nextUInt
import kotlin.random.nextULong
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
internal object NtV2RichMediaSvc: QQInterfaces() {
private val requestIdSeq = atomic(1L)
private fun fetchGroupResUploadTo(): String {
return ShamrockConfig[ResourceGroup].ifNullOrEmpty { "100000000" }!!
}
suspend fun tryUploadResourceByNt(
chatType: Int,
elementType: Int,
resources: ArrayList<File>,
timeout: Duration,
retryCnt: Int = 5
): Result<MutableList<CommonFileInfo>> {
return internalTryUploadResourceByNt(chatType, elementType, resources, timeout).onFailure {
if (retryCnt > 0) {
return tryUploadResourceByNt(chatType, elementType, resources, timeout, retryCnt - 1)
}
}
}
/**
* 批量上传图片
*/
private suspend fun internalTryUploadResourceByNt(
chatType: Int,
elementType: Int,
resources: ArrayList<File>,
timeout: Duration
): Result<MutableList<CommonFileInfo>> {
require(resources.size in 1 .. 10) { "imageFiles.size() must be in 1 .. 10" }
val messages = ArrayList(resources.map { file ->
val elem = MsgElement()
elem.elementType = elementType
when(elementType) {
MsgConstant.KELEMTYPEPIC -> {
val pic = PicElement()
pic.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
val msgService = NTServiceFetcher.kernelService.msgService!!
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
2, 0, pic.md5HexStr, file.name, 1, 0, null, "", true
)
)
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
originalPath
) != file.length()
) {
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true
)
)
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, thumbPath)
}
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(file.absolutePath, options)
val exifInterface = ExifInterface(file.absolutePath)
val orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED
)
if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) {
pic.picWidth = options.outWidth
pic.picHeight = options.outHeight
} else {
pic.picWidth = options.outHeight
pic.picHeight = options.outWidth
}
pic.sourcePath = file.absolutePath
pic.fileSize = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath)
pic.original = true
pic.picType = FileUtils.getPicType(file)
elem.picElement = pic
}
MsgConstant.KELEMTYPEPTT -> {
require(resources.size == 1) // 语音只能单个上传
var pttFile = file
val ptt = PttElement()
when (AudioUtils.getMediaType(pttFile)) {
MediaType.Silk -> {
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
ptt.duration = QRoute.api(IAIOPttApi::class.java)
.getPttFileDuration(pttFile.absolutePath)
}
MediaType.Amr -> {
ptt.duration = AudioUtils.getDurationSec(pttFile)
ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR
}
MediaType.Pcm -> {
val result = AudioUtils.pcmToSilk(pttFile)
ptt.duration = (result.second * 0.001).roundToInt()
pttFile = result.first
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
}
else -> {
val result = AudioUtils.audioToSilk(pttFile)
ptt.duration = runCatching {
QRoute.api(IAIOPttApi::class.java)
.getPttFileDuration(result.second.absolutePath)
}.getOrElse {
result.first
}
pttFile = result.second
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
}
}
ptt.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(pttFile.absolutePath)
val msgService = NTServiceFetcher.kernelService.msgService!!
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
MsgConstant.KELEMTYPEPTT, 0, ptt.md5HexStr, file.name, 1, 0, null, "", true
)
)
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(originalPath) != pttFile.length()) {
QQNTWrapperUtil.CppProxy.copyFile(pttFile.absolutePath, originalPath)
}
if (originalPath != null) {
ptt.filePath = originalPath
} else {
ptt.filePath = pttFile.absolutePath
}
ptt.canConvert2Text = true
ptt.fileId = 0
ptt.fileUuid = ""
ptt.text = ""
ptt.voiceType = MsgConstant.KPTTVOICETYPESOUNDRECORD
ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPENONE
elem.pttElement = ptt
}
MsgConstant.KELEMTYPEVIDEO -> {
require(resources.size == 1) // 视频只能单个上传
val video = VideoElement()
video.videoMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
val msgService = NTServiceFetcher.kernelService.msgService!!
val originalPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
5, 2, video.videoMd5, file.name, 1, 0, null, "", true
)
)
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo(
5, 1, video.videoMd5, file.name, 2, 0, null, "", true
)
)
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
originalPath
) != file.length()
) {
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!)
}
video.fileTime = AudioUtils.getVideoTime(file)
video.fileSize = file.length()
video.fileName = file.name
video.fileFormat = FileTransfer.VIDEO_FORMAT_MP4
video.thumbSize = QQNTWrapperUtil.CppProxy.getFileSize(thumbPath).toInt()
val options = BitmapFactory.Options()
BitmapFactory.decodeFile(thumbPath, options)
video.thumbWidth = options.outWidth
video.thumbHeight = options.outHeight
video.thumbMd5 = QQNTWrapperUtil.CppProxy.genFileMd5Hex(thumbPath)
video.thumbPath = hashMapOf(0 to thumbPath)
elem.videoElement = video
}
else -> throw IllegalArgumentException("unsupported elementType: $elementType")
}
return@map elem
})
if (messages.isEmpty()) {
return Result.failure(Exception("no valid image files"))
}
val contact = when(chatType) {
MsgConstant.KCHATTYPEC2C -> MessageHelper.generateContact(chatType, app.currentAccountUin)
else -> Contact(chatType, fetchGroupResUploadTo(), null)
}
val result = mutableListOf<CommonFileInfo>()
val msgService = NTServiceFetcher.kernelService.msgService
?: return Result.failure(Exception("kernelService.msgService is null"))
withTimeoutOrNull(timeout) {
val uniseq = MessageHelper.generateMsgId(chatType)
suspendCancellableCoroutine {
val listener = object: SimpleKernelMsgListener() {
override fun onRichMediaUploadComplete(fileTransNotifyInfo: FileTransNotifyInfo) {
if (fileTransNotifyInfo.msgId == uniseq) {
result.add(fileTransNotifyInfo.commonFileInfo)
}
if (result.size == resources.size) {
msgService.removeMsgListener(this)
it.resume(true)
}
}
}
msgService.addMsgListener(listener)
QRoute.api(IMsgService::class.java).sendMsg(contact, uniseq, messages) { _, _ ->
if (contact.chatType == MsgConstant.KCHATTYPEGROUP && contact.peerUid == "100000000") {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val service = sessionService.msgService
service.deleteMsg(contact, arrayListOf(uniseq), null)
}
}
it.invokeOnCancellation {
msgService.removeMsgListener(listener)
}
}
}
if (result.isEmpty()) {
return Result.failure(Exception("upload failed"))
}
return Result.success(result)
}
/**
* 获取NT图片的RKEY
*/
suspend fun getNtPicRKey(
fileId: String,
md5: String,
sha: String,
fileSize: ULong,
width: UInt,
height: UInt,
sceneBuilder: suspend SceneInfo.() -> Unit
): Result<String> {
runCatching {
val req = NtV2RichMediaReq(
head = MultiMediaReqHead(
commonHead = CommonHead(
requestId = requestIdSeq.incrementAndGet().toULong(),
cmd = 200u
),
sceneInfo = SceneInfo(
requestType = 2u,
businessType = 1u,
).apply {
sceneBuilder()
},
clientMeta = ClientMeta(2u)
),
download = DownloadReq(
IndexNode(
FileInfo(
fileSize = fileSize,
md5 = md5.lowercase(),
sha1 = sha.lowercase(),
name = "${md5}.jpg",
fileType = FileType(
fileType = 1u,
picFormat = 1000u,
videoFormat = 0u,
voiceFormat = 0u
),
width = width,
height = height,
time = 0u,
original = 1u
),
fileUuid = fileId,
storeId = 1u,
uploadTime = 0u,
ttl = 0u,
subType = 0u,
storeAppId = 0u
),
DownloadExt(
video = VideoDownloadExt(
busiType = 0u,
subBusiType = 0u,
msgCodecConfig = CodecConfigReq(
platformChipinfo = "",
osVer = "",
deviceName = ""
),
flag = 1u
)
)
)
).toByteArray()
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11c5_200", 4549, 200, req, true)
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
return Result.failure(Exception("unable to get multimedia pic info: ${fromServiceMsg?.wupBuffer}"))
}
fromServiceMsg.wupBuffer.slice(4).decodeProtobuf<TrpcOidb>().buffer.decodeProtobuf<NtV2RichMediaRsp>().download?.rkeyParam?.let {
return Result.success(it)
}
}.onFailure {
return Result.failure(it)
}
return Result.failure(Exception("unable to get c2c nt pic"))
}
suspend fun requestUploadNtPic(
file: File,
md5: String,
sha: String,
name: String,
width: UInt,
height: UInt,
retryCnt: Int,
sceneBuilder: suspend SceneInfo.() -> Unit
): Result<UploadRsp> {
return runCatching {
requestUploadNtPic(file, md5, sha, name, width, height, sceneBuilder).getOrThrow()
}.onFailure {
if (retryCnt > 0) {
return requestUploadNtPic(file, md5, sha, name, width, height, retryCnt - 1, sceneBuilder)
}
}
}
private suspend fun requestUploadNtPic(
file: File,
md5: String,
sha: String,
name: String,
width: UInt,
height: UInt,
sceneBuilder: suspend SceneInfo.() -> Unit
): Result<UploadRsp> {
val req = NtV2RichMediaReq(
head = MultiMediaReqHead(
commonHead = CommonHead(
requestId = requestIdSeq.incrementAndGet().toULong(),
cmd = 100u
),
sceneInfo = SceneInfo(
requestType = 2u,
businessType = 1u,
).apply {
sceneBuilder()
},
clientMeta = ClientMeta(2u)
),
upload = UploadReq(
listOf(UploadInfo(
FileInfo(
fileSize = file.length().toULong(),
md5 = md5,
sha1 = sha,
name = name,
fileType = FileType(
fileType = 1u,
picFormat = 1000u,
videoFormat = 0u,
voiceFormat = 0u
),
width = width,
height = height,
time = 0u,
original = 1u
),
subFileType = 0u
)),
tryFastUploadCompleted = true,
srvSendMsg = false,
clientRandomId = Random.nextULong(),
compatQMsgSceneType = 1u,
clientSeq = Random.nextUInt(),
noNeedCompatMsg = false
)
).toByteArray()
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true, timeout = 3.seconds)
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
return Result.failure(Exception("unable to request upload nt pic"))
}
val rspBuffer = fromServiceMsg.wupBuffer.slice(4).decodeProtobuf<TrpcOidb>().buffer
val rsp = rspBuffer.decodeProtobuf<NtV2RichMediaRsp>()
if (rsp.upload == null) {
return Result.failure(Exception("unable to request upload nt pic: ${rsp.head}"))
}
return Result.success(rsp.upload!!)
}
/**
* 使用OldBDH获取图片上传状态以及图片上传服务器
*/
suspend fun requestUploadGroupPic(
groupId: ULong,
md5: String,
fileSize: ULong,
width: UInt,
height: UInt,
): Result<TryUpPicData> {
return runCatching {
val fromServiceMsg = sendBufferAW("ImgStore.GroupPicUp", true, Cmd0x388ReqBody(
netType = 3,
subCmd = 1,
msgTryUpImg = arrayListOf(
TryUpImgReq(
groupCode = groupId.toLong(),
srcUin = app.longAccountUin,
fileMd5 = md5.hex2ByteArray(),
fileSize = fileSize.toLong(),
fileName = "$md5.jpg",
srcTerm = 2,
platformType = 9,
buType = 212,
picWidth = width.toInt(),
picHeight = height.toInt(),
picType = 1000,
buildVer = "1.0.0",
originalPic = 1,
fileIndex = byteArrayOf(),
srvUpload = 0
)
),
).toByteArray())
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
return Result.failure(Exception("unable to request upload group pic"))
}
val rsp = fromServiceMsg.wupBuffer.slice(4).decodeProtobuf<Cmd0x388RspBody>()
.msgTryUpImgRsp!!.first()
TryUpPicData(
uKey = rsp.ukey,
exist = rsp.fileExist,
fileId = rsp.fileId.toULong(),
upIp = rsp.upIp,
upPort = rsp.upPort
)
}
}
}

View File

@ -0,0 +1,429 @@
@file:OptIn(ExperimentalSerializationApi::class)
package qq.service.bdh
import com.tencent.mobileqq.pb.ByteStringMicro
import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.api.IProtoReqManager
import com.tencent.mobileqq.transfile.protohandler.RichProto
import com.tencent.mobileqq.transfile.protohandler.RichProtoProc
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.ExperimentalSerializationApi
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import moe.fuqiuluo.symbols.decodeProtobuf
import mqq.app.MobileQQ
import protobuf.auto.toByteArray
import protobuf.oidb.cmd0x11c5.C2CUserInfo
import protobuf.oidb.cmd0x11c5.ChannelUserInfo
import protobuf.oidb.cmd0x11c5.GroupUserInfo
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo
import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
import protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody
import qq.service.QQInterfaces
import qq.service.contact.ContactHelper
import tencent.im.cs.cmd0x346.cmd0x346
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
import tencent.im.oidb.cmd0xe37.cmd0xe37
import tencent.im.oidb.oidb_sso
import kotlin.coroutines.resume
private const val GPRO_PIC = "gchat.qpic.cn"
private const val MULTIMEDIA_DOMAIN = "multimedia.nt.qq.com.cn"
private const val C2C_PIC = "c2cpicdw.qpic.cn"
internal object RichProtoSvc: QQInterfaces() {
suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String {
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, Oidb0xfc2ReqBody(
msgCmd = 1200,
msgBusType = 4202,
msgChannelInfo = Oidb0xfc2ChannelInfo(
guildId = peerId.toULong(),
channelId = channelId.toULong()
),
msgTerminalType = 2,
msgApplyDownloadReq = Oidb0xfc2MsgApplyDownloadReq(
fieldId = fileId,
supportEncrypt = 0
)
).toByteArray())
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
return ""
}
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
body.bytes_bodybuffer
.get().toByteArray()
.decodeProtobuf<Oidb0xfc2RspBody>()
.msgApplyDownloadRsp?.let {
it.msgDownloadInfo?.let {
return "https://${it.downloadDomain}${it.downloadUrl}&fname=$fileId&isthumb=0"
}
}
return ""
}
suspend fun getGroupFileDownUrl(
peerId: Long,
fileId: String,
bizId: Int = 102
): String {
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x6d6_2", 1750, 2, oidb_0x6d6.ReqBody().apply {
download_file_req.set(oidb_0x6d6.DownloadFileReqBody().apply {
uint64_group_code.set(peerId)
uint32_app_id.set(3)
uint32_bus_id.set(bizId)
str_file_id.set(fileId)
})
}.toByteArray())
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
return ""
}
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val result = oidb_0x6d6.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
if (body.uint32_result.get() != 0
|| result.download_file_rsp.int32_ret_code.get() != 0) {
return ""
}
val domain = if (!result.download_file_rsp.str_download_dns.has())
("https://" + result.download_file_rsp.str_download_ip.get())
else ("http://" + result.download_file_rsp.str_download_dns.get().toByteArray().decodeToString())
val downloadUrl = result.download_file_rsp.bytes_download_url.get().toByteArray().toHexString()
val appId = MobileQQ.getMobileQQ().appId
val version = PlatformUtils.getQQVersion(MobileQQ.getContext())
return "$domain/ftn_handler/$downloadUrl/?fname=$fileId&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk"
}
suspend fun getC2CFileDownUrl(
fileId: String,
subId: String,
retryCnt: Int = 0
): String {
val fromServiceMsg = sendOidbAW("OidbSvc.0xe37_1200", 3639, 1200, cmd0xe37.Req0xe37().apply {
bytes_cmd_0x346_req_body.set(ByteStringMicro.copyFrom(cmd0x346.ReqBody().apply {
uint32_cmd.set(1200)
uint32_seq.set(1)
msg_apply_download_req.set(cmd0x346.ApplyDownloadReq().apply {
uint64_uin.set(app.longAccountUin)
bytes_uuid.set(ByteStringMicro.copyFrom(fileId.toByteArray()))
uint32_owner_type.set(2)
str_fileidcrc.set(subId)
})
uint32_business_id.set(3)
uint32_client_type.set(104)
uint32_flag_support_mediaplatform.set(1)
msg_extension_req.set(cmd0x346.ExtensionReq().apply {
uint32_download_url_type.set(1)
})
}.toByteArray()))
}.toByteArray())
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
if (retryCnt < 5) {
return getC2CFileDownUrl(fileId, subId, retryCnt + 1)
}
return ""
} else {
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val result = cmd0x346.RspBody().mergeFrom(cmd0xe37.Resp0xe37().mergeFrom(
body.bytes_bodybuffer.get().toByteArray()
).bytes_cmd_0x346_rsp_body.get().toByteArray())
if (body.uint32_result.get() != 0 ||
result.msg_apply_download_rsp.int32_ret_code.has() && result.msg_apply_download_rsp.int32_ret_code.get() != 0) {
return ""
}
val oldData = result.msg_apply_download_rsp.msg_download_info
//val newData = result[14, 40] NTQQ 文件信息
val domain = if (oldData.str_download_dns.has()) ("https://" + oldData.str_download_dns.get()) else ("http://" + oldData.rpt_str_downloadip_list.get().first())
val params = oldData.str_download_url.get()
val appId = MobileQQ.getMobileQQ().appId
val version = PlatformUtils.getQQVersion(MobileQQ.getContext())
return "$domain$params&isthumb=0&client_proto=qq&client_appid=$appId&client_type=android&client_ver=$version&client_down_type=auto&client_aio_type=unk"
}
}
suspend fun getGroupPicDownUrl(
originalUrl: String,
md5: String,
peer: String = "",
fileId: String = "",
sha: String = "",
fileSize: ULong = 0uL,
width: UInt = 0u,
height: UInt = 0u
): String {
val isNtServer = originalUrl.startsWith("/download")
val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC
if (originalUrl.isNotEmpty()) {
if (isNtServer && !originalUrl.contains("rkey=")) {
NtV2RichMediaSvc.getNtPicRKey(
fileId = fileId,
md5 = md5,
sha = sha,
fileSize = fileSize,
width = width,
height = height
) {
sceneType = 2u
grp = GroupUserInfo(peer.toULong())
}.onSuccess {
return "https://$domain$originalUrl$it"
}.onFailure {
LogCenter.log("getGroupPicDownUrl: ${it.stackTraceToString()}", Level.WARN)
}
}
return "https://$domain$originalUrl"
}
return "https://$domain/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2"
}
suspend fun getC2CPicDownUrl(
originalUrl: String,
md5: String,
peer: String = "",
fileId: String = "",
sha: String = "",
fileSize: ULong = 0uL,
width: UInt = 0u,
height: UInt = 0u,
storeId: Int = 0
): String {
val isNtServer = storeId == 1 || originalUrl.startsWith("/download")
val domain = if (isNtServer) MULTIMEDIA_DOMAIN else C2C_PIC
if (originalUrl.isNotEmpty()) {
if (fileId.isNotEmpty()) NtV2RichMediaSvc.getNtPicRKey(
fileId = fileId,
md5 = md5,
sha = sha,
fileSize = fileSize,
width = width,
height = height
) {
sceneType = 1u
c2c = C2CUserInfo(
accountType = 2u,
uid = ContactHelper.getUidByUinAsync(peer.toLong())
)
}.onSuccess {
if (isNtServer && !originalUrl.contains("rkey=")) {
return "https://$domain$originalUrl$it"
}
}.onFailure {
LogCenter.log("getC2CPicDownUrl: ${it.stackTraceToString()}", Level.WARN)
}
if (isNtServer && !originalUrl.contains("rkey=")) {
return "https://$domain$originalUrl&rkey="
}
return "https://$domain$originalUrl"
}
return "https://$domain/offpic_new/0/0-0-${md5}/0?term=2"
}
suspend fun getGuildPicDownUrl(
originalUrl: String,
md5: String,
peer: String = "",
subPeer: String = "",
fileId: String = "",
sha: String = "",
fileSize: ULong = 0uL,
width: UInt = 0u,
height: UInt = 0u
): String {
val isNtServer = originalUrl.startsWith("/download")
val domain = if (isNtServer) MULTIMEDIA_DOMAIN else GPRO_PIC
if (originalUrl.isNotEmpty()) {
if (isNtServer && !originalUrl.contains("rkey=")) {
NtV2RichMediaSvc.getNtPicRKey(
fileId = fileId,
md5 = md5,
sha = sha,
fileSize = fileSize,
width = width,
height = height
) {
sceneType = 3u
channel = ChannelUserInfo(peer.toULong(), subPeer.toULong(), 1u)
}.onSuccess {
return "https://$domain$originalUrl$it"
}.onFailure {
LogCenter.log("getGuildPicDownUrl: ${it.stackTraceToString()}", Level.WARN)
}
return "https://$domain$originalUrl&rkey="
}
return "https://$domain$originalUrl"
}
return "https://$domain/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2"
}
suspend fun getC2CVideoDownUrl(
peerId: String,
md5: ByteArray,
fileUUId: String
): String {
return suspendCancellableCoroutine {
val richProtoReq = RichProto.RichProtoReq()
val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq()
downReq.selfUin = app.currentAccountUin
downReq.peerUin = peerId
downReq.secondUin = peerId
downReq.uinType = FileMsg.UIN_BUDDY
downReq.agentType = 0
downReq.chatType = 1
downReq.troopUin = peerId
downReq.clientType = 2
downReq.fileId = fileUUId
downReq.md5 = md5
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
downReq.subBusiType = 0
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
downReq.downType = 1
downReq.sceneType = 1
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
LogCenter.log("requestDownPrivateVideo: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
it.resume("")
} else {
val videoDownResp = resp.resps.first() as RichProto.RichProtoResp.ShortVideoDownResp
val url = StringBuilder()
url.append(videoDownResp.mIpList.random().getServerUrl("http://"))
url.append(videoDownResp.mUrl)
it.resume(url.toString())
}
}
richProtoReq.protoKey = RichProtoProc.SHORT_VIDEO_DW
richProtoReq.reqs.add(downReq)
richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all")
RichProtoProc.procRichProtoReq(richProtoReq)
}
}
suspend fun getGroupVideoDownUrl(
peerId: String,
md5: ByteArray,
fileUUId: String
): String {
return suspendCancellableCoroutine {
val richProtoReq = RichProto.RichProtoReq()
val downReq: RichProto.RichProtoReq.ShortVideoDownReq = RichProto.RichProtoReq.ShortVideoDownReq()
downReq.selfUin = app.currentAccountUin
downReq.peerUin = peerId
downReq.secondUin = peerId
downReq.uinType = FileMsg.UIN_TROOP
downReq.agentType = 0
downReq.chatType = 1
downReq.troopUin = peerId
downReq.clientType = 2
downReq.fileId = fileUUId
downReq.md5 = md5
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
downReq.subBusiType = 0
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
downReq.downType = 1
downReq.sceneType = 1
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
LogCenter.log("requestDownGroupVideo: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
it.resume("")
} else {
val videoDownResp = resp.resps.first() as RichProto.RichProtoResp.ShortVideoDownResp
val url = StringBuilder()
url.append(videoDownResp.mIpList.random().getServerUrl("http://"))
url.append(videoDownResp.mUrl)
it.resume(url.toString())
}
}
richProtoReq.protoKey = RichProtoProc.SHORT_VIDEO_DW
richProtoReq.reqs.add(downReq)
richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all")
RichProtoProc.procRichProtoReq(richProtoReq)
}
}
suspend fun getC2CPttDownUrl(
peerId: String,
fileUUId: String
): String {
return suspendCancellableCoroutine {
val richProtoReq = RichProto.RichProtoReq()
val pttDownReq: RichProto.RichProtoReq.C2CPttDownReq = RichProto.RichProtoReq.C2CPttDownReq()
pttDownReq.selfUin = app.currentAccountUin
pttDownReq.peerUin = peerId
pttDownReq.secondUin = peerId
pttDownReq.uinType = FileMsg.UIN_BUDDY
pttDownReq.busiType = 1002
pttDownReq.uuid = fileUUId
pttDownReq.storageSource = "pttcenter"
pttDownReq.isSelfSend = false
pttDownReq.voiceType = 1
pttDownReq.downType = 1
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
LogCenter.log("requestDownPrivateVoice: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
it.resume("")
} else {
val pttDownResp = resp.resps.first() as RichProto.RichProtoResp.C2CPttDownResp
val url = StringBuilder()
url.append(pttDownResp.downloadUrl)
url.append("&client_proto=qq&client_appid=${MobileQQ.getMobileQQ().appId}&client_type=android&client_ver=${PlatformUtils.getQQVersion(MobileQQ.getContext())}&client_down_type=auto&client_aio_type=unk")
it.resume(url.toString())
}
}
richProtoReq.protoKey = RichProtoProc.C2C_PTT_DW
richProtoReq.reqs.add(pttDownReq)
richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all")
RichProtoProc.procRichProtoReq(richProtoReq)
}
}
suspend fun getGroupPttDownUrl(
peerId: String,
md5: ByteArray,
groupFileKey: String
): String {
return suspendCancellableCoroutine {
val richProtoReq = RichProto.RichProtoReq()
val groupPttDownReq: RichProto.RichProtoReq.GroupPttDownReq = RichProto.RichProtoReq.GroupPttDownReq()
groupPttDownReq.selfUin = app.currentAccountUin
groupPttDownReq.peerUin = peerId
groupPttDownReq.secondUin = peerId
groupPttDownReq.uinType = FileMsg.UIN_TROOP
groupPttDownReq.groupFileID = 0
groupPttDownReq.groupFileKey = groupFileKey
groupPttDownReq.md5 = md5
groupPttDownReq.voiceType = 1
groupPttDownReq.downType = 1
richProtoReq.callback = RichProtoProc.RichProtoCallback { _, resp ->
if (resp.resps.isEmpty() || resp.resps.first().errCode != 0) {
LogCenter.log("requestDownGroupVoice: ${resp.resps.firstOrNull()?.errCode}", Level.WARN)
it.resume("")
} else {
val pttDownResp = resp.resps.first() as RichProto.RichProtoResp.GroupPttDownResp
val url = StringBuilder()
url.append("http://")
url.append(pttDownResp.domainV4V6)
url.append(pttDownResp.urlPath)
url.append("&client_proto=qq&client_appid=${MobileQQ.getMobileQQ().appId}&client_type=android&client_ver=${
PlatformUtils.getQQVersion(
MobileQQ.getContext())}&client_down_type=auto&client_aio_type=unk")
it.resume(url.toString())
}
}
richProtoReq.protoKey = RichProtoProc.GRP_PTT_DW
richProtoReq.reqs.add(groupPttDownReq)
richProtoReq.protoReqMgr = app.getRuntimeService(IProtoReqManager::class.java, "all")
RichProtoProc.procRichProtoReq(richProtoReq)
}
}
}

View File

@ -0,0 +1,13 @@
package qq.service.bdh
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TryUpPicData(
@SerialName("ukey") val uKey: ByteArray,
@SerialName("exist") val exist: Boolean,
@SerialName("file_id") val fileId: ULong,
@SerialName("up_ip") var upIp: ArrayList<Long>? = null,
@SerialName("up_port") var upPort: ArrayList<Int>? = null,
)

View File

@ -33,7 +33,7 @@ internal object GroupFileHelper: QQInterfaces() {
it.uint32_bus_id.set(0) it.uint32_bus_id.set(0)
}) })
}.toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) }.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")) throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
} }
val fileCnt: Int val fileCnt: Int
@ -104,7 +104,7 @@ internal object GroupFileHelper: QQInterfaces() {
uint32_show_onlinedoc_folder.set(0) uint32_show_onlinedoc_folder.set(0)
}) })
}.toByteArray(), timeout = 15.seconds) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request")) }.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")) throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
} }
val files = arrayListOf<File>() val files = arrayListOf<File>()

View File

@ -269,7 +269,7 @@ internal object GroupHelper: QQInterfaces() {
uint32_shutup_timestap.set(0) uint32_shutup_timestap.set(0)
}) })
}.toByteArray()) ?: return Result.failure(RuntimeException("[oidb] timeout")) }.toByteArray()) ?: return Result.failure(RuntimeException("[oidb] timeout"))
if (!fromServiceMsg.isSuccess) { if (fromServiceMsg.wupBuffer == null) {
return Result.failure(RuntimeException("[oidb] failed")) return Result.failure(RuntimeException("[oidb] failed"))
} }
val body = oidb_sso.OIDBSSOPkg() val body = oidb_sso.OIDBSSOPkg()
@ -291,7 +291,7 @@ internal object GroupHelper: QQInterfaces() {
uint64_uin.set(app.longAccountUin) uint64_uin.set(app.longAccountUin)
uint64_group_code.set(groupId) uint64_group_code.set(groupId)
}.toByteArray(), trpc = true) ?: return Result.failure(RuntimeException("[oidb] timeout")) }.toByteArray(), trpc = true) ?: return Result.failure(RuntimeException("[oidb] timeout"))
if (!fromServiceMsg.isSuccess) { if (fromServiceMsg.wupBuffer == null) {
return Result.failure(RuntimeException("[oidb] failed")) return Result.failure(RuntimeException("[oidb] failed"))
} }
val body = oidb_sso.OIDBSSOPkg() val body = oidb_sso.OIDBSSOPkg()
@ -311,7 +311,7 @@ internal object GroupHelper: QQInterfaces() {
toServiceMsg.extraData.putBoolean("is_admin", false) toServiceMsg.extraData.putBoolean("is_admin", false)
toServiceMsg.extraData.putInt("from", 0) toServiceMsg.extraData.putInt("from", 0)
val fromServiceMsg = sendToServiceMsgAW(toServiceMsg) ?: return@timeout Result.failure(Exception("获取群信息超时")) val fromServiceMsg = sendToServiceMsgAW(toServiceMsg) ?: return@timeout Result.failure(Exception("获取群信息超时"))
if (!fromServiceMsg.isSuccess) { if (fromServiceMsg.wupBuffer == null) {
return@timeout Result.failure(Exception("获取群信息失败")) return@timeout Result.failure(Exception("获取群信息失败"))
} }
val uniPacket = UniPacket(true) val uniPacket = UniPacket(true)
@ -393,7 +393,7 @@ internal object GroupHelper: QQInterfaces() {
req.uint32_client_type.set(1) req.uint32_client_type.set(1)
req.uint32_rich_card_name_ver.set(1) req.uint32_rich_card_name_ver.set(1)
val fromServiceMsg = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray()) 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() val rsp = group_member_info.RspBody()
rsp.mergeFrom(fromServiceMsg.wupBuffer.slice(4)) rsp.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
if (rsp.msg_meminfo.str_location.has()) { if (rsp.msg_meminfo.str_location.has()) {

View File

@ -1,49 +1,21 @@
@file:OptIn(DelicateCoroutinesApi::class)
package qq.service.internals 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.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.kernel.nativeinterface.MsgSetting import kotlinx.coroutines.DelicateCoroutinesApi
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.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
import qq.service.kernel.SimpleKernelMsgListener
import qq.service.msg.MessageHelper import qq.service.msg.MessageHelper
object AioListener: IKernelMsgListener { object AioListener: SimpleKernelMsgListener() {
override fun onRecvMsg(msgs: ArrayList<MsgRecord>) { override fun onRecvMsg(records: ArrayList<MsgRecord>) {
msgs.forEach { records.forEach {
GlobalScope.launch { GlobalScope.launch {
try { try {
onMsg(it) onMsg(it)
@ -103,275 +75,4 @@ object AioListener: IKernelMsgListener {
else -> LogCenter.log("不支持PUSH事件: ${record.chatType}") else -> LogCenter.log("不支持PUSH事件: ${record.chatType}")
} }
} }
override fun onMsgRecall(chatType: Int, peerId: String, msgId: Long) {
LogCenter.log("onMsgRecall($chatType, $peerId, $msgId)")
}
override fun onAddSendMsg(record: MsgRecord) {
}
override fun onMsgInfoListUpdate(msgList: ArrayList<MsgRecord>?) {
}
override fun onTempChatInfoUpdate(tempChatInfo: TempChatInfo) {
}
override fun onMsgAbstractUpdate(arrayList: ArrayList<MsgAbstract>?) {
//arrayList?.forEach {
// LogCenter.log("onMsgAbstractUpdate($it)", Level.WARN)
//}
}
override fun onRecvMsgSvrRspTransInfo(
j2: Long,
contact: Contact?,
i2: Int,
i3: Int,
str: String?,
bArr: ByteArray?
) {
LogCenter.log("onRecvMsgSvrRspTransInfo($j2, $contact, $i2, $i3, $str)", Level.DEBUG)
}
override fun onRecvS2CMsg(arrayList: ArrayList<Byte>?) {
LogCenter.log("onRecvS2CMsg(${arrayList.toString()})", Level.DEBUG)
}
override fun onRecvSysMsg(arrayList: ArrayList<Byte>?) {
LogCenter.log("onRecvSysMsg(${arrayList.toString()})", Level.DEBUG)
}
override fun onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {}
override fun onBroadcastHelperProgerssUpdate(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {}
override fun onChannelFreqLimitInfoUpdate(
contact: Contact?,
z: Boolean,
freqLimitInfo: FreqLimitInfo?
) {
}
override fun onContactUnreadCntUpdate(unreadMap: HashMap<Int, HashMap<String, UnreadCntInfo>>) {
// 推送未读消息数量
}
override fun onCustomWithdrawConfigUpdate(customWithdrawConfig: CustomWithdrawConfig?) {
LogCenter.log("onCustomWithdrawConfigUpdate: " + customWithdrawConfig.toString(), Level.DEBUG)
}
override fun onDraftUpdate(contact: Contact?, arrayList: ArrayList<MsgElement>?, j2: Long) {
LogCenter.log("onDraftUpdate: " + contact.toString() + "|" + arrayList + "|" + j2.toString(), Level.DEBUG)
}
override fun onEmojiDownloadComplete(emojiNotifyInfo: EmojiNotifyInfo?) {
}
override fun onEmojiResourceUpdate(emojiResourceInfo: EmojiResourceInfo?) {
}
override fun onFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
}
override fun onFileMsgCome(arrayList: ArrayList<MsgRecord>?) {
}
override fun onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
}
override fun onFirstViewGroupGuildMapping(arrayList: ArrayList<FirstViewGroupGuildInfo>?) {
}
override fun onGrabPasswordRedBag(
i2: Int,
str: String?,
i3: Int,
recvdOrder: RecvdOrder?,
msgRecord: MsgRecord?
) {
}
override fun onKickedOffLine(kickedInfo: KickedInfo?) {
LogCenter.log("onKickedOffLine($kickedInfo)")
}
override fun onRichMediaUploadComplete(notifyInfo: FileTransNotifyInfo) {
LogCenter.log({ "[BDH] 资源上传完成(${notifyInfo.trasferStatus}, ${notifyInfo.fileId}, ${notifyInfo.msgId}, ${notifyInfo.commonFileInfo})" }, Level.DEBUG)
}
override fun onRecvOnlineFileMsg(arrayList: ArrayList<MsgRecord>?) {
LogCenter.log(("onRecvOnlineFileMsg" + arrayList?.joinToString { ", " }), Level.DEBUG)
}
override fun onRichMediaDownloadComplete(fileTransNotifyInfo: FileTransNotifyInfo) {
}
override fun onRichMediaProgerssUpdate(fileTransNotifyInfo: FileTransNotifyInfo) {
}
override fun onSearchGroupFileInfoUpdate(searchGroupFileResult: SearchGroupFileResult?) {
LogCenter.log("onSearchGroupFileInfoUpdate($searchGroupFileResult)", Level.DEBUG)
}
override fun onGroupFileInfoAdd(groupItem: GroupItem?) {
LogCenter.log("onGroupFileInfoAdd: " + groupItem.toString(), Level.DEBUG)
}
override fun onGroupFileInfoUpdate(groupFileListResult: GroupFileListResult?) {
LogCenter.log("onGroupFileInfoUpdate: " + groupFileListResult.toString(), Level.DEBUG)
}
override fun onGroupGuildUpdate(groupGuildNotifyInfo: GroupGuildNotifyInfo?) {
LogCenter.log("onGroupGuildUpdate: " + groupGuildNotifyInfo.toString(), Level.DEBUG)
}
override fun onGroupTransferInfoAdd(groupItem: GroupItem?) {
LogCenter.log("onGroupTransferInfoAdd: " + groupItem.toString(), Level.DEBUG)
}
override fun onGroupTransferInfoUpdate(groupFileListResult: GroupFileListResult?) {
LogCenter.log("onGroupTransferInfoUpdate: " + groupFileListResult.toString(), Level.DEBUG)
}
override fun onGuildInteractiveUpdate(guildInteractiveNotificationItem: GuildInteractiveNotificationItem?) {
}
override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) {
}
override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) {
}
override fun onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: DownloadRelateEmojiResultInfo?) {
}
override fun onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: HitRelatedEmojiWordsResult?) {
}
override fun onHitRelatedEmojiResult(relatedWordEmojiInfo: RelatedWordEmojiInfo?) {
}
override fun onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: ImportOldDbMsgNotifyInfo?) {
}
override fun onInputStatusPush(inputStatusInfo: InputStatusInfo?) {
}
override fun onLineDev(devList: ArrayList<DevInfo>?) {
//LogCenter.log("onLineDev($arrayList)")
}
override fun onLogLevelChanged(newLevel: Long) {
}
override fun onMsgBoxChanged(arrayList: ArrayList<ContactMsgBoxInfo>?) {
}
override fun onMsgDelete(contact: Contact?, arrayList: ArrayList<Long>?) {
}
override fun onMsgEventListUpdate(hashMap: HashMap<String, ArrayList<Long>>?) {
}
override fun onMsgInfoListAdd(arrayList: ArrayList<MsgRecord>?) {
}
override fun onMsgQRCodeStatusChanged(i2: Int) {
}
override fun onMsgSecurityNotify(msgRecord: MsgRecord?) {
LogCenter.log("onMsgSecurityNotify($msgRecord)")
}
override fun onMsgSettingUpdate(msgSetting: MsgSetting?) {
}
override fun onNtFirstViewMsgSyncEnd() {
}
override fun onNtMsgSyncEnd() {
LogCenter.log("NTKernel同步消息完成", Level.DEBUG)
}
override fun onNtMsgSyncStart() {
LogCenter.log("NTKernel同步消息开始", Level.DEBUG)
}
override fun onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
}
override fun onRecvGroupGuildFlag(i2: Int) {
}
override fun onRecvUDCFlag(i2: Int) {
LogCenter.log("onRecvUDCFlag($i2)", Level.DEBUG)
}
override fun onSendMsgError(j2: Long, contact: Contact?, i2: Int, str: String?) {
LogCenter.log("onSendMsgError($j2, $contact, $j2, $str)", Level.DEBUG)
}
override fun onSysMsgNotification(i2: Int, j2: Long, j3: Long, arrayList: ArrayList<Byte>?) {
LogCenter.log("onSysMsgNotification($i2, $j2, $j3, $arrayList)", Level.DEBUG)
}
override fun onUnreadCntAfterFirstView(hashMap: HashMap<Int, ArrayList<UnreadCntInfo>>?) {
}
override fun onUnreadCntUpdate(hashMap: HashMap<Int, ArrayList<UnreadCntInfo>>?) {
}
override fun onUserChannelTabStatusChanged(z: Boolean) {
}
override fun onUserOnlineStatusChanged(z: Boolean) {
}
override fun onUserTabStatusChanged(arrayList: ArrayList<TabStatusInfo>?) {
LogCenter.log("onUserTabStatusChanged($arrayList)", Level.DEBUG)
}
override fun onlineStatusBigIconDownloadPush(i2: Int, j2: Long, str: String?) {
}
override fun onlineStatusSmallIconDownloadPush(i2: Int, j2: Long, str: String?) {
}
} }

View File

@ -0,0 +1,316 @@
package qq.service.kernel
import com.tencent.qqnt.kernel.nativeinterface.BroadcastHelperTransNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.ContactMsgBoxInfo
import com.tencent.qqnt.kernel.nativeinterface.CustomWithdrawConfig
import com.tencent.qqnt.kernel.nativeinterface.DevInfo
import com.tencent.qqnt.kernel.nativeinterface.DownloadRelateEmojiResultInfo
import com.tencent.qqnt.kernel.nativeinterface.EmojiNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.EmojiResourceInfo
import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.FirstViewDirectMsgNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.FirstViewGroupGuildInfo
import com.tencent.qqnt.kernel.nativeinterface.FreqLimitInfo
import com.tencent.qqnt.kernel.nativeinterface.GroupFileListResult
import com.tencent.qqnt.kernel.nativeinterface.GroupGuildNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.GroupItem
import com.tencent.qqnt.kernel.nativeinterface.GuildInteractiveNotificationItem
import com.tencent.qqnt.kernel.nativeinterface.GuildMsgAbFlag
import com.tencent.qqnt.kernel.nativeinterface.GuildNotificationAbstractInfo
import com.tencent.qqnt.kernel.nativeinterface.HitRelatedEmojiWordsResult
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener
import com.tencent.qqnt.kernel.nativeinterface.ImportOldDbMsgNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.InputStatusInfo
import com.tencent.qqnt.kernel.nativeinterface.KickedInfo
import com.tencent.qqnt.kernel.nativeinterface.MsgAbstract
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.kernel.nativeinterface.MsgSetting
import com.tencent.qqnt.kernel.nativeinterface.RecvdOrder
import com.tencent.qqnt.kernel.nativeinterface.RelatedWordEmojiInfo
import com.tencent.qqnt.kernel.nativeinterface.SearchGroupFileResult
import com.tencent.qqnt.kernel.nativeinterface.TabStatusInfo
import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
import com.tencent.qqnt.kernel.nativeinterface.UnreadCntInfo
import java.util.ArrayList
import java.util.HashMap
abstract class SimpleKernelMsgListener: IKernelMsgListener {
override fun onAddSendMsg(msgRecord: MsgRecord?) {
}
override fun onBroadcastHelperDownloadComplete(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {
}
override fun onBroadcastHelperProgerssUpdate(broadcastHelperTransNotifyInfo: BroadcastHelperTransNotifyInfo?) {
}
override fun onChannelFreqLimitInfoUpdate(
contact: Contact?,
z: Boolean,
freqLimitInfo: FreqLimitInfo?
) {
}
override fun onContactUnreadCntUpdate(hashMap: HashMap<Int, HashMap<String, UnreadCntInfo>>?) {
}
override fun onCustomWithdrawConfigUpdate(customWithdrawConfig: CustomWithdrawConfig?) {
}
override fun onDraftUpdate(contact: Contact?, arrayList: ArrayList<MsgElement>?, j2: Long) {
}
override fun onEmojiDownloadComplete(emojiNotifyInfo: EmojiNotifyInfo?) {
}
override fun onEmojiResourceUpdate(emojiResourceInfo: EmojiResourceInfo?) {
}
override fun onFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
}
override fun onFileMsgCome(arrayList: ArrayList<MsgRecord>?) {
}
override fun onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
}
override fun onFirstViewGroupGuildMapping(arrayList: ArrayList<FirstViewGroupGuildInfo>?) {
}
override fun onGrabPasswordRedBag(
i2: Int,
str: String?,
i3: Int,
recvdOrder: RecvdOrder?,
msgRecord: MsgRecord?
) {
}
override fun onGroupFileInfoAdd(groupItem: GroupItem?) {
}
override fun onGroupFileInfoUpdate(groupFileListResult: GroupFileListResult?) {
}
override fun onGroupGuildUpdate(groupGuildNotifyInfo: GroupGuildNotifyInfo?) {
}
override fun onGroupTransferInfoAdd(groupItem: GroupItem?) {
}
override fun onGroupTransferInfoUpdate(groupFileListResult: GroupFileListResult?) {
}
override fun onGuildInteractiveUpdate(guildInteractiveNotificationItem: GuildInteractiveNotificationItem?) {
}
override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) {
}
override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) {
}
override fun onHitCsRelatedEmojiResult(downloadRelateEmojiResultInfo: DownloadRelateEmojiResultInfo?) {
}
override fun onHitEmojiKeywordResult(hitRelatedEmojiWordsResult: HitRelatedEmojiWordsResult?) {
}
override fun onHitRelatedEmojiResult(relatedWordEmojiInfo: RelatedWordEmojiInfo?) {
}
override fun onImportOldDbProgressUpdate(importOldDbMsgNotifyInfo: ImportOldDbMsgNotifyInfo?) {
}
override fun onInputStatusPush(inputStatusInfo: InputStatusInfo?) {
}
override fun onKickedOffLine(kickedInfo: KickedInfo?) {
}
override fun onLineDev(arrayList: ArrayList<DevInfo>?) {
}
override fun onLogLevelChanged(j2: Long) {
}
override fun onMsgAbstractUpdate(arrayList: ArrayList<MsgAbstract>?) {
}
override fun onMsgBoxChanged(arrayList: ArrayList<ContactMsgBoxInfo>?) {
}
override fun onMsgDelete(contact: Contact?, arrayList: ArrayList<Long>?) {
}
override fun onMsgEventListUpdate(hashMap: HashMap<String, ArrayList<Long>>?) {
}
override fun onMsgInfoListAdd(arrayList: ArrayList<MsgRecord>?) {
}
override fun onMsgInfoListUpdate(arrayList: ArrayList<MsgRecord>?) {
}
override fun onMsgQRCodeStatusChanged(i2: Int) {
}
override fun onMsgRecall(i2: Int, str: String?, j2: Long) {
}
override fun onMsgSecurityNotify(msgRecord: MsgRecord?) {
}
override fun onMsgSettingUpdate(msgSetting: MsgSetting?) {
}
override fun onNtFirstViewMsgSyncEnd() {
}
override fun onNtMsgSyncEnd() {
}
override fun onNtMsgSyncStart() {
}
override fun onReadFeedEventUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
}
override fun onRecvGroupGuildFlag(i2: Int) {
}
override fun onRecvMsg(records: ArrayList<MsgRecord>) {
}
override fun onRecvMsgSvrRspTransInfo(
j2: Long,
contact: Contact?,
i2: Int,
i3: Int,
str: String?,
bArr: ByteArray?
) {
}
override fun onRecvOnlineFileMsg(arrayList: ArrayList<MsgRecord>?) {
}
override fun onRecvS2CMsg(arrayList: ArrayList<Byte>?) {
}
override fun onRecvSysMsg(arrayList: ArrayList<Byte>?) {
}
override fun onRecvUDCFlag(i2: Int) {
}
override fun onRichMediaDownloadComplete(fileTransNotifyInfo: FileTransNotifyInfo?) {
}
override fun onRichMediaProgerssUpdate(fileTransNotifyInfo: FileTransNotifyInfo?) {
}
override fun onRichMediaUploadComplete(fileTransNotifyInfo: FileTransNotifyInfo) {
}
override fun onSearchGroupFileInfoUpdate(searchGroupFileResult: SearchGroupFileResult?) {
}
override fun onSendMsgError(j2: Long, contact: Contact?, i2: Int, str: String?) {
}
override fun onSysMsgNotification(i2: Int, j2: Long, j3: Long, arrayList: ArrayList<Byte>?) {
}
override fun onTempChatInfoUpdate(tempChatInfo: TempChatInfo?) {
}
override fun onUnreadCntAfterFirstView(hashMap: HashMap<Int, ArrayList<UnreadCntInfo>>?) {
}
override fun onUnreadCntUpdate(hashMap: HashMap<Int, ArrayList<UnreadCntInfo>>?) {
}
override fun onUserChannelTabStatusChanged(z: Boolean) {
}
override fun onUserOnlineStatusChanged(z: Boolean) {
}
override fun onUserTabStatusChanged(arrayList: ArrayList<TabStatusInfo>?) {
}
override fun onlineStatusBigIconDownloadPush(i2: Int, j2: Long, str: String?) {
}
override fun onlineStatusSmallIconDownloadPush(i2: Int, j2: Long, str: String?) {
}
}

View File

@ -1,12 +1,15 @@
package qq.service.msg package qq.service.msg
import com.tencent.qqnt.kernel.api.IKernelService 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 com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import qq.service.QQInterfaces import qq.service.QQInterfaces
import qq.service.contact.ContactHelper
import qq.service.internals.msgService import qq.service.internals.msgService
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -28,4 +31,25 @@ internal object MessageHelper: QQInterfaces() {
} ?: return Result.failure(Exception("获取临时会话信息失败")) } ?: return Result.failure(Exception("获取临时会话信息失败"))
return Result.success(info) 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
} }

View File

@ -0,0 +1,405 @@
package qq.service.msg
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import io.kritor.event.AtElement
import io.kritor.event.Element
import io.kritor.event.ElementKt
import io.kritor.event.ImageType
import io.kritor.event.Scene
import io.kritor.event.atElement
import io.kritor.event.basketballElement
import io.kritor.event.buttonAction
import io.kritor.event.buttonActionPermission
import io.kritor.event.buttonRender
import io.kritor.event.contactElement
import io.kritor.event.diceElement
import io.kritor.event.faceElement
import io.kritor.event.forwardElement
import io.kritor.event.imageElement
import io.kritor.event.jsonElement
import io.kritor.event.locationElement
import io.kritor.event.pokeElement
import io.kritor.event.rpsElement
import io.kritor.event.textElement
import io.kritor.event.videoElement
import io.kritor.event.voiceElement
import moe.fuqiuluo.shamrock.helper.ActionMsgException
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.db.ImageDB
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
import moe.fuqiuluo.shamrock.tools.asJsonArray
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import moe.fuqiuluo.shamrock.utils.PlatformUtils.QQ_9_0_8_VER
import qq.service.bdh.RichProtoSvc
import qq.service.contact.ContactHelper
typealias NtMessages = ArrayList<MsgElement>
typealias Convertor = suspend (MsgRecord, MsgElement) -> Result<Element>
suspend fun NtMessages.toKritorMessages(record: MsgRecord): ArrayList<Element> {
val result = arrayListOf<Element>()
forEach {
MsgConvertor[it.elementType]?.invoke(record, it)?.onSuccess {
result.add(it)
}?.onFailure {
if (it !is ActionMsgException) {
LogCenter.log("消息转换异常: " + it.stackTraceToString(), Level.WARN)
}
}
}
return result
}
private object MsgConvertor {
private val convertorMap = hashMapOf(
MsgConstant.KELEMTYPETEXT to ::convertText,
MsgConstant.KELEMTYPEFACE to ::convertFace,
MsgConstant.KELEMTYPEPIC to ::convertImage,
MsgConstant.KELEMTYPEPTT to ::convertVoice,
MsgConstant.KELEMTYPEVIDEO to ::convertVideo,
MsgConstant.KELEMTYPEMARKETFACE to ::convertMarketFace,
MsgConstant.KELEMTYPEARKSTRUCT to ::convertStructJson,
MsgConstant.KELEMTYPEREPLY to ::convertReply,
//MsgConstant.KELEMTYPEGRAYTIP to ::convertGrayTips,
MsgConstant.KELEMTYPEFILE to ::convertFile,
MsgConstant.KELEMTYPEMARKDOWN to ::convertMarkdown,
//MsgConstant.KELEMTYPEMULTIFORWARD to MsgElementConverter::convertXmlMultiMsgElem,
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MsgElementConverter::convertXmlLongMsgElem,
MsgConstant.KELEMTYPEFACEBUBBLE to ::convertBubbleFace,
MsgConstant.KELEMTYPEINLINEKEYBOARD to ::convertInlineKeyboard
)
suspend fun convertText(record: MsgRecord, element: MsgElement): Result<Element> {
val text = element.textElement
val elem = Element.newBuilder()
if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
elem.setAt(atElement {
this.uid = text.atNtUid
this.uin = ContactHelper.getUinByUidAsync(text.atNtUid).toLong()
})
} else {
elem.setText(textElement {
this.text = text.content
})
}
return Result.success(elem.build())
}
suspend fun convertFace(record: MsgRecord, element: MsgElement): Result<Element> {
val face = element.faceElement
val elem = Element.newBuilder()
if (face.faceType == 5) {
elem.setPoke(pokeElement {
this.id = face.vaspokeId
this.type = face.pokeType
this.strength = face.pokeStrength
})
} else {
when(face.faceIndex) {
114 -> elem.setBasketball(basketballElement {
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
})
358 -> elem.setDice(diceElement {
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
})
359 -> elem.setRps(rpsElement {
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
})
394 -> elem.setFace(faceElement {
this.id = face.faceIndex
this.isBig = face.faceType == 3
this.result = face.resultId.ifNullOrEmpty { "1" }?.toInt() ?: 1
})
else -> elem.setFace(faceElement {
this.id = face.faceIndex
this.isBig = face.faceType == 3
})
}
}
return Result.success(elem.build())
}
suspend fun convertImage(record: MsgRecord, element: MsgElement): Result<Element> {
val image = element.picElement
val md5 = (image.md5HexStr ?: image.fileName
.replace("{", "")
.replace("}", "")
.replace("-", "").split(".")[0])
.uppercase()
var storeId = 0
if (PlatformUtils.getQQVersionCode() > QQ_9_0_8_VER) {
storeId = image.storeID
}
ImageDB.getInstance().imageMappingDao().insert(
ImageMapping(
fileName = md5,
md5 = md5,
chatType = record.chatType,
size = image.fileSize,
sha = "",
fileId = image.fileUuid,
storeId = storeId,
)
)
val originalUrl = image.originImageUrl ?: ""
LogCenter.log({ "receive image: $image" }, Level.DEBUG)
val elem = Element.newBuilder()
elem.setImage(imageElement {
this.file = md5
this.url = when (record.chatType) {
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
originalUrl = originalUrl,
md5 = md5,
fileId = image.fileUuid,
width = image.picWidth.toUInt(),
height = image.picHeight.toUInt(),
sha = "",
fileSize = image.fileSize.toULong(),
peer = record.peerUin.toString()
)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(
originalUrl = originalUrl,
md5 = md5,
fileId = image.fileUuid,
width = image.picWidth.toUInt(),
height = image.picHeight.toUInt(),
sha = "",
fileSize = image.fileSize.toULong(),
peer = record.senderUin.toString(),
storeId = storeId
)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(
originalUrl = originalUrl,
md5 = md5,
fileId = image.fileUuid,
width = image.picWidth.toUInt(),
height = image.picHeight.toUInt(),
sha = "",
fileSize = image.fileSize.toULong(),
peer = record.channelId.ifNullOrEmpty { record.peerUin.toString() } ?: "0",
subPeer = record.guildId ?: "0"
)
else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}")
}
this.type = if (image.isFlashPic == true) ImageType.FLASH else if (image.original) ImageType.ORIGIN else ImageType.COMMON
this.subType = image.picSubType
})
return Result.success(elem.build())
}
suspend fun convertVoice(record: MsgRecord, element: MsgElement): Result<Element> {
val ptt = element.pttElement
val elem = Element.newBuilder()
val md5 = if (ptt.fileName.startsWith("silk"))
ptt.fileName.substring(5)
else ptt.md5HexStr
elem.setVoice(voiceElement {
this.url = when (record.chatType) {
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", ptt.fileUuid)
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl("0", md5.hex2ByteArray(), ptt.fileUuid)
else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}")
}
this.file = md5
this.magic = ptt.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE
})
return Result.success(elem.build())
}
suspend fun convertVideo(record: MsgRecord, element: MsgElement): Result<Element> {
val video = element.videoElement
val elem = Element.newBuilder()
val md5 = if (video.fileName.contains("/")) {
video.videoMd5.takeIf {
!it.isNullOrEmpty()
}?.hex2ByteArray() ?: video.fileName.split("/").let {
it[it.size - 2].hex2ByteArray()
}
} else video.fileName.split(".")[0].hex2ByteArray()
elem.setVideo(videoElement {
this.file = md5.toHexString()
this.url = when (record.chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
else -> throw UnsupportedOperationException("Not supported chat type: ${record.chatType}")
}
})
return Result.success(elem.build())
}
suspend fun convertMarketFace(record: MsgRecord, element: MsgElement): Result<Element> {
val marketFace = element.marketFaceElement
val elem = Element.newBuilder()
elem.setMarketFace(io.kritor.event.marketFaceElement {
this.id = marketFace.emojiId.lowercase()
})
return Result.success(elem.build())
}
suspend fun convertStructJson(record: MsgRecord, element: MsgElement): Result<Element> {
val data = element.arkElement.bytesData.asJsonObject
val elem = Element.newBuilder()
when (data["app"].asString) {
"com.tencent.multimsg" -> {
val info = data["meta"].asJsonObject["detail"].asJsonObject
elem.setForward(forwardElement {
this.id = info["resid"].asString
this.uniseq = info["uniseq"].asString
this.summary = info["summary"].asString
this.description = info["news"].asJsonArray.joinToString("\n") {
it.asJsonObject["text"].asString
}
})
}
"com.tencent.troopsharecard" -> {
val info = data["meta"].asJsonObject["contact"].asJsonObject
elem.setContact(contactElement {
this.scene = Scene.GROUP
this.peer = info["jumpUrl"].asString.split("group_code=")[1]
})
}
"com.tencent.contact.lua" -> {
val info = data["meta"].asJsonObject["contact"].asJsonObject
elem.setContact(contactElement {
this.scene = Scene.FRIEND
this.peer = info["jumpUrl"].asString.split("uin=")[1]
})
}
"com.tencent.map" -> {
val info = data["meta"].asJsonObject["Location.Search"].asJsonObject
elem.setLocation(locationElement {
this.lat = info["lat"].asString.toFloat()
this.lon = info["lng"].asString.toFloat()
this.address = info["address"].asString
this.title = info["name"].asString
})
}
else -> elem.setJson(jsonElement {
this.json = data.toString()
})
}
return Result.success(elem.build())
}
suspend fun convertReply(record: MsgRecord, element: MsgElement): Result<Element> {
val reply = element.replyElement
val elem = Element.newBuilder()
elem.setReply(io.kritor.event.replyElement {
this.messageId = reply.replayMsgId
})
return Result.success(elem.build())
}
suspend fun convertFile(record: MsgRecord, element: MsgElement): Result<Element> {
val fileMsg = element.fileElement
val fileName = fileMsg.fileName
val fileSize = fileMsg.fileSize
val expireTime = fileMsg.expireTime ?: 0
val fileId = fileMsg.fileUuid
val bizId = fileMsg.fileBizId ?: 0
val fileSubId = fileMsg.fileSubId ?: ""
val url = when (record.chatType) {
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(record.guildId, record.channelId, fileId, bizId)
else -> RichProtoSvc.getGroupFileDownUrl(record.peerUin, fileId, bizId)
}
val elem = Element.newBuilder()
elem.setFile(io.kritor.event.fileElement {
this.name = fileName
this.size = fileSize
this.url = url
this.expireTime = expireTime
this.id = fileId
this.subId = fileSubId
this.biz = bizId
})
return Result.success(elem.build())
}
suspend fun convertMarkdown(record: MsgRecord, element: MsgElement): Result<Element> {
val markdown = element.markdownElement
val elem = Element.newBuilder()
elem.setMarkdown(io.kritor.event.markdownElement {
this.markdown = markdown.content
})
return Result.success(elem.build())
}
suspend fun convertBubbleFace(record: MsgRecord, element: MsgElement): Result<Element> {
val bubbleFace = element.faceBubbleElement
val elem = Element.newBuilder()
elem.setBubbleFace(io.kritor.event.bubbleFaceElement {
this.id = bubbleFace.yellowFaceInfo.index
this.count = bubbleFace.faceCount ?: 1
})
return Result.success(elem.build())
}
suspend fun convertInlineKeyboard(record: MsgRecord, element: MsgElement): Result<Element> {
val inlineKeyboard = element.inlineKeyboardElement
val elem = Element.newBuilder()
elem.setButton(io.kritor.event.buttonElement {
inlineKeyboard.rows.forEach { row ->
this.rows.add(io.kritor.event.row {
row.buttons.forEach buttonsLoop@ { button ->
if (button == null) return@buttonsLoop
this.buttons.add(io.kritor.event.button {
this.id = button.id
this.action = buttonAction {
this.type = button.type
this.permission = buttonActionPermission {
this.type = button.permissionType
button.specifyRoleIds?.let {
this.roleIds.addAll(it)
}
button.specifyTinyids?.let {
this.userIds.addAll(it)
}
}
this.unsupportedTips = button.unsupportTips ?: ""
this.data = button.data ?: ""
this.reply = button.isReply
this.enter = button.enter
}
this.renderData = buttonRender {
this.label = button.label ?: ""
this.visitedLabel = button.visitedLabel ?: ""
this.style = button.style
}
})
}
})
}
})
return Result.success(elem.build())
}
operator fun get(case: Int): Convertor? {
return convertorMap[case]
}
}