45 Commits

Author SHA1 Message Date
042f4bd330 fix build err 2024-04-04 19:44:56 +08:00
9aef71b09f fix build err 2024-04-04 18:56:48 +08:00
9cbe755520 fix missing elem-type for kritor 2024-04-04 18:51:58 +08:00
df02f9f872 fix #316 2024-03-28 19:37:10 +08:00
5cbb695a66 fix crash 2024-03-28 19:27:11 +08:00
c014e85faa update kritor 2024-03-27 16:21:49 +08:00
4a396b0935 Update kritor 2024-03-25 01:12:59 +08:00
d59fcf9f6a update kritor 2024-03-25 01:10:33 +08:00
cdc664f44a fix build error 2024-03-24 05:33:57 +08:00
ad313f384c fix kritor 2024-03-24 05:19:50 +08:00
b6a510ce05 Update .gitmodules 2024-03-21 19:35:16 +08:00
bed5947909 update kritor 2024-03-21 19:15:58 +08:00
fb6578d243 chore: try to fix ci 2024-03-21 17:50:39 +08:00
d33cace7aa Shamrock: forward messages resources upload 2024-03-21 17:39:51 +08:00
659d4e5da4 commit Readme.md 2024-03-21 16:21:54 +08:00
ac2aee8c0e 関連プロジェクトのヒントを追加する 2024-03-21 16:17:25 +08:00
0faada7b5a Merge pull request #310 from whitechi73/kritor
kritorをmasterブランチに設定する
2024-03-21 16:15:12 +08:00
680317da13 kritorをmasterブランチに設定する
kritorをmasterブランチに設定する
2024-03-21 16:13:45 +08:00
7782feb6ac Merge pull request #303 from tobycroft/master
GetFile的type新增gzip,会将数据流压缩后再b64降低带宽占用
2024-03-18 22:43:18 +08:00
d66358a1f3 Shamrock: 提供开发者服务支持
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-18 21:02:05 +08:00
824f280b3a 修改error 提示 2024-03-18 15:40:14 +08:00
6936262d62 GetFile的type新增gzip,会将数据流压缩后再b64降低带宽占用
- 使用gzip压缩
2024-03-18 15:24:24 +08:00
0955267ee5 Merge branch 'whitechi73:master' into master 2024-03-18 13:56:26 +08:00
f3da62fa74 GetFile的type新增gzip,会将数据流压缩后再b64降低带宽占用
- 使用gzip压缩
2024-03-18 13:49:03 +08:00
abbac6315c Merge pull request #301 from tobycroft/master
新增get_file方法(算是补全下功能)
2024-03-18 13:34:51 +08:00
0cf10eabd6 fix: set field file_type not required 2024-03-18 13:34:13 +08:00
8c33267887 fileType加入空匹配,可支持空传 2024-03-18 13:30:18 +08:00
f030104ff2 get_record的ws加入单独的md5字段,方便后续get_file拿文件 2024-03-18 13:23:20 +08:00
ee5fcc3403 Shamrock: 精华消息支持
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-18 11:49:38 +08:00
5e819179b4 get_record的ws加入单独的md5字段,方便后续get_file拿文件 2024-03-18 04:08:07 +08:00
ea206faf4f get_record的ws加入单独的md5字段,方便后续get_file拿文件 2024-03-18 04:07:11 +08:00
5adfc544a2 修正file_type参数不正确问题 2024-03-18 03:46:51 +08:00
bdb75841cf AGP更新 2024-03-18 03:23:11 +08:00
a3dc0d06b2 新增get_file方法,主要解决使用反向websocket的时候获取文件麻烦的问题,目前仅支持base64的type返回,未来将支持更多模式,测试后将发布至文档 2024-03-18 03:17:36 +08:00
3664352f23 新增get_file方法,主要解决使用反向websocket的时候获取文件麻烦的问题,目前仅支持base64的type返回,未来将支持更多模式,测试后将发布至文档 2024-03-18 03:05:28 +08:00
2770979fee 新增get_file方法,主要解决使用反向websocket的时候获取文件麻烦的问题,目前仅支持base64的type返回,未来将支持更多模式,测试后将发布至文档 2024-03-18 02:57:44 +08:00
6c9b282c6a Shamrock: 实现消息服务
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-16 19:39:45 +08:00
3a07116093 Shamrock: 实现事件推送
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-15 01:37:28 +08:00
be58c368e9 Merge pull request #295 from tobycroft/master
notice类消息,新增source字段
2024-03-15 00:34:42 +08:00
a95d8d85e8 Shamrock: 完成消息推送
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-03-14 20:33:30 +08:00
1d035fa378 Shamrock: fix 群聊和私聊转发分别处理 2024-03-14 18:57:45 +08:00
7d0b60271e Shamrock: fix 群聊转发图片 2024-03-14 18:25:01 +08:00
d38777d06a notice类消息,新增source,解决poke等特殊消息没有办法直接判断消息来源的问题。修改后通过notice类消息的source字段,则可判断需要使用那种struct来接收 2024-03-14 15:05:01 +08:00
93c49953cf Merge pull request #289 from Linwenxuan05/patch-1
添加相关项目
2024-03-11 11:45:56 +08:00
883e949cc1 添加相关项目 2024-03-11 11:41:46 +08:00
82 changed files with 8438 additions and 1596 deletions

View File

@ -19,7 +19,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
submodules: recursive
- name: Setup JDK 17
uses: actions/setup-java@v4

2
.gitmodules vendored
View File

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

View File

@ -16,20 +16,35 @@
## 简介
☘ 基于 Lsposed(**Non**-Riru) 实现 OneBot 标准的 QQ 机器人框架,原作者[**fuqiuluo**](https://github.com/fuqiuluo)已脱离开发接下来由白池接手哦本项目为OpenShamrock不会有任何收费行为欢迎大家的加入
☘ 基于 Lsposed(**Non**-Riru) 实现 Kritor 标准的 QQ 机器人框架!
> 本项目仅提供学习与交流用途请在24小时内删除。
> 本项目目的是研究 Xposed 和 LSPosed 框架的使用。 Epic 框架开发相关知识。
> Riru可能导致封禁请减少使用。
> 如有违反法律,请联系删除。
> 请勿在任何平台宣传,宣扬,转发本项目,请勿恶意修改企业安装包造成相关企业产生损失,如有违背,必将追责到底。
> 官方论坛,[点我直达](https://forum.libfekit.so/)
## 兼容|迁移|替代 说明
- 一键移植:本项目基于 go-cqhttp 的文档进行开发实现。
- 平行部署:可多平台部署,未来将会支持 Docker 部署的教程
- 替代方案:[Lagrange.Core](https://github.com/LagrangeDev/Lagrange.Core)
- 平行部署:可多平台部署。
## 相关项目
<table>
<tr>
<td><a href="https://github.com/LagrangeDev/Lagrange.Core">Lagrange.Core</a></td>
<td>NTQQ 的协议实现</td>
</tr>
<tr>
<td><a href="https://github.com/whitechi73/OpenShamrock">OpenShamrock</a></td>
<td>基于 Xposed 实现 OneBot 标准的机器人框架(👈你在这里</td>
</tr>
<tr>
<td><a href="https://github.com/chrononeko/chronocat">Chronocat</a></td>
<td>基于 Electron 的、模块化的 Satori 框架</td>
</tr>
</table>
## 权限声明

View File

@ -1,7 +1,9 @@
package kritor.service
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FUNCTION)
annotation class Grpc(
val serviceName: String,
val funcName: String
)
val funcName: String,
)

View File

@ -1,8 +0,0 @@
package moe.fuqiuluo.symbols
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
annotation class OneBotHandler(
val actionName: String,
val alias: Array<String> = []
)

View File

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

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

View File

@ -118,9 +118,7 @@ private fun APIInfoCard(
text = rpcAddress,
hint = "请输入回调地址",
error = "输入的地址不合法",
checker = {
it.isEmpty() || it.contains(":")
},
checker = { true },
confirm = {
ShamrockConfig[ctx, RPCAddress] = rpcAddress.value
AppRuntime.log("设置回调RPC地址为[${rpcAddress.value}]。")

View File

@ -1,5 +1,5 @@
plugins {
kotlin("jvm") version "1.9.21"
kotlin("jvm") version "1.9.22"
}
repositories {

View File

@ -15,8 +15,9 @@ fun ktor(target: String, name: String): String {
return "io.ktor:ktor-$target-$name:${Versions.ktorVersion}"
}
fun grpc(name: String, version: String) = "io.grpc:grpc-$name:$version"
object Versions {
const val roomVersion = "2.5.0"
const val ktorVersion = "2.3.3"
}

1
kritor

Submodule kritor deleted from e4aac653e1

42
kritor/.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

76
kritor/build.gradle.kts Normal file
View File

@ -0,0 +1,76 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.protobuf") version "0.9.4"
}
android {
namespace = "moe.whitechi73.kritor"
compileSdk = 34
defaultConfig {
minSdk = 24
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
protobuf(files("kritor/protos"))
implementation("com.google.protobuf:protobuf-java:4.26.0")
implementation(kotlinx("coroutines-core", "1.8.0"))
implementation(grpc("stub", "1.62.2"))
implementation(grpc("protobuf", "1.62.2"))
implementation(grpc("kotlin-stub", "1.4.1"))
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.26.0"
}
plugins {
create("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
}
create("grpckt") {
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar"
}
}
generateProtoTasks {
all().forEach {
it.plugins {
create("grpc")
create("grpckt")
}
it.builtins {
create("java")
}
}
}
}
tasks.withType<KotlinCompile>().configureEach {
kotlinOptions.freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
}

View File

1
kritor/kritor Submodule

Submodule kritor/kritor added at c49df3074c

21
kritor/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,83 @@
@file:Suppress("UNCHECKED_CAST")
@file:OptIn(KspExperimental::class)
package moe.fuqiuluo.ksp.impl
import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getAnnotationsByType
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.KSAnnotated
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import kritor.service.Grpc
class GrpcProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger
): SymbolProcessor {
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation(Grpc::class.qualifiedName!!)
val actions = (symbols as Sequence<KSFunctionDeclaration>).toList()
if (actions.isEmpty()) return emptyList()
// 怎么返回nullable的结果
val packageName = "kritor.handlers"
val funcBuilder = FunSpec.builder("handleGrpc")
.addModifiers(KModifier.SUSPEND)
.addParameter("cmd", String::class)
.addParameter("data", ByteArray::class)
.returns(ByteArray::class)
val fileSpec = FileSpec.scriptBuilder("AutoGrpcHandlers", packageName)
logger.warn("Found ${actions.size} grpc-actions")
//logger.error(resolver.getClassDeclarationByName("io.kritor.AuthReq").toString())
//logger.error(resolver.getJavaClassByName("io.kritor.AuthReq").toString())
//logger.error(resolver.getKotlinClassByName("io.kritor.AuthReq").toString())
actions.forEach { action ->
val methodName = action.qualifiedName?.asString()!!
val grpcMethod = action.getAnnotationsByType(Grpc::class).first()
val service = grpcMethod.serviceName
val funcName = grpcMethod.funcName
funcBuilder.addStatement("if (cmd == \"${service}.${funcName}\") {\t")
val reqType = action.parameters[0].type.toString()
val rspType = action.returnType.toString()
funcBuilder.addStatement("val resp: $rspType = $methodName($reqType.parseFrom(data))")
funcBuilder.addStatement("return resp.toByteArray()")
funcBuilder.addStatement("}")
}
funcBuilder.addStatement("return EMPTY_BYTE_ARRAY")
fileSpec
.addStatement("import io.kritor.authentication.*")
.addStatement("import io.kritor.core.*")
.addStatement("import io.kritor.customization.*")
.addStatement("import io.kritor.developer.*")
.addStatement("import io.kritor.file.*")
.addStatement("import io.kritor.friend.*")
.addStatement("import io.kritor.group.*")
.addStatement("import io.kritor.guild.*")
.addStatement("import io.kritor.message.*")
.addStatement("import io.kritor.web.*")
.addFunction(funcBuilder.build())
.addImport("moe.fuqiuluo.symbols", "EMPTY_BYTE_ARRAY")
runCatching {
codeGenerator.createNewFile(
dependencies = Dependencies(aggregating = false),
packageName = packageName,
fileName = fileSpec.name
).use { outputStream ->
outputStream.writer().use {
fileSpec.build().writeTo(it)
}
}
}
return emptyList()
}
}

View File

@ -0,0 +1,17 @@
package moe.fuqiuluo.ksp.providers
import com.google.auto.service.AutoService
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.processing.SymbolProcessorProvider
import moe.fuqiuluo.ksp.impl.GrpcProcessor
@AutoService(SymbolProcessorProvider::class)
class GrpcProvider: SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return GrpcProcessor(
environment.codeGenerator,
environment.logger
)
}
}

View File

@ -37,7 +37,6 @@ android {
}
dependencies {
//implementation(DEPENDENCY_PROTOBUF)
implementation(kotlinx("serialization-protobuf", "1.6.2"))
implementation(kotlinx("serialization-json", "1.6.2"))

View File

@ -13,7 +13,7 @@ data class ButtonExtra(
@Serializable
data class Object1(
@ProtoNumber(1) val rows: List<Row>? = null,
@ProtoNumber(2) val appid: Int? = null,
@ProtoNumber(2) val appid: ULong? = null,
)
@Serializable

View File

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

View File

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

View File

@ -26,7 +26,7 @@ buildscript {
}
}
dependencies {
classpath("com.android.tools:r8:8.2.47")
classpath("com.android.tools:r8:8.3.37")
}
}
@ -34,11 +34,9 @@ rootProject.name = "Shamrock"
include(
":app",
":xposed",
":qqinterface"
)
include(":protobuf")
include(":processor")
include(":annotations")
include(":kritor")
project(":kritor").projectDir = file("kritor/protos")
":qqinterface",
":protobuf",
":processor",
":annotations",
":kritor"
)

View File

@ -5,7 +5,6 @@ plugins {
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("com.google.devtools.ksp") version "1.9.22-1.0.17"
id("com.google.protobuf") version "0.9.4"
kotlin("plugin.serialization") version "1.9.22"
}
@ -61,11 +60,10 @@ kotlin {
}
dependencies {
compileOnly ("de.robv.android.xposed:api:82")
compileOnly (project(":qqinterface"))
protobuf(project(":kritor"))
compileOnly("de.robv.android.xposed:api:82")
compileOnly(project(":qqinterface"))
implementation(project(":kritor"))
implementation(project(":protobuf"))
implementation(project(":annotations"))
ksp(project(":processor"))
@ -75,24 +73,20 @@ dependencies {
DEPENDENCY_ANDROIDX.forEach {
implementation(it)
}
//implementation(DEPENDENCY_PROTOBUF)
implementation(room("runtime"))
kapt(room("compiler"))
implementation(room("ktx"))
implementation(kotlinx("io-jvm", "0.1.16"))
implementation(kotlinx("serialization-protobuf", "1.6.2"))
implementation(ktor("client", "core"))
implementation(ktor("client", "okhttp"))
implementation(ktor("serialization", "kotlinx-json"))
implementation("io.grpc:grpc-stub:1.62.2")
implementation("io.grpc:grpc-protobuf-lite:1.62.2")
implementation("com.google.protobuf:protobuf-kotlin-lite:3.25.3")
implementation("io.grpc:grpc-kotlin-stub:1.4.1")
implementation("io.grpc:grpc-okhttp:1.62.2")
implementation(grpc("protobuf", "1.62.2"))
implementation(grpc("kotlin-stub", "1.4.1"))
implementation(grpc("okhttp", "1.62.2"))
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
@ -106,40 +100,3 @@ tasks.withType<KotlinCompile>().all {
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn")
}
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.3"
}
plugins {
create("java") {
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
}
create("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
}
create("grpckt") {
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar"
}
}
generateProtoTasks {
all().forEach {
it.plugins {
create("java") {
option("lite")
}
create("grpc") {
option("lite")
}
create("grpckt") {
option("lite")
}
}
it.builtins {
create("kotlin") {
option("lite")
}
}
}
}
}

View File

@ -34,7 +34,7 @@ active_ticket=
enable_self_message=false
# 旧BDH兼容开关
enable_old_bdh=false
enable_old_bdh=true
# 反JVM调用栈跟踪
anti_jvm_trace=true

View File

@ -0,0 +1,140 @@
@file:OptIn(DelicateCoroutinesApi::class)
package kritor.client
import com.google.protobuf.ByteString
import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import io.kritor.common.Request
import io.kritor.common.Response
import io.kritor.event.EventServiceGrpcKt
import io.kritor.event.EventStructure
import io.kritor.event.EventType
import io.kritor.reverse.ReverseServiceGrpcKt
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.launch
import kritor.handlers.handleGrpc
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
import kotlin.time.Duration.Companion.seconds
internal class KritorClient(
val host: String,
val port: Int
) {
private lateinit var channel: ManagedChannel
private lateinit var channelJob: Job
private val senderChannel = MutableSharedFlow<Response>()
fun start() {
runCatching {
if (::channel.isInitialized && isActive()){
channel.shutdown()
}
channel = ManagedChannelBuilder
.forAddress(host, port)
.usePlaintext()
.enableRetry() // 允许尝试
.executor(Dispatchers.IO.asExecutor()) // 使用协程的调度器
.build()
}.onFailure {
LogCenter.log("KritorClient start failed: ${it.stackTraceToString()}", Level.ERROR)
}
}
fun listen(retryCnt: Int = -1) {
if (::channelJob.isInitialized && channelJob.isActive) {
channelJob.cancel()
}
channelJob = GlobalScope.launch(Dispatchers.IO) {
runCatching {
val stub = ReverseServiceGrpcKt.ReverseServiceCoroutineStub(channel)
registerEvent(EventType.EVENT_TYPE_MESSAGE)
registerEvent(EventType.EVENT_TYPE_CORE_EVENT)
registerEvent(EventType.EVENT_TYPE_REQUEST)
registerEvent(EventType.EVENT_TYPE_NOTICE)
stub.reverseStream(channelFlow {
senderChannel.collect { send(it) }
}).collect {
onReceive(it)
}
}.onFailure {
LogCenter.log("KritorClient listen failed, retry after 15s: ${it.stackTraceToString()}", Level.WARN)
}
delay(15.seconds)
LogCenter.log("KritorClient listen retrying, retryCnt = $retryCnt", Level.WARN)
if (retryCnt != 0) listen(retryCnt - 1)
}
}
fun registerEvent(eventType: EventType) {
GlobalScope.launch(Dispatchers.IO) {
runCatching {
EventServiceGrpcKt.EventServiceCoroutineStub(channel).registerPassiveListener(channelFlow {
when(eventType) {
EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent {
send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_MESSAGE
this.message = it.second
}.build())
}
EventType.EVENT_TYPE_CORE_EVENT -> {}
EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onNoticeEvent {
send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_NOTICE
this.notice = it
}.build())
}
EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onRequestEvent {
send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_REQUEST
this.request = it
}.build())
}
EventType.UNRECOGNIZED -> {}
}
})
}.onFailure {
LogCenter.log("KritorClient registerEvent failed: ${it.stackTraceToString()}", Level.ERROR)
}
}
}
private suspend fun onReceive(request: Request) = GlobalScope.launch {
//LogCenter.log("KritorClient onReceive: $request")
runCatching {
val rsp = handleGrpc(request.cmd, request.buf.toByteArray())
senderChannel.emit(Response.newBuilder()
.setCmd(request.cmd)
.setCode(Response.ResponseCode.SUCCESS)
.setMsg("success")
.setSeq(request.seq)
.setBuf(ByteString.copyFrom(rsp))
.build())
}.onFailure {
senderChannel.emit(Response.newBuilder()
.setCmd(request.cmd)
.setCode(Response.ResponseCode.INTERNAL)
.setMsg(it.stackTraceToString())
.setSeq(request.seq)
.setBuf(ByteString.EMPTY)
.build())
}
}
fun isActive(): Boolean {
return !channel.isShutdown
}
fun close() {
channel.shutdown()
}
}

View File

@ -0,0 +1,6 @@
package kritor.handlers
internal object GrpcHandlers {
}

View File

@ -18,14 +18,16 @@ class KritorServer(
private val server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create())
.executor(Dispatchers.IO.asExecutor())
.intercept(AuthInterceptor)
.addService(Authentication)
.addService(ContactService)
.addService(KritorService)
.addService(AuthenticationService)
.addService(CoreService)
.addService(FriendService)
.addService(GroupService)
.addService(GroupFileService)
.addService(MessageService)
.addService(EventService)
.addService(WebService)
.addService(DeveloperService)
.addService(QsignService)
.build()!!
fun start(block: Boolean = false) {

View File

@ -1,66 +0,0 @@
package kritor.service
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.AuthCode
import io.kritor.AuthReq
import io.kritor.AuthRsp
import io.kritor.AuthenticationGrpcKt
import io.kritor.GetAuthStateReq
import io.kritor.GetAuthStateRsp
import io.kritor.authRsp
import io.kritor.getAuthStateRsp
import kritor.auth.AuthInterceptor
import moe.fuqiuluo.shamrock.config.ActiveTicket
import moe.fuqiuluo.shamrock.config.ShamrockConfig
import qq.service.QQInterfaces
object Authentication: AuthenticationGrpcKt.AuthenticationCoroutineImplBase() {
@Grpc("Authentication", "Auth")
override suspend fun auth(request: AuthReq): AuthRsp {
if (QQInterfaces.app.account != request.account) {
return authRsp {
code = AuthCode.NO_ACCOUNT
msg = "No such account"
}
}
val activeTicketName = ActiveTicket.name()
var index = 0
while (true) {
val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null)
if (ticket.isNullOrEmpty()) {
if (index == 0) {
return authRsp {
code = AuthCode.OK
msg = "OK"
}
} else {
break
}
} else if (ticket == request.ticket) {
return authRsp {
code = AuthCode.OK
msg = "OK"
}
}
index++
}
return authRsp {
code = AuthCode.NO_TICKET
msg = "Invalid ticket"
}
}
@Grpc("Authentication", "GetAuthState")
override suspend fun getAuthState(request: GetAuthStateReq): GetAuthStateRsp {
if (request.account != QQInterfaces.app.account) {
throw StatusRuntimeException(Status.CANCELLED.withDescription("No such account"))
}
return getAuthStateRsp {
isRequiredAuth = AuthInterceptor.getAllTicket().isNotEmpty()
}
}
}

View File

@ -0,0 +1,60 @@
package kritor.service
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.authentication.*
import io.kritor.authentication.AuthenticateResponse.AuthenticateResponseCode
import kritor.auth.AuthInterceptor
import moe.fuqiuluo.shamrock.config.ActiveTicket
import moe.fuqiuluo.shamrock.config.ShamrockConfig
import qq.service.QQInterfaces
internal object AuthenticationService: AuthenticationServiceGrpcKt.AuthenticationServiceCoroutineImplBase() {
@Grpc("AuthenticationService", "Authenticate")
override suspend fun authenticate(request: AuthenticateRequest): AuthenticateResponse {
if (QQInterfaces.app.account != request.account) {
return AuthenticateResponse.newBuilder().apply {
code = AuthenticateResponseCode.NO_ACCOUNT
msg = "No such account"
}.build()
}
val activeTicketName = ActiveTicket.name()
var index = 0
while (true) {
val ticket = ShamrockConfig.getProperty(activeTicketName + if (index == 0) "" else ".$index", null)
if (ticket.isNullOrEmpty()) {
if (index == 0) {
return AuthenticateResponse.newBuilder().apply {
code = AuthenticateResponseCode.OK
msg = "OK"
}.build()
} else {
break
}
} else if (ticket == request.ticket) {
return AuthenticateResponse.newBuilder().apply {
code = AuthenticateResponseCode.OK
msg = "OK"
}.build()
}
index++
}
return AuthenticateResponse.newBuilder().apply {
code = AuthenticateResponseCode.NO_TICKET
msg = "Invalid ticket"
}.build()
}
@Grpc("AuthenticationService", "GetAuthenticationState")
override suspend fun getAuthenticationState(request: GetAuthenticationStateRequest): GetAuthenticationStateResponse {
if (request.account != QQInterfaces.app.account) {
throw StatusRuntimeException(Status.CANCELLED.withDescription("No such account"))
}
return GetAuthenticationStateResponse.newBuilder().apply {
isRequired = AuthInterceptor.getAllTicket().isNotEmpty()
}.build()
}
}

View File

@ -1,183 +0,0 @@
package kritor.service
import android.os.Bundle
import com.tencent.mobileqq.profilecard.api.IProfileCardBlacklistApi
import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.*
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
import com.tencent.mobileqq.qroute.QRoute
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.contact.ContactServiceGrpcKt
import io.kritor.contact.GetUidRequest
import io.kritor.contact.GetUidResponse
import io.kritor.contact.GetUinByUidRequest
import io.kritor.contact.GetUinByUidResponse
import io.kritor.contact.IsBlackListUserRequest
import io.kritor.contact.IsBlackListUserResponse
import io.kritor.contact.ProfileCard
import io.kritor.contact.ProfileCardRequest
import io.kritor.contact.SetProfileCardRequest
import io.kritor.contact.SetProfileCardResponse
import io.kritor.contact.StrangerExt
import io.kritor.contact.StrangerInfo
import io.kritor.contact.StrangerInfoRequest
import io.kritor.contact.VoteUserRequest
import io.kritor.contact.VoteUserResponse
import io.kritor.contact.profileCard
import io.kritor.contact.strangerInfo
import io.kritor.contact.voteUserResponse
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import qq.service.QQInterfaces
import qq.service.contact.ContactHelper
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object ContactService: ContactServiceGrpcKt.ContactServiceCoroutineImplBase() {
@Grpc("ContactService", "VoteUser")
override suspend fun voteUser(request: VoteUserRequest): VoteUserResponse {
ContactHelper.voteUser(when(request.accountCase!!) {
VoteUserRequest.AccountCase.ACCOUNT_UIN -> request.accountUin
VoteUserRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid).toLong()
VoteUserRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
.withDescription("account not set")
)
}, request.voteCount).onFailure {
throw StatusRuntimeException(Status.INTERNAL
.withDescription(it.stackTraceToString())
)
}
return voteUserResponse { }
}
@Grpc("ContactService", "GetProfileCard")
override suspend fun getProfileCard(request: ProfileCardRequest): ProfileCard {
val uin = when (request.accountCase!!) {
ProfileCardRequest.AccountCase.ACCOUNT_UIN -> request.accountUin
ProfileCardRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid).toLong()
ProfileCardRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
.withDescription("account not set")
)
}
val contact = ContactHelper.getProfileCard(uin)
contact.onFailure {
throw StatusRuntimeException(Status.INTERNAL
.withDescription(it.stackTraceToString())
)
}
contact.onSuccess {
return profileCard {
this.uin = it.uin.toLong()
this.uid = if (request.hasAccountUid()) request.accountUid
else ContactHelper.getUidByUinAsync(it.uin.toLong())
this.name = it.strNick ?: ""
this.remark = it.strReMark ?: ""
this.level = it.iQQLevel
this.birthday = it.lBirthday
this.loginDay = it.lLoginDays.toInt()
this.voteCnt = it.lVoteCount.toInt()
this.qid = it.qid ?: ""
this.isSchoolVerified = it.schoolVerifiedFlag
}
}
throw StatusRuntimeException(Status.INTERNAL
.withDescription("logic failed")
)
}
@Grpc("ContactService", "GetStrangerInfo")
override suspend fun getStrangerInfo(request: StrangerInfoRequest): StrangerInfo {
val userId = request.uin
val info = ContactHelper.refreshAndGetProfileCard(userId).onFailure {
throw StatusRuntimeException(Status.INTERNAL
.withCause(it)
.withDescription("Unable to fetch stranger info")
)
}.getOrThrow()
return strangerInfo {
this.uid = ContactHelper.getUidByUinAsync(userId)
this.uin = (info.uin ?: "0").toLong()
this.name = info.strNick ?: ""
this.level = info.iQQLevel
this.loginDay = info.lLoginDays.toInt()
this.voteCnt = info.lVoteCount.toInt()
this.qid = info.qid ?: ""
this.isSchoolVerified = info.schoolVerifiedFlag
this.ext = StrangerExt.newBuilder()
.setBigVip(info.bBigClubVipOpen == 1.toByte())
.setHollywoodVip(info.bHollywoodVipOpen == 1.toByte())
.setQqVip(info.bQQVipOpen == 1.toByte())
.setSuperVip(info.bSuperQQOpen == 1.toByte())
.setVoted(info.bVoted == 1.toByte())
.build().toByteString()
}
}
@Grpc("ContactService", "GetUid")
override suspend fun getUid(request: GetUidRequest): GetUidResponse {
return GetUidResponse.newBuilder().apply {
request.uinList.forEach {
putUid(it, ContactHelper.getUidByUinAsync(it))
}
}.build()
}
@Grpc("ContactService", "GetUinByUid")
override suspend fun getUinByUid(request: GetUinByUidRequest): GetUinByUidResponse {
return GetUinByUidResponse.newBuilder().apply {
request.uidList.forEach {
putUin(it, ContactHelper.getUinByUidAsync(it).toLong())
}
}.build()
}
@Grpc("ContactService", "SetProfileCard")
override suspend fun setProfileCard(request: SetProfileCardRequest): SetProfileCardResponse {
val bundle = Bundle()
val service = QQInterfaces.app
.getRuntimeService(IProfileProtocolService::class.java, "all")
if (request.hasNickName()) {
bundle.putString(KEY_NICK, request.nickName)
}
if (request.hasCompany()) {
bundle.putString(KEY_COMPANY, request.company)
}
if (request.hasEmail()) {
bundle.putString(KEY_EMAIL, request.email)
}
if (request.hasCollege()) {
bundle.putString(KEY_COLLEGE, request.college)
}
if (request.hasPersonalNote()) {
bundle.putString(KEY_PERSONAL_NOTE, request.personalNote)
}
if (request.hasBirthday()) {
bundle.putInt(KEY_BIRTHDAY, request.birthday)
}
if (request.hasAge()) {
bundle.putInt(KEY_AGE, request.age)
}
service.setProfileDetail(bundle)
return super.setProfileCard(request)
}
@Grpc("ContactService", "IsBlackListUser")
override suspend fun isBlackListUser(request: IsBlackListUserRequest): IsBlackListUserResponse {
val blacklistApi = QRoute.api(IProfileCardBlacklistApi::class.java)
val isBlack = withTimeoutOrNull(5000) {
suspendCancellableCoroutine { continuation ->
blacklistApi.isBlackOrBlackedUin(request.uin.toString()) {
continuation.resume(it)
}
}
} ?: false
return IsBlackListUserResponse.newBuilder().setIsBlackListUser(isBlack).build()
}
}

View File

@ -4,60 +4,35 @@ import android.util.Base64
import com.tencent.mobileqq.app.QQAppInterface
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.core.ClearCacheRequest
import io.kritor.core.ClearCacheResponse
import io.kritor.core.DownloadFileRequest
import io.kritor.core.DownloadFileResponse
import io.kritor.core.GetCurrentAccountRequest
import io.kritor.core.GetCurrentAccountResponse
import io.kritor.core.GetVersionRequest
import io.kritor.core.GetVersionResponse
import io.kritor.core.KritorServiceGrpcKt
import io.kritor.core.SwitchAccountRequest
import io.kritor.core.SwitchAccountResponse
import io.kritor.core.clearCacheResponse
import io.kritor.core.downloadFileResponse
import io.kritor.core.getCurrentAccountResponse
import io.kritor.core.getVersionResponse
import io.kritor.core.switchAccountResponse
import io.kritor.core.*
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
import moe.fuqiuluo.shamrock.utils.DownloadUtils
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MD5
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
import mqq.app.MobileQQ
import qq.service.QQInterfaces
import qq.service.QQInterfaces.Companion.app
import qq.service.contact.ContactHelper
import java.io.File
object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
@Grpc("KritorService", "GetVersion")
internal object CoreService : CoreServiceGrpcKt.CoreServiceCoroutineImplBase() {
@Grpc("CoreService", "GetVersion")
override suspend fun getVersion(request: GetVersionRequest): GetVersionResponse {
return getVersionResponse {
return GetVersionResponse.newBuilder().apply {
this.version = ShamrockVersion
this.appName = "Shamrock"
}
}.build()
}
@Grpc("KritorService", "ClearCache")
override suspend fun clearCache(request: ClearCacheRequest): ClearCacheResponse {
FileUtils.clearCache()
MMKVFetcher.mmkvWithId("audio2silk")
.clear()
return clearCacheResponse {}
}
@Grpc("KritorService", "GetCurrentAccount")
@Grpc("CoreService", "GetCurrentAccount")
override suspend fun getCurrentAccount(request: GetCurrentAccountRequest): GetCurrentAccountResponse {
return getCurrentAccountResponse {
return GetCurrentAccountResponse.newBuilder().apply {
this.accountName = if (app is QQAppInterface) app.currentNickname else "unknown"
this.accountUid = app.currentUid ?: ""
this.accountUin = (app.currentUin ?: "0").toLong()
}
}.build()
}
@Grpc("KritorService", "DownloadFile")
@Grpc("CoreService", "DownloadFile")
override suspend fun downloadFile(request: DownloadFileRequest): DownloadFileResponse {
val headerMap = mutableMapOf(
"User-Agent" to "Shamrock"
@ -76,13 +51,14 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
if (request.hasBase64()) {
val bytes = Base64.decode(request.base64, Base64.DEFAULT)
tmp.writeBytes(bytes)
} else if(request.hasUrl()) {
if(!DownloadUtils.download(
} else if (request.hasUrl()) {
if (!DownloadUtils.download(
urlAdr = request.url,
dest = tmp,
headers = headerMap,
threadCount = if (request.hasThreadCnt()) request.threadCnt else 3
)) {
)
) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("download failed"))
}
}
@ -96,18 +72,22 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
}
}
return downloadFileResponse {
return DownloadFileResponse.newBuilder().apply {
this.fileMd5 = MD5.genFileMd5Hex(tmp.absolutePath)
this.fileAbsolutePath = tmp.absolutePath
}
}.build()
}
@Grpc("KritorService", "SwitchAccount")
@Grpc("CoreService", "SwitchAccount")
override suspend fun switchAccount(request: SwitchAccountRequest): SwitchAccountResponse {
val uin = when(request.accountCase!!) {
val uin = when (request.accountCase!!) {
SwitchAccountRequest.AccountCase.ACCOUNT_UID -> ContactHelper.getUinByUidAsync(request.accountUid)
SwitchAccountRequest.AccountCase.ACCOUNT_UIN -> request.accountUin.toString()
SwitchAccountRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("account not found"))
SwitchAccountRequest.AccountCase.ACCOUNT_NOT_SET -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT.withDescription(
"account not found"
)
)
}
val account = MobileQQ.getMobileQQ().allAccounts.firstOrNull { it.uin == uin }
?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("account not found"))
@ -116,6 +96,6 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
}.onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it).withDescription("failed to switch account"))
}
return switchAccountResponse { }
return SwitchAccountResponse.newBuilder().build()
}
}

View File

@ -0,0 +1,42 @@
package kritor.service
import com.google.protobuf.ByteString
import io.kritor.developer.*
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import qq.service.QQInterfaces
internal object DeveloperService: DeveloperServiceGrpcKt.DeveloperServiceCoroutineImplBase() {
@Grpc("DeveloperService", "ClearCache")
override suspend fun clearCache(request: ClearCacheRequest): ClearCacheResponse {
FileUtils.clearCache()
MMKVFetcher.mmkvWithId("audio2silk")
.clear()
return ClearCacheResponse.newBuilder().build()
}
@Grpc("DeveloperService", "GetDeviceBattery")
override suspend fun getDeviceBattery(request: GetDeviceBatteryRequest): GetDeviceBatteryResponse {
return GetDeviceBatteryResponse.newBuilder().apply {
PlatformUtils.getDeviceBattery().let {
this.battery = it.battery
this.scale = it.scale
this.status = it.status
}
}.build()
}
@Grpc("DeveloperService", "SendPacket")
override suspend fun sendPacket(request: SendPacketRequest): SendPacketResponse {
return SendPacketResponse.newBuilder().apply {
val fromServiceMsg = QQInterfaces.sendBufferAW(request.command, request.isProtobuf, request.requestBuffer.toByteArray())
if (fromServiceMsg?.wupBuffer == null) {
this.isSuccess = false
} else {
this.isSuccess = true
this.responseBuffer = ByteString.copyFrom(fromServiceMsg.wupBuffer)
}
}.build()
}
}

View File

@ -1,28 +1,42 @@
package kritor.service
import io.kritor.event.EventRequest
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.event.EventServiceGrpcKt
import io.kritor.event.EventStructure
import io.kritor.event.EventType
import io.kritor.event.eventStructure
import io.kritor.event.RequestPushEvent
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
object EventService: EventServiceGrpcKt.EventServiceCoroutineImplBase() {
override fun registerActiveListener(request: EventRequest): Flow<EventStructure> {
internal object EventService : EventServiceGrpcKt.EventServiceCoroutineImplBase() {
override fun registerActiveListener(request: RequestPushEvent): Flow<EventStructure> {
return channelFlow {
when(request.type!!) {
EventType.CORE_EVENT -> TODO()
EventType.MESSAGE -> GlobalEventTransmitter.onMessageEvent {
send(eventStructure {
this.type = EventType.MESSAGE
when (request.type!!) {
EventType.EVENT_TYPE_CORE_EVENT -> {}
EventType.EVENT_TYPE_MESSAGE -> GlobalEventTransmitter.onMessageEvent {
send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_MESSAGE
this.message = it.second
})
}.build())
}
EventType.NOTICE -> TODO()
EventType.REQUEST -> TODO()
EventType.UNRECOGNIZED -> TODO()
EventType.EVENT_TYPE_NOTICE -> GlobalEventTransmitter.onRequestEvent {
send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_NOTICE
this.request = it
}.build())
}
EventType.EVENT_TYPE_REQUEST -> GlobalEventTransmitter.onNoticeEvent {
send(EventStructure.newBuilder().apply {
this.type = EventType.EVENT_TYPE_NOTICE
this.notice = it
}.build())
}
EventType.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT)
}
}
}

View File

@ -1,28 +1,33 @@
package kritor.service
import android.os.Bundle
import com.tencent.mobileqq.profilecard.api.IProfileCardBlacklistApi
import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
import com.tencent.mobileqq.qroute.QRoute
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.friend.FriendServiceGrpcKt
import io.kritor.friend.GetFriendListRequest
import io.kritor.friend.GetFriendListResponse
import io.kritor.friend.friendData
import io.kritor.friend.friendExt
import io.kritor.friend.getFriendListResponse
import io.kritor.friend.*
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import qq.service.QQInterfaces
import qq.service.contact.ContactHelper
import qq.service.friend.FriendHelper
import kotlin.coroutines.resume
object FriendService: FriendServiceGrpcKt.FriendServiceCoroutineImplBase() {
internal object FriendService : FriendServiceGrpcKt.FriendServiceCoroutineImplBase() {
@Grpc("FriendService", "GetFriendList")
override suspend fun getFriendList(request: GetFriendListRequest): GetFriendListResponse {
val friendList = FriendHelper.getFriendList(if(request.hasRefresh()) request.refresh else false).onFailure {
throw StatusRuntimeException(Status.INTERNAL
.withDescription(it.stackTraceToString())
val friendList = FriendHelper.getFriendList(if (request.hasRefresh()) request.refresh else false).onFailure {
throw StatusRuntimeException(
Status.INTERNAL
.withDescription(it.stackTraceToString())
)
}.getOrThrow()
return getFriendListResponse {
return GetFriendListResponse.newBuilder().apply {
friendList.forEach {
this.friendList.add(friendData {
this.addFriendsInfo(FriendInfo.newBuilder().apply {
uin = it.uin.toLong()
uid = ContactHelper.getUidByUinAsync(uin)
qid = ""
@ -32,10 +37,208 @@ object FriendService: FriendServiceGrpcKt.FriendServiceCoroutineImplBase() {
level = 0
gender = it.gender.toInt()
groupId = it.groupid
ext = friendExt {}.toByteString()
ext = ExtInfo.newBuilder().build()
})
}
}
}.build()
}
@Grpc("FriendService", "GetFriendProfileCard")
override suspend fun getFriendProfileCard(request: GetFriendProfileCardRequest): GetFriendProfileCardResponse {
return GetFriendProfileCardResponse.newBuilder().apply {
request.targetUinsList.forEach {
ContactHelper.getProfileCard(it).getOrThrow().let { info ->
addFriendsProfileCard(ProfileCard.newBuilder().apply {
this.uin = info.uin.toLong()
this.uid = ContactHelper.getUidByUinAsync(info.uin.toLong())
this.nick = info.strNick
this.remark = info.strReMark
this.level = info.iQQLevel
this.birthday = info.lBirthday
this.loginDay = info.lLoginDays.toInt()
this.voteCnt = info.lVoteCount.toInt()
this.qid = info.qid ?: ""
this.isSchoolVerified = info.schoolVerifiedFlag
this.ext = ExtInfo.newBuilder().apply {
this.bigVip = info.bBigClubVipOpen == 1.toByte()
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
this.qqVip = info.bQQVipOpen == 1.toByte()
this.superVip = info.bSuperQQOpen == 1.toByte()
this.voted = info.bVoted == 1.toByte()
}.build()
}.build())
}
}
request.targetUidsList.forEach {
ContactHelper.getProfileCard(ContactHelper.getUinByUidAsync(it).toLong()).getOrThrow().let { info ->
addFriendsProfileCard(ProfileCard.newBuilder().apply {
this.uin = info.uin.toLong()
this.uid = it
this.nick = info.strNick
this.remark = info.strReMark
this.level = info.iQQLevel
this.birthday = info.lBirthday
this.loginDay = info.lLoginDays.toInt()
this.voteCnt = info.lVoteCount.toInt()
this.qid = info.qid ?: ""
this.isSchoolVerified = info.schoolVerifiedFlag
this.ext = ExtInfo.newBuilder().apply {
this.bigVip = info.bBigClubVipOpen == 1.toByte()
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
this.qqVip = info.bQQVipOpen == 1.toByte()
this.superVip = info.bSuperQQOpen == 1.toByte()
this.voted = info.bVoted == 1.toByte()
}.build()
}.build())
}
}
}.build()
}
@Grpc("FriendService", "GetStrangerProfileCard")
override suspend fun getStrangerProfileCard(request: GetStrangerProfileCardRequest): GetStrangerProfileCardResponse {
return GetStrangerProfileCardResponse.newBuilder().apply {
request.targetUinsList.forEach {
ContactHelper.refreshAndGetProfileCard(it).getOrThrow().let { info ->
addStrangersProfileCard(ProfileCard.newBuilder().apply {
this.uin = info.uin.toLong()
this.uid = ContactHelper.getUidByUinAsync(info.uin.toLong())
this.nick = info.strNick
this.level = info.iQQLevel
this.birthday = info.lBirthday
this.loginDay = info.lLoginDays.toInt()
this.voteCnt = info.lVoteCount.toInt()
this.qid = info.qid ?: ""
this.isSchoolVerified = info.schoolVerifiedFlag
this.ext = ExtInfo.newBuilder().apply {
this.bigVip = info.bBigClubVipOpen == 1.toByte()
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
this.qqVip = info.bQQVipOpen == 1.toByte()
this.superVip = info.bSuperQQOpen == 1.toByte()
this.voted = info.bVoted == 1.toByte()
}.build()
}.build())
}
}
request.targetUidsList.forEach {
ContactHelper.refreshAndGetProfileCard(ContactHelper.getUinByUidAsync(it).toLong()).getOrThrow()
.let { info ->
addStrangersProfileCard(ProfileCard.newBuilder().apply {
this.uin = info.uin.toLong()
this.uid = it
this.nick = info.strNick
this.level = info.iQQLevel
this.birthday = info.lBirthday
this.loginDay = info.lLoginDays.toInt()
this.voteCnt = info.lVoteCount.toInt()
this.qid = info.qid ?: ""
this.isSchoolVerified = info.schoolVerifiedFlag
this.ext = ExtInfo.newBuilder().apply {
this.bigVip = info.bBigClubVipOpen == 1.toByte()
this.hollywoodVip = info.bHollywoodVipOpen == 1.toByte()
this.qqVip = info.bQQVipOpen == 1.toByte()
this.superVip = info.bSuperQQOpen == 1.toByte()
this.voted = info.bVoted == 1.toByte()
}.build()
}.build())
}
}
}.build()
}
@Grpc("FriendService", "SetProfileCard")
override suspend fun setProfileCard(request: SetProfileCardRequest): SetProfileCardResponse {
val bundle = Bundle()
val service = QQInterfaces.app
.getRuntimeService(IProfileProtocolService::class.java, "all")
if (request.hasNickName()) {
bundle.putString(IProfileProtocolConst.KEY_NICK, request.nickName)
}
if (request.hasCompany()) {
bundle.putString(IProfileProtocolConst.KEY_COMPANY, request.company)
}
if (request.hasEmail()) {
bundle.putString(IProfileProtocolConst.KEY_EMAIL, request.email)
}
if (request.hasCollege()) {
bundle.putString(IProfileProtocolConst.KEY_COLLEGE, request.college)
}
if (request.hasPersonalNote()) {
bundle.putString(IProfileProtocolConst.KEY_PERSONAL_NOTE, request.personalNote)
}
if (request.hasBirthday()) {
bundle.putInt(IProfileProtocolConst.KEY_BIRTHDAY, request.birthday)
}
if (request.hasAge()) {
bundle.putInt(IProfileProtocolConst.KEY_AGE, request.age)
}
service.setProfileDetail(bundle)
return super.setProfileCard(request)
}
@Grpc("FriendService", "IsBlackListUser")
override suspend fun isBlackListUser(request: IsBlackListUserRequest): IsBlackListUserResponse {
val uin = when (request.targetCase!!) {
IsBlackListUserRequest.TargetCase.TARGET_UIN -> request.targetUin.toString()
IsBlackListUserRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
IsBlackListUserRequest.TargetCase.TARGET_NOT_SET -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("account not set")
)
}
val blacklistApi = QRoute.api(IProfileCardBlacklistApi::class.java)
val isBlack = withTimeoutOrNull(5000) {
suspendCancellableCoroutine { continuation ->
blacklistApi.isBlackOrBlackedUin(uin) {
continuation.resume(it)
}
}
} ?: false
return IsBlackListUserResponse.newBuilder().setIsBlackListUser(isBlack).build()
}
@Grpc("FriendService", "VoteUser")
override suspend fun voteUser(request: VoteUserRequest): VoteUserResponse {
ContactHelper.voteUser(
when (request.targetCase!!) {
VoteUserRequest.TargetCase.TARGET_UIN -> request.targetUin
VoteUserRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
VoteUserRequest.TargetCase.TARGET_NOT_SET -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("account not set")
)
}, request.voteCount
).onFailure {
throw StatusRuntimeException(
Status.INTERNAL
.withDescription(it.stackTraceToString())
)
}
return VoteUserResponse.newBuilder().build()
}
@Grpc("FriendService", "GetUidByUin")
override suspend fun getUidByUin(request: GetUidRequest): GetUidResponse {
return GetUidResponse.newBuilder().apply {
request.targetUinsList.forEach {
putUidMap(it, ContactHelper.getUidByUinAsync(it))
}
}.build()
}
@Grpc("FriendService", "GetUinByUid")
override suspend fun getUinByUid(request: GetUinByUidRequest): GetUinByUidResponse {
return GetUinByUidResponse.newBuilder().apply {
request.targetUidsList.forEach {
putUinMap(it, ContactHelper.getUinByUidAsync(it).toLong())
}
}.build()
}
}

View File

@ -15,10 +15,9 @@ import qq.service.QQInterfaces
import qq.service.file.GroupFileHelper
import qq.service.file.GroupFileHelper.getGroupFileSystemInfo
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
import tencent.im.oidb.cmd0x6d8.oidb_0x6d8
import tencent.im.oidb.oidb_sso
internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCoroutineImplBase() {
internal object GroupFileService : GroupFileServiceGrpcKt.GroupFileServiceCoroutineImplBase() {
@Grpc("GroupFileService", "CreateFolder")
override suspend fun createFolder(request: CreateFolderRequest): CreateFolderResponse {
val data = Oidb0x6d7ReqBody(
@ -31,7 +30,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
).toByteArray()
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_0", 1751, 0, data)
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (!fromServiceMsg.isSuccess) {
if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
}
val oidbPkg = oidb_sso.OIDBSSOPkg()
@ -42,22 +41,24 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
if (rsp.createFolder?.retCode != 0) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to create folder: ${rsp.createFolder?.retCode}"))
}
return createFolderResponse {
return CreateFolderResponse.newBuilder().apply {
this.id = rsp.createFolder?.folderInfo?.folderId ?: ""
this.usedSpace = 0
}
}.build()
}
@Grpc("GroupFileService", "DeleteFolder")
override suspend fun deleteFolder(request: DeleteFolderRequest): DeleteFolderResponse {
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody(
deleteFolder = DeleteFolderReq(
groupCode = request.groupId.toULong(),
appId = 3u,
folderId = request.folderId
)
).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (!fromServiceMsg.isSuccess) {
val fromServiceMsg = QQInterfaces.sendOidbAW(
"OidbSvc.0x6d7_1", 1751, 1, Oidb0x6d7ReqBody(
deleteFolder = DeleteFolderReq(
groupCode = request.groupId.toULong(),
appId = 3u,
folderId = request.folderId
)
).toByteArray()
) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
}
val oidbPkg = oidb_sso.OIDBSSOPkg()
@ -66,7 +67,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
if (rsp.deleteFolder?.retCode != 0) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete folder: ${rsp.deleteFolder?.retCode}"))
}
return deleteFolderResponse { }
return DeleteFolderResponse.newBuilder().build()
}
@Grpc("GroupFileService", "DeleteFile")
@ -82,7 +83,7 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
}
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d6_3", 1750, 3, oidb0x6d6ReqBody.toByteArray())
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (!fromServiceMsg.isSuccess) {
if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
}
val oidbPkg = oidb_sso.OIDBSSOPkg()
@ -93,20 +94,22 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
if (rsp.delete_file_rsp.int32_ret_code.get() != 0) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to delete file: ${rsp.delete_file_rsp.int32_ret_code.get()}"))
}
return deleteFileResponse { }
return DeleteFileResponse.newBuilder().build()
}
@Grpc("GroupFileService", "RenameFolder")
override suspend fun renameFolder(request: RenameFolderRequest): RenameFolderResponse {
val fromServiceMsg = QQInterfaces.sendOidbAW("OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody(
renameFolder = RenameFolderReq(
groupCode = request.groupId.toULong(),
appId = 3u,
folderId = request.folderId,
folderName = request.name
)
).toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (!fromServiceMsg.isSuccess) {
val fromServiceMsg = QQInterfaces.sendOidbAW(
"OidbSvc.0x6d7_3", 1751, 3, Oidb0x6d7ReqBody(
renameFolder = RenameFolderReq(
groupCode = request.groupId.toULong(),
appId = 3u,
folderId = request.folderId,
folderName = request.name
)
).toByteArray()
) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
}
val oidbPkg = oidb_sso.OIDBSSOPkg()
@ -115,24 +118,19 @@ internal object GroupFileService: GroupFileServiceGrpcKt.GroupFileServiceCorouti
if (rsp.renameFolder?.retCode != 0) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to rename folder: ${rsp.renameFolder?.retCode}"))
}
return renameFolderResponse { }
return RenameFolderResponse.newBuilder().build()
}
@Grpc("GroupFileService", "GetFileSystemInfo")
override suspend fun getFileSystemInfo(request: GetFileSystemInfoRequest): GetFileSystemInfoResponse {
return getGroupFileSystemInfo(request.groupId)
}
@Grpc("GroupFileService", "GetRootFiles")
override suspend fun getRootFiles(request: GetRootFilesRequest): GetRootFilesResponse {
return getRootFilesResponse {
val response = GroupFileHelper.getGroupFiles(request.groupId)
this.files.addAll(response.filesList)
this.folders.addAll(response.foldersList)
}
}
@Grpc("GroupFileService", "GetFiles")
override suspend fun getFiles(request: GetFilesRequest): GetFilesResponse {
return GroupFileHelper.getGroupFiles(request.groupId, request.folderId)
@Grpc("GroupFileService", "GetFileList")
override suspend fun getFileList(request: GetFileListRequest): GetFileListResponse {
return if (request.hasFolderId())
GroupFileHelper.getGroupFiles(request.groupId, request.folderId)
else
GroupFileHelper.getGroupFiles(request.groupId)
}
}

View File

@ -2,213 +2,186 @@ package kritor.service
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.group.BanMemberRequest
import io.kritor.group.BanMemberResponse
import io.kritor.group.GetGroupHonorRequest
import io.kritor.group.GetGroupHonorResponse
import io.kritor.group.GetGroupInfoRequest
import io.kritor.group.GetGroupInfoResponse
import io.kritor.group.GetGroupListRequest
import io.kritor.group.GetGroupListResponse
import io.kritor.group.GetGroupMemberInfoRequest
import io.kritor.group.GetGroupMemberInfoResponse
import io.kritor.group.GetGroupMemberListRequest
import io.kritor.group.GetGroupMemberListResponse
import io.kritor.group.GetNotJoinedGroupInfoRequest
import io.kritor.group.GetNotJoinedGroupInfoResponse
import io.kritor.group.GetProhibitedUserListRequest
import io.kritor.group.GetProhibitedUserListResponse
import io.kritor.group.GetRemainCountAtAllRequest
import io.kritor.group.GetRemainCountAtAllResponse
import io.kritor.group.GroupServiceGrpcKt
import io.kritor.group.KickMemberRequest
import io.kritor.group.KickMemberResponse
import io.kritor.group.LeaveGroupRequest
import io.kritor.group.LeaveGroupResponse
import io.kritor.group.ModifyGroupNameRequest
import io.kritor.group.ModifyGroupNameResponse
import io.kritor.group.ModifyGroupRemarkRequest
import io.kritor.group.ModifyGroupRemarkResponse
import io.kritor.group.ModifyMemberCardRequest
import io.kritor.group.ModifyMemberCardResponse
import io.kritor.group.PokeMemberRequest
import io.kritor.group.PokeMemberResponse
import io.kritor.group.SetGroupAdminRequest
import io.kritor.group.SetGroupAdminResponse
import io.kritor.group.SetGroupUniqueTitleRequest
import io.kritor.group.SetGroupUniqueTitleResponse
import io.kritor.group.SetGroupWholeBanRequest
import io.kritor.group.SetGroupWholeBanResponse
import io.kritor.group.banMemberResponse
import io.kritor.group.getGroupHonorResponse
import io.kritor.group.getGroupInfoResponse
import io.kritor.group.getGroupListResponse
import io.kritor.group.getGroupMemberInfoResponse
import io.kritor.group.getGroupMemberListResponse
import io.kritor.group.getNotJoinedGroupInfoResponse
import io.kritor.group.getProhibitedUserListResponse
import io.kritor.group.getRemainCountAtAllResponse
import io.kritor.group.groupHonorInfo
import io.kritor.group.groupMemberInfo
import io.kritor.group.kickMemberResponse
import io.kritor.group.leaveGroupResponse
import io.kritor.group.modifyGroupNameResponse
import io.kritor.group.modifyGroupRemarkResponse
import io.kritor.group.modifyMemberCardResponse
import io.kritor.group.notJoinedGroupInfo
import io.kritor.group.pokeMemberResponse
import io.kritor.group.prohibitedUserInfo
import io.kritor.group.setGroupAdminResponse
import io.kritor.group.setGroupUniqueTitleResponse
import io.kritor.group.setGroupWholeBanResponse
import moe.fuqiuluo.shamrock.helper.TroopHonorHelper
import io.kritor.group.*
import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
import qq.service.contact.ContactHelper
import qq.service.group.GroupHelper
internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase() {
internal object GroupService : GroupServiceGrpcKt.GroupServiceCoroutineImplBase() {
@Grpc("GroupService", "BanMember")
override suspend fun banMember(request: BanMemberRequest): BanMemberResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.banMember(request.groupId, when(request.targetCase!!) {
BanMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
BanMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.duration)
GroupHelper.banMember(
request.groupId, when (request.targetCase!!) {
BanMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
BanMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.duration
)
return banMemberResponse {
return BanMemberResponse.newBuilder().apply {
groupId = request.groupId
}
}.build()
}
@Grpc("GroupService", "PokeMember")
override suspend fun pokeMember(request: PokeMemberRequest): PokeMemberResponse {
GroupHelper.pokeMember(request.groupId, when(request.targetCase!!) {
PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
PokeMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
.withDescription("target not set")
)
})
return pokeMemberResponse { }
GroupHelper.pokeMember(
request.groupId, when (request.targetCase!!) {
PokeMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
PokeMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}
)
return PokeMemberResponse.newBuilder().build()
}
@Grpc("GroupService", "KickMember")
override suspend fun kickMember(request: KickMemberRequest): KickMemberResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.kickMember(request.groupId, request.rejectAddRequest, if (request.hasKickReason()) request.kickReason else "", when(request.targetCase!!) {
KickMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
KickMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
.withDescription("target not set")
)
})
return kickMemberResponse { }
GroupHelper.kickMember(
request.groupId,
request.rejectAddRequest,
if (request.hasKickReason()) request.kickReason else "",
when (request.targetCase!!) {
KickMemberRequest.TargetCase.TARGET_UIN -> request.targetUin
KickMemberRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}
)
return KickMemberResponse.newBuilder().build()
}
@Grpc("GroupService", "LeaveGroup")
override suspend fun leaveGroup(request: LeaveGroupRequest): LeaveGroupResponse {
GroupHelper.resignTroop(request.groupId.toString())
return leaveGroupResponse { }
return LeaveGroupResponse.newBuilder().build()
}
@Grpc("GroupService", "ModifyMemberCard")
override suspend fun modifyMemberCard(request: ModifyMemberCardRequest): ModifyMemberCardResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.modifyGroupMemberCard(request.groupId, when(request.targetCase!!) {
ModifyMemberCardRequest.TargetCase.TARGET_UIN -> request.targetUin
ModifyMemberCardRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.card)
return modifyMemberCardResponse { }
GroupHelper.modifyGroupMemberCard(
request.groupId, when (request.targetCase!!) {
ModifyMemberCardRequest.TargetCase.TARGET_UIN -> request.targetUin
ModifyMemberCardRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
.toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.card
)
return ModifyMemberCardResponse.newBuilder().build()
}
@Grpc("GroupService", "ModifyGroupName")
override suspend fun modifyGroupName(request: ModifyGroupNameRequest): ModifyGroupNameResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.modifyTroopName(request.groupId.toString(), request.groupName)
return modifyGroupNameResponse { }
return ModifyGroupNameResponse.newBuilder().build()
}
@Grpc("GroupService", "ModifyGroupRemark")
override suspend fun modifyGroupRemark(request: ModifyGroupRemarkRequest): ModifyGroupRemarkResponse {
GroupHelper.modifyGroupRemark(request.groupId, request.remark)
return modifyGroupRemarkResponse { }
return ModifyGroupRemarkResponse.newBuilder().build()
}
@Grpc("GroupService", "SetGroupAdmin")
override suspend fun setGroupAdmin(request: SetGroupAdminRequest): SetGroupAdminResponse {
if (!GroupHelper.isOwner(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.setGroupAdmin(request.groupId, when(request.targetCase!!) {
SetGroupAdminRequest.TargetCase.TARGET_UIN -> request.targetUin
SetGroupAdminRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.isAdmin)
GroupHelper.setGroupAdmin(
request.groupId, when (request.targetCase!!) {
SetGroupAdminRequest.TargetCase.TARGET_UIN -> request.targetUin
SetGroupAdminRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.isAdmin
)
return setGroupAdminResponse { }
return SetGroupAdminResponse.newBuilder().build()
}
@Grpc("GroupService", "SetGroupUniqueTitle")
override suspend fun setGroupUniqueTitle(request: SetGroupUniqueTitleRequest): SetGroupUniqueTitleResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.setGroupUniqueTitle(request.groupId, when(request.targetCase!!) {
SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin
SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.uniqueTitle)
GroupHelper.setGroupUniqueTitle(
request.groupId.toString(), when (request.targetCase!!) {
SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin
SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
.toLong()
return setGroupUniqueTitleResponse { }
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}.toString(), request.uniqueTitle
)
return SetGroupUniqueTitleResponse.newBuilder().build()
}
@Grpc("GroupService", "SetGroupWholeBan")
override suspend fun setGroupWholeBan(request: SetGroupWholeBanRequest): SetGroupWholeBanResponse {
if (!GroupHelper.isAdmin(request.groupId.toString())) {
throw StatusRuntimeException(Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.setGroupWholeBan(request.groupId, request.isBan)
return setGroupWholeBanResponse { }
return SetGroupWholeBanResponse.newBuilder().build()
}
@Grpc("GroupService", "GetGroupInfo")
@ -216,18 +189,20 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
val groupInfo = GroupHelper.getGroupInfo(request.groupId.toString(), true).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group info").withCause(it))
}.getOrThrow()
return getGroupInfoResponse {
this.groupInfo = io.kritor.group.groupInfo {
return GetGroupInfoResponse.newBuilder().apply {
this.groupInfo = GroupInfo.newBuilder().apply {
groupId = groupInfo.troopcode.toLong()
groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: ""
groupName =
groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName }
?: ""
groupRemark = groupInfo.troopRemark ?: ""
owner = groupInfo.troopowneruin?.toLong() ?: 0
admins.addAll(GroupHelper.getAdminList(groupId))
addAllAdmins(GroupHelper.getAdminList(groupId))
maxMemberCount = groupInfo.wMemberMax
memberCount = groupInfo.wMemberNum
groupUin = groupInfo.troopuin?.toLong() ?: 0
}
}
}.build()
}.build()
}
@Grpc("GroupService", "GetGroupList")
@ -235,36 +210,45 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
val groupList = GroupHelper.getGroupList(if (request.hasRefresh()) request.refresh else false).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group list").withCause(it))
}.getOrThrow()
return getGroupListResponse {
return GetGroupListResponse.newBuilder().apply {
groupList.forEach { groupInfo ->
this.groupInfo.add(io.kritor.group.groupInfo {
this.addGroupsInfo(GroupInfo.newBuilder().apply {
groupId = groupInfo.troopcode.toLong()
groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: ""
groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }
.ifNullOrEmpty { groupInfo.newTroopName } ?: ""
groupRemark = groupInfo.troopRemark ?: ""
owner = groupInfo.troopowneruin?.toLong() ?: 0
admins.addAll(GroupHelper.getAdminList(groupId))
addAllAdmins(GroupHelper.getAdminList(groupId))
maxMemberCount = groupInfo.wMemberMax
memberCount = groupInfo.wMemberNum
groupUin = groupInfo.troopuin?.toLong() ?: 0
})
}
}
}.build()
}
@Grpc("GroupService", "GetGroupMemberInfo")
override suspend fun getGroupMemberInfo(request: GetGroupMemberInfoRequest): GetGroupMemberInfoResponse {
val memberInfo = GroupHelper.getTroopMemberInfoByUin(request.groupId, when(request.targetCase!!) {
GetGroupMemberInfoRequest.TargetCase.UIN -> request.uin
GetGroupMemberInfoRequest.TargetCase.UID -> ContactHelper.getUinByUidAsync(request.uid).toLong()
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
.withDescription("target not set")
val memberInfo = GroupHelper.getTroopMemberInfoByUin(
request.groupId.toString(), when (request.targetCase!!) {
GetGroupMemberInfoRequest.TargetCase.TARGET_UID -> request.targetUin
GetGroupMemberInfoRequest.TargetCase.TARGET_UIN -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}.toString()
).onFailure {
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get group member info").withCause(it)
)
}).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member info").withCause(it))
}.getOrThrow()
return getGroupMemberInfoResponse {
groupMemberInfo = groupMemberInfo {
uid = if (request.targetCase == GetGroupMemberInfoRequest.TargetCase.UID) request.uid else ContactHelper.getUidByUinAsync(request.uin)
return GetGroupMemberInfoResponse.newBuilder().apply {
groupMemberInfo = GroupMemberInfo.newBuilder().apply {
uid =
if (request.targetCase == GetGroupMemberInfoRequest.TargetCase.TARGET_UID) request.targetUid else ContactHelper.getUidByUinAsync(
request.targetUin
)
uin = memberInfo.memberuin?.toLong() ?: 0
nick = memberInfo.troopnick
.ifNullOrEmpty { memberInfo.hwName }
@ -280,24 +264,29 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
shutUpTimestamp = memberInfo.gagTimeStamp
distance = memberInfo.distance
honor.addAll((memberInfo.honorList ?: "")
addAllHonors((memberInfo.honorList ?: "")
.split("|")
.filter { it.isNotBlank() }
.map { it.toInt() })
unfriendly = false
cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
}
}
}.build()
}.build()
}
@Grpc("GroupService", "GetGroupMemberList")
override suspend fun getGroupMemberList(request: GetGroupMemberListRequest): GetGroupMemberListResponse {
val memberList = GroupHelper.getGroupMemberList(request.groupId.toString(), if (request.hasRefresh()) request.refresh else false).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member list").withCause(it))
val memberList = GroupHelper.getGroupMemberList(
request.groupId.toString(),
if (request.hasRefresh()) request.refresh else false
).onFailure {
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get group member list").withCause(it)
)
}.getOrThrow()
return getGroupMemberListResponse {
return GetGroupMemberListResponse.newBuilder().apply {
memberList.forEach { memberInfo ->
this.groupMemberInfo.add(groupMemberInfo {
this.addGroupMembersInfo(GroupMemberInfo.newBuilder().apply {
uid = ContactHelper.getUidByUinAsync(memberInfo.memberuin?.toLong() ?: 0)
uin = memberInfo.memberuin?.toLong() ?: 0
nick = memberInfo.troopnick
@ -314,7 +303,7 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
shutUpTimestamp = memberInfo.gagTimeStamp
distance = memberInfo.distance
honor.addAll((memberInfo.honorList ?: "")
addAllHonors((memberInfo.honorList ?: "")
.split("|")
.filter { it.isNotBlank() }
.map { it.toInt() })
@ -322,23 +311,25 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
})
}
}
}.build()
}
@Grpc("GroupService", "GetProhibitedUserList")
override suspend fun getProhibitedUserList(request: GetProhibitedUserListRequest): GetProhibitedUserListResponse {
val prohibitedList = GroupHelper.getProhibitedMemberList(request.groupId).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get prohibited user list").withCause(it))
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get prohibited user list").withCause(it)
)
}.getOrThrow()
return getProhibitedUserListResponse {
return GetProhibitedUserListResponse.newBuilder().apply {
prohibitedList.forEach {
this.prohibitedUserInfo.add(prohibitedUserInfo {
this.addProhibitedUsersInfo(ProhibitedUserInfo.newBuilder().apply {
uid = ContactHelper.getUidByUinAsync(it.memberUin)
uin = it.memberUin
prohibitedTime = it.shutuptimestap
})
}
}
}.build()
}
@Grpc("GroupService", "GetRemainCountAtAll")
@ -346,20 +337,22 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
val remainAtAllRsp = GroupHelper.getGroupRemainAtAllRemain(request.groupId).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get remain count").withCause(it))
}.getOrThrow()
return getRemainCountAtAllResponse {
return GetRemainCountAtAllResponse.newBuilder().apply {
accessAtAll = remainAtAllRsp.bool_can_at_all.get()
remainCountForGroup = remainAtAllRsp.uint32_remain_at_all_count_for_group.get()
remainCountForSelf = remainAtAllRsp.uint32_remain_at_all_count_for_uin.get()
}
}.build()
}
@Grpc("GroupService", "GetNotJoinedGroupInfo")
override suspend fun getNotJoinedGroupInfo(request: GetNotJoinedGroupInfoRequest): GetNotJoinedGroupInfoResponse {
val groupInfo = GroupHelper.getNotJoinedGroupInfo(request.groupId).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get not joined group info").withCause(it))
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get not joined group info").withCause(it)
)
}.getOrThrow()
return getNotJoinedGroupInfoResponse {
this.groupInfo = notJoinedGroupInfo {
return GetNotJoinedGroupInfoResponse.newBuilder().apply {
this.groupInfo = NotJoinedGroupInfo.newBuilder().apply {
groupId = groupInfo.groupId
groupName = groupInfo.groupName
owner = groupInfo.owner
@ -369,15 +362,17 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
createTime = groupInfo.createTime.toInt()
groupFlag = groupInfo.groupFlag
groupFlagExt = groupInfo.groupFlagExt
}
}
}.build()
}.build()
}
@Grpc("GroupService", "GetGroupHonor")
override suspend fun getGroupHonor(request: GetGroupHonorRequest): GetGroupHonorResponse {
return getGroupHonorResponse {
return GetGroupHonorResponse.newBuilder().apply {
GroupHelper.getGroupMemberList(request.groupId.toString(), true).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member list").withCause(it))
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get group member list").withCause(it)
)
}.onSuccess { memberList ->
memberList.forEach { member ->
(member.honorList ?: "").split("|")
@ -385,7 +380,7 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
.map { it.toInt() }.forEach {
val honor = decodeHonor(member.memberuin.toLong(), it, member.mHonorRichFlag)
if (honor != null) {
groupHonorInfo.add(groupHonorInfo {
addGroupHonorsInfo(GroupHonorInfo.newBuilder().apply {
uid = ContactHelper.getUidByUinAsync(member.memberuin.toLong())
uin = member.memberuin.toLong()
nick = member.troopnick
@ -401,6 +396,6 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
}
}
}
}
}.build()
}
}

View File

@ -1,7 +1,470 @@
package kritor.service
import io.kritor.message.MessageServiceGrpcKt
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.msg.api.IMsgService
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.common.*
import io.kritor.message.*
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import protobuf.auto.toByteArray
import protobuf.message.*
import protobuf.message.element.GeneralFlags
import protobuf.message.routing.C2C
import protobuf.message.routing.Grp
import qq.service.QQInterfaces
import qq.service.contact.longPeer
import qq.service.internals.NTServiceFetcher
import qq.service.msg.*
import qq.service.msg.ForwardMessageHelper
import qq.service.msg.MessageHelper
import kotlin.coroutines.resume
import kotlin.random.Random
import kotlin.random.nextUInt
internal object MessageService: MessageServiceGrpcKt.MessageServiceCoroutineImplBase() {
internal object MessageService : MessageServiceGrpcKt.MessageServiceCoroutineImplBase() {
@Grpc("MessageService", "SendMessage")
override suspend fun sendMessage(request: SendMessageRequest): SendMessageResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val uniseq = MessageHelper.generateMsgId(contact.chatType)
return SendMessageResponse.newBuilder().apply {
this.messageId = MessageHelper.sendMessage(
contact,
NtMsgConvertor.convertToNtMsgs(contact, uniseq, request.elementsList),
request.retryCount,
uniseq
).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
}.getOrThrow().toString()
}.build()
}
@Grpc("MessageService", "SendMessageByResId")
override suspend fun sendMessageByResId(request: SendMessageByResIdRequest): SendMessageByResIdResponse {
val contact = request.contact
val req = PbSendMsgReq(
routingHead = when (request.contact.scene) {
Scene.GROUP -> RoutingHead(grp = Grp(contact.longPeer().toUInt()))
Scene.FRIEND -> RoutingHead(c2c = C2C(contact.longPeer().toUInt()))
else -> RoutingHead(grp = Grp(contact.longPeer().toUInt()))
},
contentHead = ContentHead(1, 0, 0, 0),
msgBody = MsgBody(
richText = RichText(
elements = arrayListOf(
Elem(
generalFlags = GeneralFlags(
longTextFlag = 1u,
longTextResid = request.resId
)
)
)
)
),
msgSeq = Random.nextUInt(),
msgRand = Random.nextUInt(),
msgVia = 0u
)
QQInterfaces.sendBuffer("MessageSvc.PbSendMsg", true, req.toByteArray())
return SendMessageByResIdResponse.newBuilder().build()
}
@Grpc("MessageService", "SetMessageReaded")
override suspend fun setMessageReaded(request: SetMessageReadRequest): SetMessageReadResponse {
val contact = request.contact
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val service = sessionService.msgService
val chatType = when (contact.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}
service.clearMsgRecords(Contact(chatType, contact.peer, contact.subPeer), null)
return SetMessageReadResponse.newBuilder().build()
}
@Grpc("MessageService", "RecallMessage")
override suspend fun recallMessage(request: RecallMessageRequest): RecallMessageResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val service = sessionService.msgService
service.recallMsg(contact, arrayListOf(request.messageId.toLong())) { code, msg ->
if (code != 0) {
LogCenter.log("消息撤回失败: $code:$msg", Level.WARN)
}
}
return RecallMessageResponse.newBuilder().build()
}
@Grpc("MessageService", "GetMessage")
override suspend fun getMessage(request: GetMessageRequest): GetMessageResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val msg: MsgRecord = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
return GetMessageResponse.newBuilder().apply {
this.message = PushMessageBody.newBuilder().apply {
this.messageId = msg.msgId.toString()
this.contact = request.contact
this.sender = Sender.newBuilder().apply {
this.uid = msg.senderUid ?: ""
this.uin = msg.senderUin
this.nick = msg.sendNickName ?: ""
}.build()
this.messageSeq = msg.msgSeq
this.addAllElements(msg.elements.toKritorReqMessages(contact))
}.build()
}.build()
}
@Grpc("MessageService", "GetMessageBySeq")
override suspend fun getMessageBySeq(request: GetMessageBySeqRequest): GetMessageBySeqResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val msg: MsgRecord = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsBySeqAndCount(contact, request.messageSeq, 1, true) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
return GetMessageBySeqResponse.newBuilder().apply {
this.message = PushMessageBody.newBuilder().apply {
this.messageId = msg.msgId.toString()
this.contact = request.contact
this.sender = Sender.newBuilder().apply {
this.uin = msg.senderUin
this.nick = msg.sendNickName ?: ""
this.uid = msg.senderUid ?: ""
}.build()
this.messageSeq = msg.msgSeq
this.addAllElements(msg.elements.toKritorReqMessages(contact))
}.build()
}.build()
}
@Grpc("MessageService", "GetHistoryMessage")
override suspend fun getHistoryMessage(request: GetHistoryMessageRequest): GetHistoryMessageResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val msgs: List<MsgRecord> = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgs(contact, request.startMessageId.toLong(), request.count, true) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords)
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Messages not found"))
return GetHistoryMessageResponse.newBuilder().apply {
msgs.forEach {
addMessages(PushMessageBody.newBuilder().apply {
this.messageId = it.msgId.toString()
this.contact = request.contact
this.sender = Sender.newBuilder().apply {
this.uin = it.senderUin
this.nick = it.sendNickName ?: ""
this.uid = it.senderUid ?: ""
}.build()
this.messageSeq = it.msgSeq
this.addAllElements(it.elements.toKritorReqMessages(contact))
})
}
}.build()
}
@Grpc("MessageService", "UploadForwardMessage")
override suspend fun uploadForwardMessage(request: UploadForwardMessageRequest): UploadForwardMessageResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val forwardMessage = ForwardMessageHelper.uploadMultiMsg(
contact,
request.messagesList
).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
}.getOrThrow()
return UploadForwardMessageResponse.newBuilder().apply {
this.resId = forwardMessage.resId
}.build()
}
@Grpc("MessageService", "DownloadForwardMessage")
override suspend fun downloadForwardMessage(request: DownloadForwardMessageRequest): DownloadForwardMessageResponse {
return DownloadForwardMessageResponse.newBuilder().apply {
this.addAllMessages(
MessageHelper.getForwardMsg(request.resId).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
}.getOrThrow().map { detail ->
PushMessageBody.newBuilder().apply {
this.time = detail.time
this.messageId = detail.qqMsgId.toString()
this.messageSeq = detail.msgSeq
this.contact = io.kritor.common.Contact.newBuilder().apply {
this.scene = when (detail.msgType) {
MsgConstant.KCHATTYPEC2C -> Scene.FRIEND
MsgConstant.KCHATTYPEGROUP -> Scene.GROUP
MsgConstant.KCHATTYPEGUILD -> Scene.GUILD
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Scene.STRANGER_FROM_GROUP
MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN -> Scene.NEARBY
else -> Scene.STRANGER
}
this.peer = detail.peerId.toString()
}.build()
this.sender = Sender.newBuilder().apply {
this.uin = detail.sender.userId
this.nick = detail.sender.nickName
this.uid = detail.sender.uid
}.build()
detail.message?.elements?.toKritorResponseMessages(
com.tencent.qqnt.kernel.nativeinterface.Contact(
detail.msgType,
detail.peerId.toString(),
null
)
)?.let {
this.addAllElements(it)
}
}.build()
}
)
}.build()
}
@Grpc("MessageService", "DeleteEssenceMessage")
override suspend fun deleteEssenceMessage(request: DeleteEssenceMessageRequest): DeleteEssenceMessageResponse {
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString())
val msg: MsgRecord = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
if (MessageHelper.deleteEssenceMessage(request.groupId, msg.msgSeq, msg.msgRandom) == null)
throw StatusRuntimeException(Status.NOT_FOUND.withDescription("delete essence message failed"))
return DeleteEssenceMessageResponse.newBuilder().build()
}
@Grpc("MessageService", "GetEssenceMessageList")
override suspend fun getEssenceMessageList(request: GetEssenceMessageListRequest): GetEssenceMessageListResponse {
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString())
return GetEssenceMessageListResponse.newBuilder().apply {
MessageHelper.getEssenceMessageList(request.groupId, request.page, request.pageSize).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withCause(it))
}.getOrThrow().forEach {
addMessages(EssenceMessageBody.newBuilder().apply {
withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsBySeqAndCount(contact, it.messageSeq, 1, true) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
}?.let {
this.messageId = it.msgId.toString()
}
this.messageSeq = it.messageSeq
this.messageTime = it.senderTime.toInt()
this.senderNick = it.senderNick
this.senderUin = it.senderId
this.operationTime = it.operatorTime.toInt()
this.operatorNick = it.operatorNick
this.operatorUin = it.operatorId
this.jsonElements = it.messageContent.toString()
})
}
}.build()
}
@Grpc("MessageService", "SetEssenceMessage")
override suspend fun setEssenceMessage(request: SetEssenceMessageRequest): SetEssenceMessageResponse {
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, request.groupId.toString())
val msg: MsgRecord = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
if (MessageHelper.setEssenceMessage(request.groupId, msg.msgSeq, msg.msgRandom) == null) {
throw StatusRuntimeException(Status.NOT_FOUND.withDescription("set essence message failed"))
}
return SetEssenceMessageResponse.newBuilder().build()
}
@Grpc("MessageService", "ReactMessageWithEmoji")
override suspend fun reactMessageWithEmoji(request: ReactMessageWithEmojiRequest): ReactMessageWithEmojiResponse {
val contact = request.contact.let {
MessageHelper.generateContact(
when (it.scene!!) {
Scene.GROUP -> MsgConstant.KCHATTYPEGROUP
Scene.FRIEND -> MsgConstant.KCHATTYPEC2C
Scene.GUILD -> MsgConstant.KCHATTYPEGUILD
Scene.STRANGER_FROM_GROUP -> MsgConstant.KCHATTYPETEMPC2CFROMGROUP
Scene.NEARBY -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.STRANGER -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN
Scene.UNRECOGNIZED -> throw StatusRuntimeException(Status.INVALID_ARGUMENT.withDescription("Unrecognized scene"))
}, it.peer, it.subPeer
)
}
val msg: MsgRecord = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsByMsgId(contact, arrayListOf(request.messageId.toLong())) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: throw StatusRuntimeException(Status.NOT_FOUND.withDescription("Message not found"))
MessageHelper.setGroupMessageCommentFace(
request.contact.longPeer(),
msg.msgSeq.toULong(),
request.faceId.toString(),
request.isComment
)
return ReactMessageWithEmojiResponse.newBuilder().build()
}
}

View File

@ -0,0 +1,26 @@
package kritor.service
import com.google.protobuf.ByteString
import com.tencent.mobileqq.fe.FEKit
import com.tencent.mobileqq.qsec.qsecdandelionsdk.Dandelion
import io.kritor.developer.*
internal object QsignService: QsignServiceGrpcKt.QsignServiceCoroutineImplBase() {
@Grpc("QsignService", "Sign")
override suspend fun sign(request: SignRequest): SignResponse {
return SignResponse.newBuilder().apply {
val result = FEKit.getInstance().getSign(request.command, request.buffer.toByteArray(), request.seq, request.uin)
this.secSig = ByteString.copyFrom(result.sign)
this.secDeviceToken = ByteString.copyFrom(result.token)
this.secExtra = ByteString.copyFrom(result.extra)
}.build()
}
@Grpc("QsignService", "Energy")
override suspend fun energy(request: EnergyRequest): EnergyResponse {
return EnergyResponse.newBuilder().apply {
this.result = ByteString.copyFrom(Dandelion.getInstance().fly(request.data, request.salt.toByteArray()))
}.build()
}
}

View File

@ -0,0 +1,58 @@
package kritor.service
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.web.*
import qq.service.ticket.TicketHelper
internal object WebService: WebServiceGrpcKt.WebServiceCoroutineImplBase() {
@Grpc("WebService", "GetCookies")
override suspend fun getCookies(request: GetCookiesRequest): GetCookiesResponse {
return GetCookiesResponse.newBuilder().apply {
if (request.domain.isNullOrEmpty()) {
this.cookie = TicketHelper.getCookie()
} else {
this.cookie = TicketHelper.getCookie(request.domain)
}
}.build()
}
@Grpc("WebService", "GetCredentials")
override suspend fun getCredentials(request: GetCredentialsRequest): GetCredentialsResponse {
return GetCredentialsResponse.newBuilder().apply {
if (request.domain.isNullOrEmpty()) {
val uin = TicketHelper.getUin()
val skey = TicketHelper.getRealSkey(uin)
val pskey = TicketHelper.getPSKey(uin)
this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey;"
this.bkn = TicketHelper.getCSRF(pskey)
} else {
val uin = TicketHelper.getUin()
val skey = TicketHelper.getRealSkey(uin)
val pskey = TicketHelper.getPSKey(uin, request.domain) ?: ""
val pt4token = TicketHelper.getPt4Token(uin, request.domain) ?: ""
this.cookie = "o_cookie=$uin; ied_qq=o$uin; pac_uid=1_$uin; uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token;"
this.bkn = TicketHelper.getCSRF(pskey)
}
}.build()
}
@Grpc("WebService", "GetCSRFToken")
override suspend fun getCSRFToken(request: GetCSRFTokenRequest): GetCSRFTokenResponse {
return GetCSRFTokenResponse.newBuilder().apply {
if (request.domain.isNullOrEmpty()) {
this.bkn = TicketHelper.getCSRF()
} else {
this.bkn = TicketHelper.getCSRF(TicketHelper.getUin(), request.domain)
}
}.build()
}
@Grpc("WebService", "GetHttpCookies")
override suspend fun getHttpCookies(request: GetHttpCookiesRequest): GetHttpCookiesResponse {
return GetHttpCookiesResponse.newBuilder().apply {
this.cookie = TicketHelper.getHttpCookies(request.appid, request.daid, request.jumpUrl)
?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get http cookies"))
}.build()
}
}

View File

@ -2,5 +2,5 @@ package moe.fuqiuluo.shamrock.config
object EnableOldBDH: ConfigKey<Boolean>() {
override fun name() = "enable_old_bdh"
override fun default() = false
override fun default() = true
}

View File

@ -14,7 +14,10 @@ import moe.fuqiuluo.shamrock.tools.toast
import moe.fuqiuluo.shamrock.xposed.helper.AppTalker
import mqq.app.MobileQQ
import java.io.File
import java.util.Calendar
import java.util.Date
import java.util.Timer
import java.util.TimerTask
internal enum class Level(
val id: Byte
@ -31,7 +34,29 @@ internal object LogCenter {
// 格式化时间
SimpleDateFormat("yyyy-MM-dd").format(Date())
}_"
private val LogFile = MobileQQ.getContext().getExternalFilesDir(null)!!
private var LogFile = generateLogFile()
private val format = SimpleDateFormat("[HH:mm:ss] ")
private val timer = Timer()
init {
val now = Calendar.getInstance()
val tomorrowMidnight = Calendar.getInstance().apply {
add(Calendar.DAY_OF_YEAR, 1)
set(Calendar.HOUR_OF_DAY, 0)
set(Calendar.MINUTE, 0)
set(Calendar.SECOND, 0)
set(Calendar.MILLISECOND, 0)
}
val delay = tomorrowMidnight.timeInMillis - now.timeInMillis
timer.scheduleAtFixedRate(object : TimerTask() {
override fun run() {
LogFile = generateLogFile()
}
}, delay, 24 * 60 * 60 * 1000)
}
private fun generateLogFile() = MobileQQ.getContext().getExternalFilesDir(null)!!
.parentFile!!.resolve("Tencent/Shamrock/log").also {
if (it.exists()) it.delete()
it.mkdirs()
@ -49,8 +74,6 @@ internal object LogCenter {
return@let result
}
private val format = SimpleDateFormat("[HH:mm:ss] ")
fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) {
if (!ShamrockConfig[DebugMode] && level == Level.DEBUG) {
return

View File

@ -1,57 +1,60 @@
@file:OptIn(DelicateCoroutinesApi::class)
package moe.fuqiuluo.shamrock.internals
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import io.kritor.Scene
import io.kritor.contact
import io.kritor.event.MessageEvent
import io.kritor.event.messageEvent
import io.kritor.sender
import io.kritor.event.*
import io.kritor.common.PushMessageBody
import io.kritor.common.Contact
import io.kritor.common.Sender
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.io.core.BytePacketBuilder
import qq.service.QQInterfaces
import qq.service.msg.toKritorEventMessages
internal object GlobalEventTransmitter: QQInterfaces() {
private val messageEventFlow by lazy {
MutableSharedFlow<Pair<MsgRecord, MessageEvent>>()
internal object GlobalEventTransmitter : QQInterfaces() {
private val MessageEventFlow by lazy {
MutableSharedFlow<Pair<MsgRecord, PushMessageBody>>()
}
private val noticeEventFlow by lazy {
MutableSharedFlow<NoticeEvent>()
}
private val requestEventFlow by lazy {
MutableSharedFlow<RequestsEvent>()
}
//private val noticeEventFlow by lazy {
// MutableSharedFlow<NoticeEvent>()
//}
//private val requestEventFlow by lazy {
// MutableSharedFlow<RequestEvent>()
//}
//private suspend fun pushNotice(noticeEvent: NoticeEvent) = noticeEventFlow.emit(noticeEvent)
private suspend fun pushNotice(noticeEvent: NoticeEvent) = noticeEventFlow.emit(noticeEvent)
//private suspend fun pushRequest(requestEvent: RequestEvent) = requestEventFlow.emit(requestEvent)
private suspend fun pushRequest(requestEvent: RequestsEvent) = requestEventFlow.emit(requestEvent)
private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message)
private suspend fun transMessageEvent(record: MsgRecord, message: PushMessageBody) =
MessageEventFlow.emit(record to message)
object MessageTransmitter {
suspend fun transGroupMessage(
record: MsgRecord,
elements: ArrayList<MsgElement>,
): Boolean {
transMessageEvent(record, messageEvent {
transMessageEvent(record, PushMessageBody.newBuilder().apply {
this.time = record.msgTime.toInt()
this.scene = Scene.GROUP
this.messageId = record.msgId
this.messageId = record.msgId.toString()
this.messageSeq = record.msgSeq
this.contact = contact {
this.contact = Contact.newBuilder().apply {
this.scene = scene
this.peer = record.peerUin.toString()
this.subPeer = record.peerUid
}
this.sender = sender {
}.build()
this.sender = Sender.newBuilder().apply {
this.uin = record.senderUin
this.uid = record.senderUid
this.nick = record.sendNickName
}
})
}.build()
this.addAllElements(elements.toKritorEventMessages(record))
}.build())
return true
}
@ -59,9 +62,22 @@ internal object GlobalEventTransmitter: QQInterfaces() {
record: MsgRecord,
elements: ArrayList<MsgElement>,
): Boolean {
val botUin = app.longAccountUin
var nickName = record.sendNickName
transMessageEvent(record, PushMessageBody.newBuilder().apply {
this.time = record.msgTime.toInt()
this.messageId = record.msgId.toString()
this.messageSeq = record.msgSeq
this.contact = Contact.newBuilder().apply {
this.scene = scene
this.peer = record.senderUin.toString()
this.subPeer = record.senderUid
}.build()
this.sender = Sender.newBuilder().apply {
this.uin = record.senderUin
this.uid = record.senderUid
this.nick = record.sendNickName
}.build()
this.addAllElements(elements.toKritorEventMessages(record))
}.build())
return true
}
@ -71,9 +87,22 @@ internal object GlobalEventTransmitter: QQInterfaces() {
groupCode: Long,
fromNick: String,
): Boolean {
val botUin = app.longAccountUin
var nickName = record.sendNickName
transMessageEvent(record, PushMessageBody.newBuilder().apply {
this.time = record.msgTime.toInt()
this.messageId = record.msgId.toString()
this.messageSeq = record.msgSeq
this.contact = Contact.newBuilder().apply {
this.scene = scene
this.peer = record.senderUin.toString()
this.subPeer = groupCode.toString()
}.build()
this.sender = Sender.newBuilder().apply {
this.uin = record.senderUin
this.uid = record.senderUid
this.nick = record.sendNickName
}.build()
this.addAllElements(elements.toKritorEventMessages(record))
}.build())
return true
}
@ -81,17 +110,29 @@ internal object GlobalEventTransmitter: QQInterfaces() {
record: MsgRecord,
elements: ArrayList<MsgElement>,
): Boolean {
val botUin = app.longAccountUin
var nickName = record.sendNickName
transMessageEvent(record, PushMessageBody.newBuilder().apply {
this.time = record.msgTime.toInt()
this.messageId = record.msgId.toString()
this.messageSeq = record.msgSeq
this.contact = Contact.newBuilder().apply {
this.scene = scene
this.peer = record.guildId ?: ""
this.subPeer = record.channelId ?: ""
}.build()
this.sender = Sender.newBuilder().apply {
this.uin = record.senderUin
this.uid = record.senderUid
this.nick = record.sendNickName
}.build()
this.addAllElements(elements.toKritorEventMessages(record))
}.build())
return true
}
}
/*
/**
* 文件通知 通知器
*/
**/
object FileNoticeTransmitter {
/**
* 推送私聊文件事件
@ -106,23 +147,19 @@ internal object GlobalEventTransmitter: QQInterfaces() {
expireTime: Long,
url: String
): Boolean {
pushNotice(NoticeEvent(
time = msgTime,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.PrivateUpload,
operatorId = userId,
userId = userId,
senderId = userId,
privateFile = PrivateFileMsg(
id = fileId,
name = fileName,
size = fileSize,
url = url,
subId = fileSubId,
expire = expireTime
)
))
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.FRIEND_FILE_COME
this.time = msgTime.toInt()
this.friendFileUploaded = FriendFileUploadedNotice.newBuilder().apply {
this.fileId = fileId
this.fileName = fileName
this.operatorUin = userId
this.fileSize = fileSize
this.expireTime = expireTime.toInt()
this.fileSubId = fileSubId
this.url = url
}.build()
}.build())
return true
}
@ -139,22 +176,19 @@ internal object GlobalEventTransmitter: QQInterfaces() {
bizId: Int,
url: String
): Boolean {
pushNotice(NoticeEvent(
time = msgTime,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.GroupUpload,
operatorId = userId,
userId = userId,
groupId = groupId,
file = GroupFileMsg(
id = uuid,
name = fileName,
size = fileSize,
busid = bizId.toLong(),
url = url
)
))
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_FILE_COME
this.time = msgTime.toInt()
this.groupFileUploaded = GroupFileUploadedNotice.newBuilder().apply {
this.groupId = groupId
this.operatorUin = userId
this.fileId = uuid
this.fileName = fileName
this.fileSize = fileSize
this.biz = bizId
this.url = url
}.build()
}.build())
return true
}
}
@ -163,69 +197,96 @@ internal object GlobalEventTransmitter: QQInterfaces() {
* 群聊通知 通知器
*/
object GroupNoticeTransmitter {
suspend fun transGroupSign(time: Long, target: Long, action: String?, rankImg: String?, groupCode: Long): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.Notify,
subType = NoticeSubType.Sign,
userId = target,
groupId = groupCode,
target = target,
signDetail = SignDetail(
rankImg = rankImg,
action = action
)
))
suspend fun transGroupSign(
time: Long,
target: Long,
action: String?,
rankImg: String?,
groupCode: Long
): Boolean {
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_SIGN
this.time = time.toInt()
this.groupSignIn = GroupSignInNotice.newBuilder().apply {
this.groupId = groupCode
this.targetUin = target
this.action = action ?: ""
this.suffix = ""
this.rankImage = rankImg ?: ""
}.build()
}.build())
return true
}
suspend fun transGroupPoke(time: Long, operation: Long, target: Long, action: String?, suffix: String?, actionImg: String?, groupCode: Long): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.Notify,
subType = NoticeSubType.Poke,
operatorId = operation,
userId = operation,
groupId = groupCode,
target = target,
pokeDetail = PokeDetail(
action = action,
suffix = suffix,
actionImg = actionImg
)
))
suspend fun transGroupPoke(
time: Long,
operator: Long,
target: Long,
action: String?,
suffix: String?,
actionImg: String?,
groupCode: Long
): Boolean {
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_POKE
this.time = time.toInt()
this.groupPoke = GroupPokeNotice.newBuilder().apply {
this.groupId = groupCode
this.action = action ?: ""
this.targetUin = target
this.operatorUin = operator
this.suffix = suffix ?: ""
this.actionImage = actionImg ?: ""
}.build()
}.build())
return true
}
suspend fun transGroupMemberNumChanged(
suspend fun transGroupMemberNumIncreased(
time: Long,
target: Long,
targetUid: String,
groupCode: Long,
operator: Long,
operatorUid: String,
noticeType: NoticeType,
noticeSubType: NoticeSubType
type: GroupMemberIncreasedNotice.GroupMemberIncreasedType
): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = noticeType,
subType = noticeSubType,
operatorId = operator,
userId = target,
senderId = operator,
target = target,
groupId = groupCode,
targetUid = targetUid,
operatorUid = operatorUid,
userUid = targetUid
))
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_MEMBER_INCREASE
this.time = time.toInt()
this.groupMemberIncrease = GroupMemberIncreasedNotice.newBuilder().apply {
this.groupId = groupCode
this.operatorUid = operatorUid
this.operatorUin = operator
this.targetUid = targetUid
this.targetUin = target
this.type = type
}.build()
}.build())
return true
}
suspend fun transGroupMemberNumDecreased(
time: Long,
target: Long,
targetUid: String,
groupCode: Long,
operator: Long,
operatorUid: String,
type: GroupMemberDecreasedNotice.GroupMemberDecreasedType
): Boolean {
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_MEMBER_INCREASE
this.time = time.toInt()
this.groupMemberDecrease = GroupMemberDecreasedNotice.newBuilder().apply {
this.groupId = groupCode
this.operatorUid = operatorUid
this.operatorUin = operator
this.targetUid = targetUid
this.targetUin = target
this.type = type
}.build()
}.build())
return true
}
@ -236,25 +297,39 @@ internal object GlobalEventTransmitter: QQInterfaces() {
groupCode: Long,
setAdmin: Boolean
): Boolean {
pushNotice(NoticeEvent(
time = msgTime,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.GroupAdminChange,
subType = if (setAdmin) NoticeSubType.Set else NoticeSubType.UnSet,
operatorId = 0,
userId = target,
userUid = targetUid,
target = target,
targetUid = targetUid,
groupId = groupCode
))
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_ADMIN_CHANGED
this.time = msgTime.toInt()
this.groupAdminChange = GroupAdminChangedNotice.newBuilder().apply {
this.groupId = groupCode
this.targetUid = targetUid
this.targetUin = target
this.isAdmin = setAdmin
}.build()
}.build())
return true
}
suspend fun transGroupWholeBan(
msgTime: Long,
operator: Long,
groupCode: Long,
isOpen: Boolean
): Boolean {
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_WHOLE_BAN
this.time = msgTime.toInt()
this.groupWholeBan = GroupWholeBanNotice.newBuilder().apply {
this.groupId = groupCode
this.isBan = isOpen
this.operatorUin = operator
}.build()
}.build())
return true
}
suspend fun transGroupBan(
msgTime: Long,
subType: NoticeSubType,
operator: Long,
operatorUid: String,
target: Long,
@ -262,63 +337,64 @@ internal object GlobalEventTransmitter: QQInterfaces() {
groupCode: Long,
duration: Int
): Boolean {
pushNotice(NoticeEvent(
time = msgTime,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.GroupBan,
subType = subType,
operatorId = operator,
userId = target,
senderId = operator,
target = target,
groupId = groupCode,
duration = duration,
operatorUid = operatorUid,
targetUid = targetUid
))
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_MEMBER_BANNED
this.time = msgTime.toInt()
this.groupMemberBan = GroupMemberBanNotice.newBuilder().apply {
this.groupId = groupCode
this.operatorUid = operatorUid
this.operatorUin = operator
this.targetUid = targetUid
this.targetUin = target
this.duration = duration
this.type = if (duration > 0) GroupMemberBanNotice.GroupMemberBanType.BAN
else GroupMemberBanNotice.GroupMemberBanType.LIFT_BAN
}.build()
}.build())
return true
}
suspend fun transGroupMsgRecall(
time: Long,
operator: Long,
operatorUid: String,
target: Long,
targetUid: String,
groupCode: Long,
msgHash: Int,
msgId: Long,
tipText: String
): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.GroupRecall,
operatorId = operator,
userId = target,
msgId = msgHash,
tip = tipText,
groupId = groupCode
))
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_RECALL
this.time = time.toInt()
this.groupRecall = GroupRecallNotice.newBuilder().apply {
this.groupId = groupCode
this.operatorUid = operatorUid
this.operatorUin = operator
this.targetUid = targetUid
this.targetUin = target
this.messageId = msgId.toString()
this.tipText = tipText
}.build()
}.build())
return true
}
suspend fun transCardChange(
time: Long,
targetId: Long,
oldCard: String,
newCard: String,
groupId: Long
): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.GroupCard,
userId = targetId,
cardNew = newCard,
cardOld = oldCard,
groupId = groupId
))
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_CARD_CHANGED
this.time = time.toInt()
this.groupCardChanged = GroupCardChangedNotice.newBuilder().apply {
this.groupId = groupId
this.targetUin = targetId
this.newCard = newCard
}.build()
}.build())
return true
}
@ -328,16 +404,15 @@ internal object GlobalEventTransmitter: QQInterfaces() {
title: String,
groupId: Long
): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.Notify,
userId = targetId,
groupId = groupId,
title = title,
subType = NoticeSubType.Title
))
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_MEMBER_UNIQUE_TITLE_CHANGED
this.time = time.toInt()
this.groupMemberUniqueTitleChanged = GroupUniqueTitleChangedNotice.newBuilder().apply {
this.groupId = groupId
this.target = targetId
this.title = title
}.build()
}.build())
return true
}
@ -345,21 +420,21 @@ internal object GlobalEventTransmitter: QQInterfaces() {
time: Long,
senderUin: Long,
operatorUin: Long,
msgId: Int,
msgId: Long,
groupId: Long,
subType: NoticeSubType
subType: UInt
): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.Essence,
senderId = senderUin,
groupId = groupId,
operatorId = operatorUin,
msgId = msgId,
subType = subType
))
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_ESSENCE_CHANGED
this.time = time.toInt()
this.groupEssenceChanged = GroupEssenceMessageNotice.newBuilder().apply {
this.groupId = groupId
this.messageId = msgId.toString()
this.targetUin = senderUin
this.operatorUin = operatorUin
this.subType = subType.toInt()
}.build()
}.build())
return true
}
}
@ -368,37 +443,37 @@ internal object GlobalEventTransmitter: QQInterfaces() {
* 私聊通知 通知器
*/
object PrivateNoticeTransmitter {
suspend fun transPrivatePoke(msgTime: Long, operation: Long, target: Long, action: String?, suffix: String?, actionImg: String?): Boolean {
pushNotice(NoticeEvent(
time = msgTime,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.Notify,
subType = NoticeSubType.Poke,
operatorId = operation,
userId = operation,
senderId = operation,
target = target,
pokeDetail = PokeDetail(
actionImg = actionImg,
action = action,
suffix = suffix
)
))
suspend fun transPrivatePoke(
msgTime: Long,
operator: Long,
target: Long,
action: String?,
suffix: String?,
actionImg: String?
): Boolean {
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.FRIEND_POKE
this.time = msgTime.toInt()
this.friendPoke = FriendPokeNotice.newBuilder().apply {
this.action = action ?: ""
this.operatorUin = operator
this.suffix = suffix ?: ""
this.actionImage = actionImg ?: ""
}.build()
}.build())
return true
}
suspend fun transPrivateRecall(time: Long, operation: Long, msgHashId: Int, tipText: String): Boolean {
pushNotice(NoticeEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Notice,
type = NoticeType.FriendRecall,
operatorId = operation,
userId = operation,
msgId = msgHashId,
tip = tipText
))
suspend fun transPrivateRecall(time: Long, operator: Long, msgId: Long, tipText: String): Boolean {
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.FRIEND_RECALL
this.time = time.toInt()
this.friendRecall = FriendRecallNotice.newBuilder().apply {
this.operatorUin = operator
this.messageId = msgId.toString()
this.tipText = tipText
}.build()
}.build())
return true
}
@ -408,53 +483,50 @@ internal object GlobalEventTransmitter: QQInterfaces() {
* 请求 通知器
*/
object RequestTransmitter {
suspend fun transFriendApp(time: Long, operation: Long, tipText: String, flag: String): Boolean {
pushRequest(RequestEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Request,
type = RequestType.Friend,
userId = operation,
comment = tipText,
flag = flag
))
suspend fun transFriendApp(time: Long, operator: Long, tipText: String, flag: String): Boolean {
pushRequest(RequestsEvent.newBuilder().apply {
this.type = RequestsEvent.RequestType.FRIEND_APPLY
this.time = time.toInt()
this.friendApply = FriendApplyRequest.newBuilder().apply {
this.applierUin = operator
this.message = tipText
this.flag = flag
}.build()
}.build())
return true
}
suspend fun transGroupApply(
time: Long,
applier: Long,
applierUin: Long,
applierUid: String,
reason: String,
groupCode: Long,
flag: String,
subType: RequestSubType
flag: String
): Boolean {
pushRequest(RequestEvent(
time = time,
selfId = app.longAccountUin,
postType = PostType.Request,
type = RequestType.Group,
userId = applier,
userUid = applierUid,
comment = reason,
groupId = groupCode,
subType = subType,
flag = flag
))
pushRequest(RequestsEvent.newBuilder().apply {
this.type = RequestsEvent.RequestType.GROUP_APPLY
this.time = time.toInt()
this.groupApply = GroupApplyRequest.newBuilder().apply {
this.applierUid = applierUid
this.applierUin = applierUin
this.groupId = groupCode
this.reason = reason
this.flag = flag
}.build()
}.build())
return true
}
}*/
}
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, MessageEvent>>) {
messageEventFlow.collect {
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, PushMessageBody>>) {
MessageEventFlow.collect {
GlobalScope.launch {
collector.emit(it)
}
}
}
/*
suspend inline fun onNoticeEvent(collector: FlowCollector<NoticeEvent>) {
noticeEventFlow.collect {
GlobalScope.launch {
@ -463,11 +535,11 @@ internal object GlobalEventTransmitter: QQInterfaces() {
}
}
suspend inline fun onRequestEvent(collector: FlowCollector<RequestEvent>) {
suspend inline fun onRequestEvent(collector: FlowCollector<RequestsEvent>) {
requestEventFlow.collect {
GlobalScope.launch {
collector.emit(it)
}
}
}*/
}
}

View File

@ -7,6 +7,7 @@ import android.content.pm.PackageManager
import android.content.pm.VersionedPackage
import android.os.Build
import android.os.Looper
import com.tencent.qphone.base.remote.ToServiceMsg
import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XSharedPreferences
import de.robv.android.xposed.XposedHelpers
@ -21,6 +22,8 @@ import moe.fuqiuluo.shamrock.xposed.XposedEntry
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
import moe.fuqiuluo.symbols.XposedHook
import mqq.app.MobileQQ
import qq.service.QQInterfaces
@XposedHook(priority = 0)
class AntiDetection: IAction {
@ -36,6 +39,17 @@ class AntiDetection: IAction {
if (ShamrockConfig[AntiJvmTrace])
antiTrace()
antiMemoryWalking()
//antiO3Report()
}
private fun antiO3Report() {
QQInterfaces.app.javaClass.hookMethod("sendToService").before {
val toServiceMsg = it.args[0] as ToServiceMsg?
if (toServiceMsg != null && toServiceMsg.serviceCmd.startsWith("trpc.o3")) {
LogCenter.log("拦截trpc.o3环境上报包", Level.WARN)
it.result = null
}
}
}
private fun antiGetPackageGidsDetection(ctx: Context) {

View File

@ -6,8 +6,11 @@ import android.content.Context
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kritor.client.KritorClient
import kritor.server.KritorServer
import moe.fuqiuluo.shamrock.config.ActiveRPC
import moe.fuqiuluo.shamrock.config.PassiveRPC
import moe.fuqiuluo.shamrock.config.RPCAddress
import moe.fuqiuluo.shamrock.config.RPCPort
import moe.fuqiuluo.shamrock.config.ShamrockConfig
import moe.fuqiuluo.shamrock.config.get
@ -17,6 +20,7 @@ import moe.fuqiuluo.symbols.Process
import moe.fuqiuluo.symbols.XposedHook
private lateinit var server: KritorServer
private lateinit var client: KritorClient
@XposedHook(Process.MAIN, priority = 10)
internal class InitRemoteService : IAction {
@ -32,6 +36,21 @@ internal class InitRemoteService : IAction {
LogCenter.log("ActiveRPC is disabled, KritorServer will not be started.")
}
if (PassiveRPC.get()) {
if (!::client.isInitialized) {
val hostAndPort = RPCAddress.get().split(":").let {
it.first() to it.last().toInt()
}
LogCenter.log("Connect RPC to ${hostAndPort.first}:${hostAndPort.second}")
client = KritorClient(hostAndPort.first, hostAndPort.second)
client.start()
client.listen()
}
} else {
LogCenter.log("PassiveRPC is disabled, KritorServer will not be started.")
}
}.onFailure {
LogCenter.log("Start RPC failed: ${it.message}", Level.ERROR)

View File

@ -9,11 +9,13 @@ import qq.service.QQInterfaces
object SwitchStatus: IInteract, QQInterfaces() {
override fun invoke(intent: Intent) {
AppTalker.talk("switch_status") {
put("account", app.currentAccountUin)
put("nickname", if (app is QQAppInterface) app.currentNickname else "unknown")
put("voice", NativeLoader.isVoiceLoaded)
put("core_version", ShamrockVersion)
if (app.isLogin) {
AppTalker.talk("switch_status") {
put("account", app.currentAccountUin)
put("nickname", if (app is QQAppInterface) (app.currentNickname ?: "unknown") else "unknown")
put("voice", NativeLoader.isVoiceLoaded)
put("core_version", ShamrockVersion)
}
}
}
}

View File

@ -83,6 +83,17 @@ abstract class QQInterfaces {
app.sendToService(to)
}
fun sendBuffer(
cmd: String,
isProto: Boolean,
data: ByteArray,
) {
val toServiceMsg = createToServiceMsg(cmd)
toServiceMsg.putWupBuffer(data)
toServiceMsg.addAttribute("req_pb_protocol_flag", isProto)
sendToServiceMsg(toServiceMsg)
}
@DelicateCoroutinesApi
suspend fun sendBufferAW(
cmd: String,

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,505 @@
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)
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,
chatType: Int,
sceneBuilder: suspend SceneInfo.() -> Unit
): Result<UploadRsp> {
return runCatching {
requestUploadNtPic(file, md5, sha, name, width, height, chatType, sceneBuilder).getOrThrow()
}.onFailure {
if (retryCnt > 0) {
return requestUploadNtPic(file, md5, sha, name, width, height, retryCnt - 1, chatType, sceneBuilder)
}
}
}
private suspend fun requestUploadNtPic(
file: File,
md5: String,
sha: String,
name: String,
width: UInt,
height: UInt,
chatType: Int,
sceneBuilder: suspend SceneInfo.() -> Unit
): Result<UploadRsp> {
val req = NtV2RichMediaReq(
head = MultiMediaReqHead(
commonHead = CommonHead(
requestId = requestIdSeq.incrementAndGet().toULong(),
cmd = 100u
),
sceneInfo = SceneInfo(
requestType = 2u,
businessType = 1u,
).apply {
sceneBuilder()
},
clientMeta = ClientMeta(2u)
),
upload = UploadReq(
listOf(UploadInfo(
FileInfo(
fileSize = file.length().toULong(),
md5 = md5,
sha1 = sha,
name = name,
fileType = FileType(
fileType = 1u,
picFormat = 1000u,
videoFormat = 0u,
voiceFormat = 0u
),
width = width,
height = height,
time = 0u,
original = 1u
),
subFileType = 0u
)),
tryFastUploadCompleted = true,
srvSendMsg = false,
clientRandomId = Random.nextULong(),
compatQMsgSceneType = 2u,
clientSeq = Random.nextUInt(),
noNeedCompatMsg = true
)
).toByteArray()
val fromServiceMsg = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> {
sendOidbAW("OidbSvcTrpcTcp.0x11c4_100", 4548, 100, req, true, timeout = 3.seconds)
}
MsgConstant.KCHATTYPEC2C -> {
sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true, timeout = 3.seconds)
}
else -> return Result.failure(Exception("unknown chat type: $chatType"))
}
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,58 @@
package qq.service.bdh
import com.tencent.mobileqq.data.MessageRecord
import java.io.File
internal enum class ContactType {
TROOP,
PRIVATE,
}
internal interface TransTarget {
val id: String
val type: ContactType
val mRec: MessageRecord?
}
internal class Troop(
override val id: String,
override val mRec: MessageRecord? = null
): TransTarget {
override val type: ContactType = ContactType.TROOP
}
internal class Private(
override val id: String,
override val mRec: MessageRecord? = null
): TransTarget {
override val type: ContactType = ContactType.PRIVATE
}
internal enum class ResourceType {
Picture,
Video,
Voice
}
internal interface Resource {
val type: ResourceType
}
internal data class PictureResource(
val src: File
): Resource {
override val type = ResourceType.Picture
}
internal data class VideoResource(
val src: File, val thumb: File
): Resource {
override val type = ResourceType.Video
}
internal data class VoiceResource(
val src: File
): Resource {
override val type = ResourceType.Voice
}

View File

@ -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,135 @@
package qq.service.bdh
import com.tencent.mobileqq.data.MessageForShortVideo
import com.tencent.mobileqq.data.MessageRecord
import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.TransferRequest
import moe.fuqiuluo.shamrock.utils.MD5
import qq.service.bdh.ResourceType.*
import java.io.File
internal object Transfer: FileTransfer() {
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
ContactType.TROOP to mapOf(
Picture to { uploadGroupPic(id, (it as PictureResource).src, mRec) },
Voice to { uploadGroupVoice(id, (it as VoiceResource).src) },
Video to { uploadGroupVideo(id, (it as VideoResource).src, it.thumb) },
),
ContactType.PRIVATE to mapOf(
Picture to { uploadC2CPic(id, (it as PictureResource).src, mRec) },
Voice to { uploadC2CVoice(id, (it as VoiceResource).src) },
Video to { uploadC2CVideo(id, (it as VideoResource).src, it.thumb) },
)
)
suspend fun uploadC2CVideo(
userId: String,
file: File,
thumb: File,
wait: Boolean = true
): Boolean {
return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_C2C, BUSI_TYPE_SHORT_VIDEO, wait) {
it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4
it.mRec = MessageForShortVideo().also {
it.busiType = BUSI_TYPE_SHORT_VIDEO
}
it.mThumbPath = thumb.absolutePath
it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath)
}
}
suspend fun uploadGroupVideo(
groupId: String,
file: File,
thumb: File,
wait: Boolean = true
): Boolean {
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_SHORT_VIDEO_TROOP, BUSI_TYPE_SHORT_VIDEO, wait) {
it.mSourceVideoCodecFormat = VIDEO_FORMAT_MP4
it.mRec = MessageForShortVideo().also {
it.busiType = BUSI_TYPE_SHORT_VIDEO
}
it.mThumbPath = thumb.absolutePath
it.mThumbMd5 = MD5.genFileMd5Hex(thumb.absolutePath)
}
}
suspend fun uploadC2CVoice(
userId: String,
file: File,
wait: Boolean = true
): Boolean {
return transC2CResource(userId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) {
it.mPttUploadPanel = 3
it.mPttCompressFinish = true
it.mIsPttPreSend = true
}
}
suspend fun uploadGroupVoice(
groupId: String,
file: File,
wait: Boolean = true
): Boolean {
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PTT, 1002, wait) {
it.mPttUploadPanel = 3
it.mPttCompressFinish = true
it.mIsPttPreSend = true
}
}
suspend fun uploadC2CPic(
peerId: String,
file: File,
record: MessageRecord? = null,
wait: Boolean = true
): Boolean {
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
picUpExtraInfo.mIsRaw = false
picUpExtraInfo.mUinType = FileMsg.UIN_BUDDY
it.mPicSendSource = 8
it.mExtraObj = picUpExtraInfo
it.mIsPresend = true
it.delayShowProgressTimeInMs = 2000
it.mRec = record
}
}
suspend fun uploadGroupPic(
groupId: String,
file: File,
record: MessageRecord? = null,
wait: Boolean = true
): Boolean {
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
picUpExtraInfo.mIsRaw = false
picUpExtraInfo.mUinType = FileMsg.UIN_TROOP
it.mPicSendSource = 8
it.delayShowProgressTimeInMs = 2000
it.mExtraObj = picUpExtraInfo
it.mRec = record
}
}
operator fun get(contactType: ContactType, resourceType: ResourceType): suspend TransTarget.(Resource) -> Boolean {
return (ROUTE[contactType] ?: error("unsupported contact type: $contactType"))[resourceType]
?: error("Unsupported resource type: $resourceType")
}
}
internal suspend infix fun TransferTaskBuilder.trans(res: Resource): Boolean {
return Transfer[contact.type, res.type](contact, res)
}
internal class TransferTaskBuilder {
lateinit var contact: TransTarget
}
internal infix fun Transfer.with(contact: TransTarget): TransferTaskBuilder {
return TransferTaskBuilder().also {
it.contact = contact
}
}

View File

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

@ -0,0 +1,21 @@
package qq.service.contact
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import io.kritor.common.Scene
suspend fun Contact.longPeer(): Long {
return when(this.chatType) {
MsgConstant.KCHATTYPEGROUP -> peerUid.toLong()
MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> if (peerUid.startsWith("u_")) ContactHelper.getUinByUidAsync(peerUid).toLong() else peerUid.toLong()
else -> 0L
}
}
suspend fun io.kritor.common.Contact.longPeer(): Long {
return when(this.scene) {
Scene.GROUP -> peer.toLong()
Scene.FRIEND, Scene.STRANGER, Scene.STRANGER_FROM_GROUP -> if (peer.startsWith("u_")) ContactHelper.getUinByUidAsync(peer).toLong() else peer.toLong()
else -> 0L
}
}

View File

@ -7,11 +7,15 @@ import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.PARAM_SELF_UIN
import com.tencent.mobileqq.profilecard.api.IProfileProtocolConst.PARAM_TARGET_UIN
import com.tencent.mobileqq.profilecard.api.IProfileProtocolService
import com.tencent.mobileqq.profilecard.observer.ProfileCardObserver
import com.tencent.protofile.join_group_link.join_group_link
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.fuqiuluo.shamrock.tools.slice
import qq.service.internals.NTServiceFetcher
import qq.service.QQInterfaces
import tencent.im.oidb.cmd0x11b2.oidb_0x11b2
import tencent.im.oidb.oidb_sso
import kotlin.coroutines.resume
internal object ContactHelper: QQInterfaces() {
@ -177,4 +181,31 @@ internal object ContactHelper: QQInterfaces() {
}
}[peerId]!!
}
suspend fun getSharePrivateArkMsg(peerId: Long): String {
val reqBody = oidb_0x11b2.BusinessCardV3Req()
reqBody.uin.set(peerId)
reqBody.jump_url.set("mqqapi://card/show_pslcard?src_type=internal&source=sharecard&version=1&uin=$peerId")
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x11ca_0", 4790, 0, reqBody.toByteArray())
?: error("unable to fetch contact ark_json_text")
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val rsp = oidb_0x11b2.BusinessCardV3Rsp()
rsp.mergeFrom(body.bytes_bodybuffer.get().toByteArray())
return rsp.signed_ark_msg.get()
}
suspend fun getShareTroopArkMsg(groupId: Long): String {
val reqBody = join_group_link.ReqBody()
reqBody.get_ark.set(true)
reqBody.type.set(1)
reqBody.group_code.set(groupId)
val fromServiceMsg = sendBufferAW("GroupSvc.JoinGroupLink", true, reqBody.toByteArray())
?: error("unable to fetch contact ark_json_text")
val body = join_group_link.RspBody()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
return body.signed_ark.get().toStringUtf8()
}
}

View File

@ -5,15 +5,7 @@ package qq.service.file
import com.tencent.mobileqq.pb.ByteStringMicro
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.file.File
import io.kritor.file.Folder
import io.kritor.file.GetFileSystemInfoResponse
import io.kritor.file.GetFilesRequest
import io.kritor.file.GetFilesResponse
import io.kritor.file.folder
import io.kritor.file.getFileSystemInfoResponse
import io.kritor.file.getFilesRequest
import io.kritor.file.getFilesResponse
import io.kritor.file.*
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
@ -33,7 +25,7 @@ internal object GroupFileHelper: QQInterfaces() {
it.uint32_bus_id.set(0)
})
}.toByteArray()) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (!fromServiceMsg.isSuccess) {
if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
}
val fileCnt: Int
@ -73,15 +65,15 @@ internal object GroupFileHelper: QQInterfaces() {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response x2"))
}
return getFileSystemInfoResponse {
return GetFileSystemInfoResponse.newBuilder().apply {
this.fileCount = fileCnt
this.totalCount = limitCnt
this.totalSpace = totalSpace.toInt()
this.usedSpace = usedSpace.toInt()
}
}.build()
}
suspend fun getGroupFiles(groupId: Long, folderId: String = "/"): GetFilesResponse {
suspend fun getGroupFiles(groupId: Long, folderId: String = "/"): GetFileListResponse {
val fileSystemInfo = getGroupFileSystemInfo(groupId)
val fromServiceMsg = sendOidbAW("OidbSvc.0x6d8_1", 1752, 1, oidb_0x6d8.ReqBody().also {
it.file_list_info_req.set(oidb_0x6d8.GetFileListReqBody().apply {
@ -104,11 +96,11 @@ internal object GroupFileHelper: QQInterfaces() {
uint32_show_onlinedoc_folder.set(0)
})
}.toByteArray(), timeout = 15.seconds) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
if (!fromServiceMsg.isSuccess) {
if (fromServiceMsg.wupBuffer == null) {
throw StatusRuntimeException(Status.INTERNAL.withDescription("oidb request failed"))
}
val files = arrayListOf<File>()
val dirs = arrayListOf<Folder>()
val folders = arrayListOf<Folder>()
if (fromServiceMsg.wupBuffer != null) {
val oidb = oidb_sso.OIDBSSOPkg().mergeFrom(fromServiceMsg.wupBuffer.slice(4).let {
if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it
@ -119,7 +111,7 @@ internal object GroupFileHelper: QQInterfaces() {
rpt_item_list.get().forEach { file ->
if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FILE) {
val fileInfo = file.file_info
files.add(io.kritor.file.file {
files.add(File.newBuilder().apply {
this.fileId = fileInfo.str_file_id.get()
this.fileName = fileInfo.str_file_name.get()
this.fileSize = fileInfo.uint64_file_size.get()
@ -133,18 +125,18 @@ internal object GroupFileHelper: QQInterfaces() {
this.sha = fileInfo.bytes_sha.get().toByteArray().toHexString()
this.sha3 = fileInfo.bytes_sha3.get().toByteArray().toHexString()
this.md5 = fileInfo.bytes_md5.get().toByteArray().toHexString()
})
}.build())
}
else if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FOLDER) {
val folderInfo = file.folder_info
dirs.add(folder {
folders.add(Folder.newBuilder().apply {
this.folderId = folderInfo.str_folder_id.get()
this.folderName = folderInfo.str_folder_name.get()
this.totalFileCount = folderInfo.uint32_total_file_count.get()
this.createTime = folderInfo.uint32_create_time.get()
this.creator = folderInfo.uint64_create_uin.get()
this.creatorName = folderInfo.str_creator_name.get()
})
}.build())
} else {
LogCenter.log("未知文件类型: ${file.uint32_type.get()}", Level.WARN)
}
@ -154,9 +146,9 @@ internal object GroupFileHelper: QQInterfaces() {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to fetch oidb response"))
}
return getFilesResponse {
this.files.addAll(files)
this.folders.addAll(folders)
}
return GetFileListResponse.newBuilder().apply {
this.addAllFiles(files)
this.addAllFolders(folders)
}.build()
}
}

View File

@ -3,11 +3,15 @@ package qq.service.friend
import com.tencent.mobileqq.data.Friends
import com.tencent.mobileqq.friend.api.IFriendDataService
import com.tencent.mobileqq.friend.api.IFriendHandlerService
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.mobileqq.relation.api.IAddFriendTempApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import moe.fuqiuluo.shamrock.tools.slice
import qq.service.QQInterfaces
import tencent.mobileim.structmsg.structmsg
import kotlin.coroutines.resume
internal object FriendHelper: QQInterfaces() {
@ -21,6 +25,69 @@ internal object FriendHelper: QQInterfaces() {
return Result.success(service.allFriends)
}
// ProfileService.Pb.ReqSystemMsgAction.Friend
fun requestFriendRequest(msgSeq: Long, uin: Long, remark: String = "", approve: Boolean? = true, notSee: Boolean? = false) {
val service = QRoute.api(IAddFriendTempApi::class.java)
val action = structmsg.SystemMsgActionInfo()
action.type.set(if (approve != false) 2 else 3)
action.group_id.set(0)
action.remark.set(remark)
val snInfo = structmsg.AddFrdSNInfo()
snInfo.uint32_not_see_dynamic.set(if (notSee != false) 1 else 0)
snInfo.uint32_set_sn.set(0)
action.addFrdSNInfo.set(snInfo)
service.sendFriendSystemMsgAction(1, msgSeq, uin, 1, 2004, 11, 0, action, 0, structmsg.StructMsg(), false, app)
}
suspend fun requestFriendSystemMsgNew(msgNum: Int, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 3): List<structmsg.StructMsg>? {
if (retryCnt < 0) {
return ArrayList()
}
val req = structmsg.ReqSystemMsgNew()
req.msg_num.set(msgNum)
req.latest_friend_seq.set(latestFriendSeq)
req.latest_group_seq.set(latestGroupSeq)
req.version.set(1000)
req.checktype.set(2)
val flag = structmsg.FlagInfo()
// flag.GrpMsg_Kick_Admin.set(1)
// flag.GrpMsg_HiddenGrp.set(1)
// flag.GrpMsg_WordingDown.set(1)
flag.FrdMsg_GetBusiCard.set(1)
// flag.GrpMsg_GetOfficialAccount.set(1)
// flag.GrpMsg_GetPayInGroup.set(1)
flag.FrdMsg_Discuss2ManyChat.set(1)
// flag.GrpMsg_NotAllowJoinGrp_InviteNotFrd.set(1)
flag.FrdMsg_NeedWaitingMsg.set(1)
flag.FrdMsg_uint32_need_all_unread_msg.set(1)
// flag.GrpMsg_NeedAutoAdminWording.set(1)
// flag.GrpMsg_get_transfer_group_msg_flag.set(1)
// flag.GrpMsg_get_quit_pay_group_msg_flag.set(1)
// flag.GrpMsg_support_invite_auto_join.set(1)
// flag.GrpMsg_mask_invite_auto_join.set(1)
// flag.GrpMsg_GetDisbandedByAdmin.set(1)
flag.GrpMsg_GetC2cInviteJoinGroup.set(1)
req.flag.set(flag)
req.is_get_frd_ribbon.set(false)
req.is_get_grp_ribbon.set(false)
req.friend_msg_type_flag.set(1)
req.uint32_req_msg_type.set(1)
req.uint32_need_uid.set(1)
val fromServiceMsg = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Friend", true, req.toByteArray())
return if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
ArrayList()
} else {
try {
val msg = structmsg.RspSystemMsgNew()
msg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
return msg.friendmsgs.get()
} catch (err: Throwable) {
requestFriendSystemMsgNew(msgNum, latestFriendSeq, latestGroupSeq, retryCnt - 1)
}
}
}
private suspend fun requestFriendList(dataService: IFriendDataService): Boolean {
val service = app.getRuntimeService(IFriendHandlerService::class.java, "all")
service.requestFriendList(true, 0)

View File

@ -37,10 +37,12 @@ import tencent.im.oidb.cmd0x8fc.Oidb_0x8fc
import tencent.im.oidb.cmd0xed3.oidb_cmd0xed3
import tencent.im.oidb.oidb_sso
import tencent.im.troop.honor.troop_honor
import tencent.mobileim.structmsg.structmsg
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.nio.ByteBuffer
import kotlin.coroutines.resume
import kotlin.time.Duration.Companion.seconds
internal object GroupHelper: QQInterfaces() {
private val RefreshTroopMemberInfoLock by lazy { Mutex() }
@ -98,6 +100,61 @@ internal object GroupHelper: QQInterfaces() {
return Result.success(troopList)
}
suspend fun getTroopMemberInfoByUinV2(
groupId: String,
uin: String,
refresh: Boolean = false
): Result<TroopMemberInfo> {
val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
var info = service.getTroopMember(groupId, uin)
if (refresh || !service.isMemberInCache(groupId, uin) || info == null || info.troopnick == null) {
info = requestTroopMemberInfo(service, groupId, uin, timeout = 2000).getOrNull()
}
if (info == null) {
info = getTroopMemberInfoByUinViaNt(groupId, uin, timeout = 2000L).getOrNull()?.let {
TroopMemberInfo().apply {
troopnick = it.cardName
friendnick = it.nick
}
}
}
try {
if (info != null && (info.alias == null || info.alias.isBlank())) {
val req = group_member_info.ReqBody()
req.uint64_group_code.set(groupId.toLong())
req.uint64_uin.set(uin.toLong())
req.bool_new_client.set(true)
req.uint32_client_type.set(1)
req.uint32_rich_card_name_ver.set(1)
val fromServiceMsg = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray(), timeout = 2.seconds)
if (fromServiceMsg != null && fromServiceMsg.wupBuffer != null) {
val rsp = group_member_info.RspBody()
rsp.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
if (rsp.msg_meminfo.str_location.has()) {
info.alias = rsp.msg_meminfo.str_location.get().toStringUtf8()
}
if (rsp.msg_meminfo.uint32_age.has()) {
info.age = rsp.msg_meminfo.uint32_age.get().toByte()
}
if (rsp.msg_meminfo.bytes_group_honor.has()) {
val honorBytes = rsp.msg_meminfo.bytes_group_honor.get().toByteArray()
val honor = troop_honor.GroupUserCardHonor()
honor.mergeFrom(honorBytes)
info.level = honor.level.get()
// 10315: medal_id not real group level
}
}
}
} catch (err: Throwable) {
LogCenter.log(err.stackTraceToString(), Level.WARN)
}
return if (info != null) {
Result.success(info)
} else {
Result.failure(Exception("获取群成员信息失败"))
}
}
private suspend fun requestGroupInfo(
service: ITroopInfoService
): Boolean {
@ -217,12 +274,112 @@ internal object GroupHelper: QQInterfaces() {
sendOidb("OidbSvc.0x55c_1", 1372, 1, array)
}
suspend fun setGroupUniqueTitle(groupId: Long, userId: Long, title: String) {
// ProfileService.Pb.ReqSystemMsgAction.Group
suspend fun requestGroupRequest(
msgSeq: Long,
uin: Long,
gid: Long,
msg: String? = "",
approve: Boolean? = true,
notSee: Boolean? = false,
subType: String
): Result<String>{
val req = structmsg.ReqSystemMsgAction().apply {
if (subType == "invite") {
msg_type.set(1)
src_id.set(3)
sub_src_id.set(10016)
group_msg_type.set(2)
} else {
msg_type.set(2)
src_id.set(2)
sub_src_id.set(30024)
group_msg_type.set(1)
}
msg_seq.set(msgSeq)
req_uin.set(uin)
sub_type.set(1)
action_info.set(structmsg.SystemMsgActionInfo().apply {
type.set(if (approve != false) 11 else 12)
group_code.set(gid)
if (subType == "add") {
this.msg.set(msg)
this.blacklist.set(notSee != false)
}
})
language.set(1000)
}
val fromServiceMsg = sendBufferAW("ProfileService.Pb.ReqSystemMsgAction.Group", true, req.toByteArray())
?: return Result.failure(Exception("ReqSystemMsgAction.Group: No Response"))
if (fromServiceMsg.wupBuffer == null) {
return Result.failure(Exception("ReqSystemMsgAction.Group: No WupBuffer"))
}
val rsp = structmsg.RspSystemMsgAction().mergeFrom(fromServiceMsg.wupBuffer.slice(4))
return if (rsp.head.result.has()) {
if (rsp.head.result.get() == 0) {
Result.success(rsp.msg_detail.get())
} else {
Result.failure(Exception(rsp.head.msg_fail.get()))
}
} else {
Result.failure(Exception("操作失败"))
}
}
suspend fun requestGroupSystemMsgNew(msgNum: Int, reqMsgType: Int = 1, latestFriendSeq: Long = 0, latestGroupSeq: Long = 0, retryCnt: Int = 5): List<structmsg.StructMsg> {
if (retryCnt < 0) {
return ArrayList()
}
val req = structmsg.ReqSystemMsgNew()
req.msg_num.set(msgNum)
req.latest_friend_seq.set(latestFriendSeq)
req.latest_group_seq.set(latestGroupSeq)
req.version.set(1000)
req.checktype.set(3)
val flag = structmsg.FlagInfo()
flag.GrpMsg_Kick_Admin.set(1)
flag.GrpMsg_HiddenGrp.set(1)
flag.GrpMsg_WordingDown.set(1)
// flag.FrdMsg_GetBusiCard.set(1)
flag.GrpMsg_GetOfficialAccount.set(1)
flag.GrpMsg_GetPayInGroup.set(1)
flag.FrdMsg_Discuss2ManyChat.set(1)
flag.GrpMsg_NotAllowJoinGrp_InviteNotFrd.set(1)
flag.FrdMsg_NeedWaitingMsg.set(1)
// flag.FrdMsg_uint32_need_all_unread_msg.set(1)
flag.GrpMsg_NeedAutoAdminWording.set(1)
flag.GrpMsg_get_transfer_group_msg_flag.set(1)
flag.GrpMsg_get_quit_pay_group_msg_flag.set(1)
flag.GrpMsg_support_invite_auto_join.set(1)
flag.GrpMsg_mask_invite_auto_join.set(1)
flag.GrpMsg_GetDisbandedByAdmin.set(1)
flag.GrpMsg_GetC2cInviteJoinGroup.set(1)
req.flag.set(flag)
req.is_get_frd_ribbon.set(false)
req.is_get_grp_ribbon.set(false)
req.friend_msg_type_flag.set(1)
req.uint32_req_msg_type.set(reqMsgType)
req.uint32_need_uid.set(1)
val fromServiceMsg = sendBufferAW("ProfileService.Pb.ReqSystemMsgNew.Group", true, req.toByteArray())
return if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
ArrayList()
} else {
try {
val msg = structmsg.RspSystemMsgNew()
msg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
return msg.groupmsgs.get().orEmpty()
} catch (err: Throwable) {
requestGroupSystemMsgNew(msgNum, reqMsgType, latestFriendSeq, latestGroupSeq, retryCnt - 1)
}
}
}
suspend fun setGroupUniqueTitle(groupId: String, userId: String, title: String) {
val localMemberInfo = getTroopMemberInfoByUin(groupId, userId, true).getOrThrow()
val req = Oidb_0x8fc.ReqBody()
req.uint64_group_code.set(groupId)
req.uint64_group_code.set(groupId.toLong())
val memberInfo = Oidb_0x8fc.MemberInfo()
memberInfo.uint64_uin.set(userId)
memberInfo.uint64_uin.set(userId.toLong())
memberInfo.bytes_uin_name.set(ByteStringMicro.copyFromUtf8(localMemberInfo.troopnick.ifEmpty {
localMemberInfo.troopremark.ifNullOrEmpty { "" }
}))
@ -269,7 +426,7 @@ internal object GroupHelper: QQInterfaces() {
uint32_shutup_timestap.set(0)
})
}.toByteArray()) ?: return Result.failure(RuntimeException("[oidb] timeout"))
if (!fromServiceMsg.isSuccess) {
if (fromServiceMsg.wupBuffer == null) {
return Result.failure(RuntimeException("[oidb] failed"))
}
val body = oidb_sso.OIDBSSOPkg()
@ -291,7 +448,7 @@ internal object GroupHelper: QQInterfaces() {
uint64_uin.set(app.longAccountUin)
uint64_group_code.set(groupId)
}.toByteArray(), trpc = true) ?: return Result.failure(RuntimeException("[oidb] timeout"))
if (!fromServiceMsg.isSuccess) {
if (fromServiceMsg.wupBuffer == null) {
return Result.failure(RuntimeException("[oidb] failed"))
}
val body = oidb_sso.OIDBSSOPkg()
@ -311,7 +468,7 @@ internal object GroupHelper: QQInterfaces() {
toServiceMsg.extraData.putBoolean("is_admin", false)
toServiceMsg.extraData.putInt("from", 0)
val fromServiceMsg = sendToServiceMsgAW(toServiceMsg) ?: return@timeout Result.failure(Exception("获取群信息超时"))
if (!fromServiceMsg.isSuccess) {
if (fromServiceMsg.wupBuffer == null) {
return@timeout Result.failure(Exception("获取群信息失败"))
}
val uniPacket = UniPacket(true)
@ -367,13 +524,13 @@ internal object GroupHelper: QQInterfaces() {
}
suspend fun getTroopMemberInfoByUin(
groupId: Long,
uin: Long,
groupId: String,
uin: String,
refresh: Boolean = false
): Result<TroopMemberInfo> {
val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
var info = service.getTroopMember(groupId.toString(), uin.toString())
if (refresh || !service.isMemberInCache(groupId.toString(), uin.toString()) || info == null || info.troopnick == null) {
var info = service.getTroopMember(groupId, uin)
if (refresh || !service.isMemberInCache(groupId, uin) || info == null || info.troopnick == null) {
info = requestTroopMemberInfo(service, groupId, uin).getOrNull()
}
if (info == null) {
@ -387,13 +544,13 @@ internal object GroupHelper: QQInterfaces() {
try {
if (info != null && (info.alias == null || info.alias.isBlank())) {
val req = group_member_info.ReqBody()
req.uint64_group_code.set(groupId)
req.uint64_uin.set(uin)
req.uint64_group_code.set(groupId.toLong())
req.uint64_uin.set(uin.toLong())
req.bool_new_client.set(true)
req.uint32_client_type.set(1)
req.uint32_rich_card_name_ver.set(1)
val fromServiceMsg = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray())
if (fromServiceMsg != null && fromServiceMsg.isSuccess) {
if (fromServiceMsg != null && fromServiceMsg.wupBuffer != null) {
val rsp = group_member_info.RspBody()
rsp.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
if (rsp.msg_meminfo.str_location.has()) {
@ -422,8 +579,8 @@ internal object GroupHelper: QQInterfaces() {
}
suspend fun getTroopMemberInfoByUinViaNt(
groupId: Long,
qq: Long,
groupId: String,
qq: String,
timeout: Long = 5000L
): Result<MemberInfo> {
return runCatching {
@ -432,13 +589,13 @@ internal object GroupHelper: QQInterfaces() {
val groupService = sessionService.groupService
val info = withTimeoutOrNull(timeout) {
suspendCancellableCoroutine {
groupService.getTransferableMemberInfo(groupId) { code, _, data ->
groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data ->
if (code != 0) {
it.resume(null)
return@getTransferableMemberInfo
}
data.forEach { (_, info) ->
if (info.uin == qq) {
if (info.uin == qq.toLong()) {
it.resume(info)
return@forEach
}
@ -455,21 +612,18 @@ internal object GroupHelper: QQInterfaces() {
}
}
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long, timeout: Long = 10_000): Result<TroopMemberInfo> {
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: String, memberUin: String, timeout: Long = 10_000): Result<TroopMemberInfo> {
val info = RefreshTroopMemberInfoLock.withLock {
val groupIdStr = groupId.toString()
val memberUinStr = memberUin.toString()
service.deleteTroopMember(groupIdStr, memberUinStr)
service.deleteTroopMember(groupId, memberUin)
requestMemberInfoV2(groupId, memberUin)
requestMemberInfo(groupId, memberUin)
withTimeoutOrNull(timeout) {
while (!service.isMemberInCache(groupIdStr, memberUinStr)) {
while (!service.isMemberInCache(groupId, memberUin)) {
delay(200)
}
return@withTimeoutOrNull service.getTroopMember(groupIdStr, memberUinStr)
return@withTimeoutOrNull service.getTroopMember(groupId, memberUin)
}
}
return if (info != null) {
@ -479,7 +633,7 @@ internal object GroupHelper: QQInterfaces() {
}
}
private fun requestMemberInfo(groupId: Long, memberUin: Long) {
private fun requestMemberInfo(groupId: String, memberUin: String) {
val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER)
if (!::METHOD_REQ_MEMBER_INFO.isInitialized) {
@ -491,10 +645,10 @@ internal object GroupHelper: QQInterfaces() {
}
}
METHOD_REQ_MEMBER_INFO.invoke(businessHandler, groupId, memberUin)
METHOD_REQ_MEMBER_INFO.invoke(businessHandler, groupId.toLong(), memberUin.toLong())
}
private fun requestMemberInfoV2(groupId: Long, memberUin: Long) {
private fun requestMemberInfoV2(groupId: String, memberUin: String) {
val businessHandler = app.getBusinessHandler(BusinessHandlerFactory.TROOP_MEMBER_CARD_HANDLER)
if (!::METHOD_REQ_MEMBER_INFO_V2.isInitialized) {
@ -506,7 +660,8 @@ internal object GroupHelper: QQInterfaces() {
}
}
METHOD_REQ_MEMBER_INFO_V2.invoke(businessHandler, groupId.toString(), groupUin2GroupCode(groupId).toString(), arrayListOf(memberUin.toString()))
METHOD_REQ_MEMBER_INFO_V2.invoke(businessHandler,
groupId, groupUin2GroupCode(groupId.toLong()).toString(), arrayListOf(memberUin))
}
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: String): Result<List<TroopMemberInfo>> {

View File

@ -1,49 +1,22 @@
@file:OptIn(DelicateCoroutinesApi::class)
package qq.service.internals
import com.tencent.qqnt.kernel.nativeinterface.BroadcastHelperTransNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.ContactMsgBoxInfo
import com.tencent.qqnt.kernel.nativeinterface.CustomWithdrawConfig
import com.tencent.qqnt.kernel.nativeinterface.DevInfo
import com.tencent.qqnt.kernel.nativeinterface.DownloadRelateEmojiResultInfo
import com.tencent.qqnt.kernel.nativeinterface.EmojiNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.EmojiResourceInfo
import com.tencent.qqnt.kernel.nativeinterface.FileTransNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.FirstViewDirectMsgNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.FirstViewGroupGuildInfo
import com.tencent.qqnt.kernel.nativeinterface.FreqLimitInfo
import com.tencent.qqnt.kernel.nativeinterface.GroupFileListResult
import com.tencent.qqnt.kernel.nativeinterface.GroupGuildNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.GroupItem
import com.tencent.qqnt.kernel.nativeinterface.GuildInteractiveNotificationItem
import com.tencent.qqnt.kernel.nativeinterface.GuildMsgAbFlag
import com.tencent.qqnt.kernel.nativeinterface.GuildNotificationAbstractInfo
import com.tencent.qqnt.kernel.nativeinterface.HitRelatedEmojiWordsResult
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener
import com.tencent.qqnt.kernel.nativeinterface.ImportOldDbMsgNotifyInfo
import com.tencent.qqnt.kernel.nativeinterface.InputStatusInfo
import com.tencent.qqnt.kernel.nativeinterface.KickedInfo
import com.tencent.qqnt.kernel.nativeinterface.MsgAbstract
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.kernel.nativeinterface.MsgSetting
import com.tencent.qqnt.kernel.nativeinterface.RecvdOrder
import com.tencent.qqnt.kernel.nativeinterface.RelatedWordEmojiInfo
import com.tencent.qqnt.kernel.nativeinterface.SearchGroupFileResult
import com.tencent.qqnt.kernel.nativeinterface.TabStatusInfo
import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
import com.tencent.qqnt.kernel.nativeinterface.UnreadCntInfo
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
import qq.service.bdh.RichProtoSvc
import qq.service.kernel.SimpleKernelMsgListener
import qq.service.msg.MessageHelper
object AioListener: IKernelMsgListener {
override fun onRecvMsg(msgs: ArrayList<MsgRecord>) {
msgs.forEach {
object AioListener : SimpleKernelMsgListener() {
override fun onRecvMsg(records: ArrayList<MsgRecord>) {
records.forEach {
GlobalScope.launch {
try {
onMsg(it)
@ -57,6 +30,8 @@ object AioListener: IKernelMsgListener {
private suspend fun onMsg(record: MsgRecord) {
when (record.chatType) {
MsgConstant.KCHATTYPEGROUP -> {
if (record.senderUin == 0L) return
LogCenter.log("群消息(group = ${record.peerName}(${record.peerUin}), uin = ${record.senderUin}, id = ${record.msgId})")
if (!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(record, record.elements)) {
@ -85,7 +60,12 @@ object AioListener: IKernelMsgListener {
LogCenter.log("私聊临时消息(private = ${record.senderUin}, groupId=$groupCode)")
if (!GlobalEventTransmitter.MessageTransmitter.transTempMessage(record, record.elements, groupCode, fromNick)
if (!GlobalEventTransmitter.MessageTransmitter.transTempMessage(
record,
record.elements,
groupCode,
fromNick
)
) {
LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN)
}
@ -104,274 +84,68 @@ object AioListener: IKernelMsgListener {
}
}
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>?) {
arrayList?.forEach { record ->
GlobalScope.launch {
when (record.chatType) {
MsgConstant.KCHATTYPEGROUP -> onGroupFileMsg(record)
MsgConstant.KCHATTYPEC2C -> onC2CFileMsg(record)
else -> LogCenter.log("不支持该来源的文件上传事件:${record}", Level.WARN)
}
}
}
}
override fun onFirstViewDirectMsgUpdate(firstViewDirectMsgNotifyInfo: FirstViewDirectMsgNotifyInfo?) {
private suspend fun onC2CFileMsg(record: MsgRecord) {
val userId = record.senderUin
val fileMsg = record.elements.firstOrNull {
it.elementType == MsgConstant.KELEMTYPEFILE
}?.fileElement ?: kotlin.run {
LogCenter.log("消息为私聊文件消息但不包含文件消息,来自:${record.peerUin}", Level.WARN)
return
}
val fileName = fileMsg.fileName
val fileSize = fileMsg.fileSize
val expireTime = fileMsg.expireTime ?: 0
val fileId = fileMsg.fileUuid
val fileSubId = fileMsg.fileSubId ?: ""
val url = RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
if (!GlobalEventTransmitter.FileNoticeTransmitter
.transPrivateFileEvent(record.msgTime, userId, fileId, fileSubId, fileName, fileSize, expireTime, url)
) {
LogCenter.log("私聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
}
}
override fun onFirstViewGroupGuildMapping(arrayList: ArrayList<FirstViewGroupGuildInfo>?) {
private suspend fun onGroupFileMsg(record: MsgRecord) {
val groupId = record.peerUin
val userId = record.senderUin
val fileMsg = record.elements.firstOrNull {
it.elementType == MsgConstant.KELEMTYPEFILE
}?.fileElement ?: kotlin.run {
LogCenter.log("消息为群聊文件消息但不包含文件消息,来自:${record.peerUin}", Level.WARN)
return
}
//val fileMd5 = fileMsg.fileMd5
val fileName = fileMsg.fileName
val fileSize = fileMsg.fileSize
val uuid = fileMsg.fileUuid
val bizId = fileMsg.fileBizId
val url = RichProtoSvc.getGroupFileDownUrl(record.peerUin, uuid, bizId)
if (!GlobalEventTransmitter.FileNoticeTransmitter
.transGroupFileEvent(record.msgTime, userId, groupId, uuid, fileName, fileSize, bizId, url)
) {
LogCenter.log("群聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
}
}
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?) {
@OptIn(ExperimentalStdlibApi::class)
override fun onRecvSysMsg(arrayList: ArrayList<Byte>?) {
LogCenter.log("onRecvSysMsg")
LogCenter.log(arrayList?.toByteArray()?.toHexString() ?: "")
}
}

View File

@ -0,0 +1,14 @@
package qq.service.internals
import com.tencent.qqnt.kernel.nativeinterface.DevInfo
import com.tencent.qqnt.kernel.nativeinterface.KickedInfo
import qq.service.kernel.SimpleKernelMsgListener
import java.util.ArrayList
object LineDevListener: SimpleKernelMsgListener() {
override fun onKickedOffLine(kickedInfo: KickedInfo) {
}
override fun onLineDev(devs: ArrayList<DevInfo>) {
}
}

View File

@ -52,8 +52,12 @@ internal object MSFHandler {
fun onPush(fromServiceMsg: FromServiceMsg) {
val cmd = fromServiceMsg.serviceCmd
val push = mPushHandlers[cmd]
push?.invoke(fromServiceMsg)
if (cmd == "trpc.msg.olpush.OlPushService.MsgPush") {
PrimitiveListener.onPush(fromServiceMsg)
} else {
val push = mPushHandlers[cmd]
push?.invoke(fromServiceMsg)
}
}
fun onResp(toServiceMsg: ToServiceMsg, fromServiceMsg: FromServiceMsg) {

View File

@ -58,6 +58,7 @@ internal object NTServiceFetcher {
try {
LogCenter.log("Register MSG listener successfully.")
msgService.addMsgListener(AioListener)
msgService.addMsgListener(LineDevListener)
// 接口缺失 暂不使用
//groupService.addKernelGroupListener(GroupEventListener)

View File

@ -0,0 +1,667 @@
@file:OptIn(DelicateCoroutinesApi::class)
package qq.service.internals
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qphone.base.remote.FromServiceMsg
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.msg.api.IMsgService
import io.kritor.event.GroupMemberDecreasedNotice.GroupMemberDecreasedType
import io.kritor.event.GroupMemberIncreasedNotice.GroupMemberIncreasedType
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readBytes
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.internals.GlobalEventTransmitter
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.readBuf32Long
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.message.ContentHead
import protobuf.message.MsgBody
import protobuf.message.ResponseHead
import protobuf.push.C2CCommonTipsEvent
import protobuf.push.C2CRecallEvent
import protobuf.push.FriendApplyEvent
import protobuf.push.GroupAdminChangeEvent
import protobuf.push.GroupApplyEvent
import protobuf.push.GroupBanEvent
import protobuf.push.GroupCommonTipsEvent
import protobuf.push.GroupInviteEvent
import protobuf.push.GroupInvitedApplyEvent
import protobuf.push.GroupListChangeEvent
import protobuf.push.MessagePush
import protobuf.push.MessagePushClientInfo
import qq.service.QQInterfaces
import qq.service.contact.ContactHelper
import qq.service.friend.FriendHelper.requestFriendSystemMsgNew
import qq.service.group.GroupHelper
import qq.service.group.GroupHelper.requestGroupSystemMsgNew
import qq.service.msg.MessageHelper
import kotlin.coroutines.resume
internal object PrimitiveListener {
fun onPush(fromServiceMsg: FromServiceMsg) {
if (fromServiceMsg.wupBuffer == null) return
try {
val push = fromServiceMsg.wupBuffer.slice(4)
.decodeProtobuf<MessagePush>()
GlobalScope.launch {
onMsgPush(push)
}
} catch (e: Exception) {
LogCenter.log(e.stackTraceToString(), Level.WARN)
}
}
private suspend fun onMsgPush(push: MessagePush) {
if (
push.msgBody == null ||
push.msgBody!!.contentHead == null ||
push.msgBody!!.body == null ||
push.msgBody!!.contentHead!!.msgTime == null
) return
val msgBody = push.msgBody!!
val contentHead = msgBody.contentHead!!
val msgType = contentHead.msgType
val subType = contentHead.msgSubType
val msgTime = contentHead.msgTime!!
val body = msgBody.body!!
try {
when (msgType) {
33 -> onGroupMemIncreased(msgTime, body)
34 -> onGroupMemberDecreased(msgTime, body)
44 -> onGroupAdminChange(msgTime, body)
82 -> onGroupMessage(msgTime, body)
84 -> onGroupApply(msgTime, contentHead, body)
87 -> onInviteGroup(msgTime, msgBody.msgHead!!, body)
528 -> when (subType) {
35 -> onFriendApply(msgTime, push.clientInfo!!, body)
39 -> onCardChange(msgTime, body)
68 -> onGroupApply(msgTime, contentHead, body)
138 -> onC2CRecall(msgTime, body)
290 -> onC2CPoke(msgTime, body)
}
732 -> when (subType) {
12 -> onGroupBan(msgTime, body)
16 -> onGroupUniqueTitleChange(msgTime, body)
17 -> onGroupRecall(msgTime, body)
20 -> onGroupCommonTips(msgTime, body)
21 -> onEssenceMessage(msgTime, push.clientInfo, body)
}
}
} catch (e: Exception) {
LogCenter.log("onMsgPush(msgType: $msgType, subType: $subType): " + e.stackTraceToString(), Level.WARN)
}
}
private fun onGroupMessage(msgTime: Long, body: MsgBody) {
}
private suspend fun onC2CPoke(msgTime: Long, body: MsgBody) {
val event = body.msgContent!!.decodeProtobuf<C2CCommonTipsEvent>()
if (event.params == null) return
val params = event.params!!.associate {
it.key to it.value
}
val target = params["uin_str2"] ?: return
val operation = params["uin_str1"] ?: return
val suffix = params["suffix_str"] ?: ""
val actionImg = params["action_img_url"] ?: ""
val action = params["alt_str1"] ?: ""
LogCenter.log("私聊戳一戳: $operation $action $target $suffix")
if (!GlobalEventTransmitter.PrivateNoticeTransmitter
.transPrivatePoke(msgTime, operation.toLong(), target.toLong(), action, suffix, actionImg)
) {
LogCenter.log("私聊戳一戳推送失败!", Level.WARN)
}
}
private suspend fun onFriendApply(
msgTime: Long,
clientInfo: MessagePushClientInfo,
body: MsgBody
) {
val event = body.msgContent!!.decodeProtobuf<FriendApplyEvent>()
if (event.head == null) return
val head = event.head!!
val applierUid = head.applierUid
val msg = head.applyMsg ?: ""
val source = head.source ?: ""
var applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
if (applier == 0L) {
applier = clientInfo.liteHead?.sender?.toLong() ?: 0
}
val src = head.srcId
val subSrc = head.subSrc
val flag: String = try {
val reqs = requestFriendSystemMsgNew(20, 0, 0)
val req = reqs?.first {
it.msg_time.get() == msgTime
}
val seq = req?.msg_seq?.get()
"$seq;$src;$subSrc;$applier"
} catch (err: Throwable) {
"$msgTime;$src;$subSrc;$applier"
}
LogCenter.log("来自$applier 的好友申请:$msg ($source)")
if (!GlobalEventTransmitter.RequestTransmitter
.transFriendApp(msgTime, applier, msg, flag)
) {
LogCenter.log("好友申请推送失败!", Level.WARN)
}
}
private suspend fun onCardChange(msgTime: Long, body: MsgBody) {
// val event = runCatching {
// body.msgContent!!.decodeProtobuf<GroupCardChangeEvent>()
// }.getOrElse {
// val readPacket = ByteReadPacket(body.msgContent!!)
// readPacket.readBuf32Long()
// readPacket.discardExact(1)
//
// readPacket.readBytes(readPacket.readShort().toInt()).also {
// readPacket.release()
// }.decodeProtobuf<GroupCommonTipsEvent>()
// }
//
// val targetId = detail[1, 13, 2].asUtf8String
// val newCardList = detail[1, 13, 3].asList
// var newCard = ""
// newCardList
// .value
// .forEach {
// if (it[1].asInt == 1) {
// newCard = it[2].asUtf8String
// }
// }
// val groupId = detail[1, 13, 4].asLong
// var oldCard = ""
// val targetQQ = ContactHelper.getUinByUidAsync(targetId).toLong()
// LogCenter.log("群组[$groupId]成员$targetQQ 群名片变动 -> $newCard")
// // oldCard暂时获取不到
// if (!GlobalEventTransmitter.GroupNoticeTransmitter
// .transCardChange(msgTime, targetQQ, oldCard, newCard, groupId)
// ) {
// LogCenter.log("群名片变动推送失败!", Level.WARN)
// }
}
private suspend fun onGroupUniqueTitleChange(msgTime: Long, body: MsgBody) {
val event = runCatching {
body.msgContent!!.decodeProtobuf<GroupCommonTipsEvent>()
}.getOrElse {
val readPacket = ByteReadPacket(body.msgContent!!)
readPacket.readBuf32Long()
readPacket.discardExact(1)
readPacket.readBytes(readPacket.readShort().toInt()).also {
readPacket.release()
}.decodeProtobuf<GroupCommonTipsEvent>()
}
val groupId = event.groupCode.toLong()
val detail = event.uniqueTitleChangeDetail!!.first()
//detail = if (detail[5] is ProtoList) {
// (detail[5] as ProtoList).value[0]
//} else {
// detail[5]
// }
val targetUin = detail.targetUin.toLong()
// 恭喜<{\"cmd\":5,\"data\":\"qq\",\"text}\":\"nickname\"}>获得群主授予的<{\"cmd\":1,\"data\":\"https://qun.qq.com/qqweb/m/qun/medal/detail.html?_wv=16777223&bid=2504&gc=gid&isnew=1&medal=302&uin=uin\",\"text\":\"title\",\"url\":\"https://qun.qq.com/qqweb/m/qun/medal/detail.html?_wv=16777223&bid=2504&gc=gid&isnew=1&medal=302&uin=uin\"}>头衔
val titleChangeInfo = detail.wording
if (titleChangeInfo.indexOf("群主授予") == -1) {
return
}
val titleJson = titleChangeInfo.split("获得群主授予的<")[1].replace(">头衔", "")
val titleJsonObj = Json.decodeFromString<JsonElement>(titleJson).asJsonObject
val title = titleJsonObj["text"].asString
LogCenter.log("群组[$groupId]成员$targetUin 获得群头衔 -> $title")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transTitleChange(msgTime, targetUin, title, groupId)
) {
LogCenter.log("群头衔变动推送失败!", Level.WARN)
}
}
private suspend fun onEssenceMessage(
msgTime: Long,
clientInfo: MessagePushClientInfo?,
body: MsgBody
) {
if (clientInfo == null) return
val event = runCatching {
body.msgContent!!.decodeProtobuf<GroupCommonTipsEvent>()
}.getOrElse {
val readPacket = ByteReadPacket(body.msgContent!!)
readPacket.readBuf32Long()
readPacket.discardExact(1)
readPacket.readBytes(readPacket.readShort().toInt()).also {
readPacket.release()
}.decodeProtobuf<GroupCommonTipsEvent>()
}
val groupId = event.groupCode.toLong()
val detail = event.essenceMsgInfo!!.first()
val msgSeq = event.msgSeq.toLong()
val senderUin = detail.sender.toLong()
val operatorUin = detail.operator.toLong()
when (val type = detail.type) {
1u -> {
LogCenter.log("群设精消息(groupId=$groupId, sender=$senderUin, msgSeq=$msgSeq, operator=$operatorUin)")
}
2u -> {
LogCenter.log("群撤精消息(groupId=$groupId, sender=$senderUin, msgId=$msgSeq, operator=$operatorUin)")
}
else -> error("onEssenceMessage unknown type: $type")
}
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId.toString())
val sourceRecord = withTimeoutOrNull(3000) {
suspendCancellableCoroutine {
QRoute.api(IMsgService::class.java).getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records ->
it.resume(records)
}
}
}?.firstOrNull()
if (sourceRecord == null) {
LogCenter.log("无法获取源消息记录,无法推送精华消息变动!", Level.WARN)
return
}
val msgId = sourceRecord.msgId
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transEssenceChange(msgTime, senderUin, operatorUin, msgId, groupId, detail.type)
) {
LogCenter.log("精华消息变动推送失败!", Level.WARN)
}
}
private suspend fun onGroupCommonTips(time: Long, body: MsgBody) {
val event = runCatching {
body.msgContent!!.decodeProtobuf<GroupCommonTipsEvent>()
}.getOrElse {
val readPacket = ByteReadPacket(body.msgContent!!)
readPacket.discardExact(4)
readPacket.discardExact(1)
readPacket.readBytes(readPacket.readShort().toInt()).also {
readPacket.release()
}.decodeProtobuf<GroupCommonTipsEvent>()
}
val groupId = event.groupCode.toLong()
val detail = event.baseTips!!.first()
val params = detail.params!!.associate {
it.key to it.value
}
val target = params["uin_str2"] ?: params["mqq_uin"] ?: return
val operation = params["uin_str1"] ?: return
val suffix = params["suffix_str"] ?: ""
val actionImg = params["action_img_url"] ?: ""
val action = params["alt_str1"]
?: params["action_str"]
?: params["user_sign"]
?: ""
val rankImg = params["rank_img"] ?: ""
when (detail.type) {
1061u -> {
LogCenter.log("群戳一戳($groupId): $operation $action $target $suffix")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupPoke(time, operation.toLong(), target.toLong(), action, suffix, actionImg, groupId)
) {
LogCenter.log("群戳一戳推送失败!", Level.WARN)
}
}
1068u -> {
LogCenter.log("群打卡($groupId): $action $target")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupSign(time, target.toLong(), action, rankImg, groupId)
) {
LogCenter.log("群打卡推送失败!", Level.WARN)
}
}
else -> {
LogCenter.log("onGroupPokeAndGroupSign unknown type ${detail.type}", Level.WARN)
}
}
}
private suspend fun onC2CRecall(time: Long, body: MsgBody) {
val event = body.msgContent!!.decodeProtobuf<C2CRecallEvent>()
val head = event.head!!
val operationUid = head.operator!!
val operator = ContactHelper.getUinByUidAsync(operationUid).toLong()
val msgSeq = head.msgSeq
val tipText = head.wording?.wording ?: ""
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, operationUid)
val sourceRecord = withTimeoutOrNull(3000) {
suspendCancellableCoroutine {
QRoute.api(IMsgService::class.java).getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records ->
it.resume(records)
}
}
}?.firstOrNull()
if (sourceRecord == null) {
LogCenter.log("无法获取源消息记录,无法推送撤回消息!", Level.WARN)
return
}
val msgId = sourceRecord.msgId
LogCenter.log("私聊消息撤回: $operator, seq = $msgSeq, msgId = ${msgId}, tip = $tipText")
if (!GlobalEventTransmitter.PrivateNoticeTransmitter
.transPrivateRecall(time, operator, msgId, tipText)
) {
LogCenter.log("私聊消息撤回推送失败!", Level.WARN)
}
}
private suspend fun onGroupMemIncreased(time: Long, body: MsgBody) {
val event = body.msgContent!!.decodeProtobuf<GroupListChangeEvent>()
val groupCode = event.groupCode
val targetUid = event.memberUid
val type = event.type
GroupHelper.getGroupMemberList(groupCode.toString(), true).onFailure {
LogCenter.log("新成员加入刷新群成员列表失败: $groupCode", Level.WARN)
}.onSuccess {
LogCenter.log("新成员加入刷新群成员列表成功,群成员数量: ${it.size}", Level.INFO)
}
val operatorUid = event.operatorUid
val operator = ContactHelper.getUinByUidAsync(operatorUid).toLong()
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
LogCenter.log("群成员增加($groupCode): $target, type = $type")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupMemberNumIncreased(
time,
target,
targetUid,
groupCode,
operator,
operatorUid,
when (type) {
130 -> GroupMemberIncreasedType.APPROVE
131 -> GroupMemberIncreasedType.INVITE
else -> GroupMemberIncreasedType.APPROVE
}
)
) {
LogCenter.log("群成员增加推送失败!", Level.WARN)
}
}
private suspend fun onGroupMemberDecreased(time: Long, body: MsgBody) {
val event = body.msgContent!!.decodeProtobuf<GroupListChangeEvent>()
val groupCode = event.groupCode
val targetUid = event.memberUid
val type = event.type
val operatorUid = event.operatorUid
GroupHelper.getGroupMemberList(groupCode.toString(), true).onFailure {
LogCenter.log("新成员加入刷新群成员列表失败: $groupCode", Level.WARN)
}.onSuccess {
LogCenter.log("新成员加入刷新群成员列表成功,群成员数量: ${it.size}", Level.INFO)
}
val operator = ContactHelper.getUinByUidAsync(operatorUid).toLong()
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
val subtype = when (type) {
130 -> GroupMemberDecreasedType.LEAVE
131 -> GroupMemberDecreasedType.KICK
3 -> GroupMemberDecreasedType.KICK_ME
else -> GroupMemberDecreasedType.KICK
}
LogCenter.log("群成员减少($groupCode): $target, type = $subtype ($type)")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupMemberNumDecreased(
time,
target,
targetUid,
groupCode,
operator,
operatorUid,
subtype
)
) {
LogCenter.log("群成员减少推送失败!", Level.WARN)
}
}
private suspend fun onGroupAdminChange(msgTime: Long, body: MsgBody) {
val event = body.msgContent!!.decodeProtobuf<GroupAdminChangeEvent>()
val groupCode = event.groupCode
if (event.operation == null) return
val operation = event.operation!!
if (operation.setInfo == null && operation.unsetInfo == null) return
val isSetAdmin: Boolean
val targetUid: String
if (operation.setInfo == null) {
isSetAdmin = false
targetUid = operation.unsetInfo!!.targetUid!!
} else {
isSetAdmin = true
targetUid = operation.setInfo!!.targetUid!!
}
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
LogCenter.log("群管理员变动($groupCode): $target, isSetAdmin = $isSetAdmin")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupAdminChanged(msgTime, target, targetUid, groupCode, isSetAdmin)
) {
LogCenter.log("群管理员变动推送失败!", Level.WARN)
}
}
private suspend fun onGroupBan(msgTime: Long, body: MsgBody) {
val event = body.msgContent!!.decodeProtobuf<GroupBanEvent>()
val groupCode = event.groupCode.toLong()
val operatorUid = event.operatorUid
val wholeBan = event.target?.target?.targetUid == null
val targetUid = event.target?.target?.targetUid ?: ""
val rawDuration = event.target?.target?.rawDuration?.toInt() ?: 0
val operator = ContactHelper.getUinByUidAsync(operatorUid).toLong()
val duration = if (wholeBan) -1 else rawDuration
val target = if (wholeBan) 0 else ContactHelper.getUinByUidAsync(targetUid).toLong()
if (wholeBan) {
LogCenter.log("群全员禁言($groupCode): $operator -> ${if (rawDuration != 0) "开启" else "关闭"}")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupWholeBan(msgTime, groupCode, operator, rawDuration != 0)
) {
LogCenter.log("群禁言推送失败!", Level.WARN)
}
} else {
LogCenter.log("群禁言($groupCode): $operator -> $target, 时长 = ${duration}s")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupBan(msgTime, operator, operatorUid, target, targetUid, groupCode, duration)
) {
LogCenter.log("群禁言推送失败!", Level.WARN)
}
}
}
private suspend fun onGroupRecall(time: Long, body: MsgBody) {
val event = runCatching {
body.msgContent!!.decodeProtobuf<GroupCommonTipsEvent>()
}.getOrElse {
val readPacket = ByteReadPacket(body.msgContent!!)
readPacket.discardExact(4)
readPacket.discardExact(1)
readPacket.readBytes(readPacket.readShort().toInt()).also {
readPacket.release()
}.decodeProtobuf<GroupCommonTipsEvent>()
}
val groupCode = event.groupCode.toLong()
val detail = event.recallDetails!!
val operatorUid = detail.operatorUid
val targetUid = detail.msgInfo!!.senderUid
val msgSeq = detail.msgInfo!!.msgSeq.toLong()
val tipText = detail.wording?.wording ?: ""
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupCode.toString())
val sourceRecord = withTimeoutOrNull(3000) {
suspendCancellableCoroutine {
QRoute.api(IMsgService::class.java).getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records ->
it.resume(records)
}
}
}?.firstOrNull()
if (sourceRecord == null) {
LogCenter.log("无法获取源消息记录,无法推送撤回消息!", Level.WARN)
return
}
val msgId = sourceRecord.msgId
val operator = ContactHelper.getUinByUidAsync(operatorUid).toLong()
val target = ContactHelper.getUinByUidAsync(targetUid).toLong()
LogCenter.log("群消息撤回($groupCode): $operator -> $target, seq = $msgSeq, id = $msgId, tip = $tipText")
if (!GlobalEventTransmitter.GroupNoticeTransmitter
.transGroupMsgRecall(time, operator, operatorUid, target, targetUid, groupCode, msgId, tipText)
) {
LogCenter.log("群消息撤回推送失败!", Level.WARN)
}
}
private suspend fun onGroupApply(time: Long, contentHead: ContentHead, body: MsgBody) {
when (contentHead.msgType) {
84 -> {
val event = body.msgContent!!.decodeProtobuf<GroupApplyEvent>()
val groupCode = event.groupCode
val applierUid = event.applierUid
val reason = event.applyMsg ?: ""
var applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
if (applier == QQInterfaces.app.longAccountUin) {
return
}
val msgSeq = contentHead.msgSeq
val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(5, 2)
reqs = reqs + riskReqs
val req = reqs.firstOrNull {
it.msg_time.get() == time && it.msg?.group_code?.get() == groupCode
}
val seq = req?.msg_seq?.get() ?: time
if (applier == 0L) {
applier = req?.req_uin?.get() ?: 0L
}
"$seq;$groupCode;$applier"
} catch (err: Throwable) {
"$time;$groupCode;$applier"
}
LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq")
if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, applierUid, reason, groupCode, flag)
) {
LogCenter.log("入群申请推送失败!", Level.WARN)
}
}
528 -> {
val event = body.msgContent!!.decodeProtobuf<GroupInvitedApplyEvent>()
val groupCode = event.applyInfo?.groupCode ?: return
val applierUid = event.applyInfo?.applierUid ?: return
var applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
if (applier == QQInterfaces.app.longAccountUin) {
return
}
if ((event.applyInfo?.type ?: return) < 3) {
// todo
return
}
val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(5, 2)
reqs = reqs + riskReqs
val req = reqs.firstOrNull() {
it.msg_time.get() == time
}
val seq = req?.msg_seq?.get() ?: time
if (applier == 0L) {
applier = req?.req_uin?.get() ?: 0L
}
"$seq;$groupCode;$applier"
} catch (err: Throwable) {
"$time;$groupCode;$applier"
}
LogCenter.log("邀请入群申请($groupCode): $applier")
if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, applierUid, "", groupCode, flag)
) {
LogCenter.log("邀请入群申请推送失败!", Level.WARN)
}
}
}
}
private suspend fun onInviteGroup(time: Long, msgHead: ResponseHead, body: MsgBody) {
val event = body.msgContent!!.decodeProtobuf<GroupInviteEvent>()
val groupCode = event.groupCode
val invitorUid = event.inviterUid
val invitor = ContactHelper.getUinByUidAsync(invitorUid).toLong()
val uin = msgHead.receiver
LogCenter.log("邀请入群: $groupCode, 邀请者: \"$invitor\"")
val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(10, 2)
reqs = reqs + riskReqs
val req = reqs.firstOrNull {
it.msg_time.get() == time
}
val seq = req?.msg_seq?.get() ?: time
"$seq;$groupCode;$uin"
} catch (err: Throwable) {
"$time;$groupCode;$uin"
}
if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, invitor, invitorUid, "", groupCode, flag)
) {
LogCenter.log("邀请入群推送失败!", Level.WARN)
}
}
}

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(devs: 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(chatType: Int, tips: String?, msgId: 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

@ -0,0 +1,40 @@
package qq.service.lightapp
sealed class ArkAppInfo(
val appId: Long,
val version: String,
val packageName: String,
val signature: String,
val miniAppId: Long = 0,
val appName: String = ""
) {
data object QQMusic: ArkAppInfo(
appId = 100497308,
version = "0.0.0",
packageName = "com.tencent.qqmusic",
signature = "cbd27cd7c861227d013a25b2d10f0799"
)
data object NetEaseMusic: ArkAppInfo(
appId = 100495085,
version = "0.0.0",
packageName = "com.netease.cloudmusic",
signature = "da6b069da1e2982db3e386233f68d76d"
)
data object DanMaKu: ArkAppInfo(
appId = 100951776,
version = "0.0.0",
packageName = "tv.danmaku.bili",
signature = "7194d531cbe7960a22007b9f6bdaa38b",
miniAppId = 1109937557,
appName = "哔哩哔哩"
)
data object Docs: ArkAppInfo(
appId = 0,
version = "0.0.0",
packageName = "",
signature = "f3da3147654d9a21f3237b88f20dce9c",
miniAppId = 1108338344
)
}

View File

@ -0,0 +1,48 @@
package qq.service.lightapp
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import qq.service.QQInterfaces
import qq.service.contact.longPeer
import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
internal object ArkMsgHelper: QQInterfaces() {
suspend fun tryShareMusic(
contact: Contact,
msgId: Long,
arkAppInfo: ArkAppInfo,
title: String,
singer: String,
jumpUrl: String,
previewUrl: String,
musicUrl: String,
) {
val req = oidb_cmd0xb77.ReqBody()
req.appid.set(arkAppInfo.appId)
req.app_type.set(1)
req.msg_style.set(4)
req.client_info.set(oidb_cmd0xb77.ClientInfo().also {
it.platform.set(1)
it.sdk_version.set(arkAppInfo.version)
it.android_package_name.set(arkAppInfo.packageName)
it.android_signature.set(arkAppInfo.signature)
})
req.ext_info.set(oidb_cmd0xb77.ExtInfo().also {
it.msg_seq.set(msgId)
})
req.recv_uin.set(contact.longPeer())
req.rich_msg_body.set(oidb_cmd0xb77.RichMsgBody().also {
it.title.set(title)
it.summary.set(singer)
it.url.set(jumpUrl)
it.picture_url.set(previewUrl)
it.music_url.set(musicUrl)
})
when (contact.chatType) {
MsgConstant.KCHATTYPEGROUP -> req.send_type.set(1)
MsgConstant.KCHATTYPEC2C -> req.send_type.set(0)
else -> error("不支持该聊天类型发送音乐分享: chatType: ${contact.chatType}")
}
sendOidb("OidbSvc.0xb77_9", 0xb77, 9, req.toByteArray())
}
}

View File

@ -0,0 +1,58 @@
package qq.service.lightapp
import com.tencent.biz.map.trpcprotocol.LbsSendInfo
import com.tencent.proto.lbsshare.LBSShare
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import moe.fuqiuluo.shamrock.helper.IllegalParamsException
import moe.fuqiuluo.shamrock.tools.slice
import qq.service.QQInterfaces
import qq.service.contact.longPeer
import kotlin.math.roundToInt
internal object LbsHelper: QQInterfaces() {
suspend fun tryShareLocation(contact: Contact, lat: Double, lon: Double): Result<Unit> {
val req = LbsSendInfo.SendMessageReq()
req.uint64_peer_account.set(contact.longPeer())
when (contact.chatType) {
MsgConstant.KCHATTYPEGROUP -> req.enum_relation_type.set(1)
MsgConstant.KCHATTYPEC2C -> req.enum_relation_type.set(0)
else -> error("Not supported chat type: $contact")
}
req.str_name.set("位置分享")
req.str_address.set(getAddressWithLonLat(lat, lon).onFailure {
return Result.failure(it)
}.getOrNull())
req.str_lat.set(lat.toString())
req.str_lng.set(lon.toString())
sendBuffer("trpc.qq_lbs.qq_lbs_ark.LocationArk.SsoSendMessage", true, req.toByteArray())
return Result.success(Unit)
}
suspend fun getAddressWithLonLat(lat: Double, lon: Double): Result<String> {
if (lat > 90 || lat < 0) {
return Result.failure(IllegalParamsException("纬度大小错误"))
}
if (lon > 180 || lon < 0) {
return Result.failure(IllegalParamsException("经度大小错误"))
}
val latO = (lat * 1000000).roundToInt()
val lngO = (lon * 1000000).roundToInt()
val req = LBSShare.LocationReq()
req.lat.set(latO)
req.lng.set(lngO)
req.coordinate.set(1)
req.keyword.set("")
req.category.set("")
req.page.set(0)
req.count.set(20)
req.requireMyLbs.set(1)
req.imei.set("")
val fromServiceMsg = sendBufferAW("LbsShareSvr.location", true, req.toByteArray())
?: return Result.failure(Exception("获取位置失败"))
val resp = LBSShare.LocationResp()
resp.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val location = resp.mylbs
return Result.success(location.addr.get())
}
}

View File

@ -0,0 +1,96 @@
package qq.service.lightapp
import com.tencent.qqnt.kernel.nativeinterface.Contact
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.json.Json
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.tools.asInt
import moe.fuqiuluo.shamrock.tools.asJsonArray
import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.asStringOrNull
import moe.fuqiuluo.shamrock.utils.MD5
internal object MusicHelper {
suspend fun tryShare163MusicById(contact: Contact, msgId: Long, id: String): Boolean {
try {
val respond = GlobalClient.get("https://music.163.com/api/song/detail/?id=$id&ids=[$id]")
val songInfo = Json.parseToJsonElement(respond.bodyAsText()).asJsonObject["songs"].asJsonArray.first().asJsonObject
val name = songInfo["name"].asString
val title = songInfo["name"].asString
val singerName = songInfo["artists"].asJsonArray.first().asJsonObject["name"].asString
val previewUrl = songInfo["album"].asJsonObject["picUrl"].asString
val playUrl = "https://music.163.com/song/media/outer/url?id=$id.mp3"
val jumpUrl = "https://music.163.com/#/song?id=$id"
ArkMsgHelper.tryShareMusic(
contact,
msgId,
ArkAppInfo.NetEaseMusic,
title.ifBlank { name },
singerName,
jumpUrl,
previewUrl,
playUrl
)
return true
} catch (e: Throwable) {
LogCenter.log(e.stackTraceToString(), Level.ERROR)
}
return false
}
suspend fun tryShareQQMusicById(contact: Contact, msgId: Long, id: String): Boolean {
try {
val respond = GlobalClient.get("https://u.y.qq.com/cgi-bin/musicu.fcg?format=json&inCharset=utf8&outCharset=utf-8&notice=0&platform=yqq.json&needNewCode=0&data={%22comm%22:{%22ct%22:24,%22cv%22:0},%22songinfo%22:{%22method%22:%22get_song_detail_yqq%22,%22param%22:{%22song_type%22:0,%22song_mid%22:%22%22,%22song_id%22:$id},%22module%22:%22music.pf_song_detail_svr%22}}")
val songInfo = Json.parseToJsonElement(respond.bodyAsText()).asJsonObject["songinfo"].asJsonObject
if (songInfo["code"].asInt != 0) {
LogCenter.log("获取QQ音乐($id)的歌曲信息失败。")
return false
} else {
val data = songInfo["data"].asJsonObject
val trackInfo = data["track_info"].asJsonObject
val mid = trackInfo["mid"].asString
val previewMid = trackInfo["album"].asJsonObject["mid"].asString
val singerMid = trackInfo["singer"].asJsonArrayOrNull?.let {
it[0].asJsonObject["mid"].asStringOrNull
} ?: ""
val name = trackInfo["name"].asString
val title = trackInfo["title"].asString
val singerName = trackInfo["singer"].asJsonArray.first().asJsonObject["name"].asString
val vs = trackInfo["vs"].asJsonArrayOrNull?.let {
it[0].asStringOrNull
} ?: ""
val code = MD5.getMd5Hex("${mid}q;z(&l~sdf2!nK".toByteArray()).substring(0 .. 4).uppercase()
val playUrl = "http://c6.y.qq.com/rsc/fcgi-bin/fcg_pyq_play.fcg?songid=&songmid=$mid&songtype=1&fromtag=50&uin=&code=$code"
val previewUrl = if (vs.isNotEmpty()) {
"http://y.gtimg.cn/music/photo_new/T062R150x150M000$vs}.jpg"
} else if (previewMid.isNotEmpty()) {
"http://y.gtimg.cn/music/photo_new/T002R150x150M000$previewMid.jpg"
} else if (singerMid.isNotEmpty()){
"http://y.gtimg.cn/music/photo_new/T001R150x150M000$singerMid.jpg"
} else {
""
}
val jumpUrl = "https://i.y.qq.com/v8/playsong.html?platform=11&appshare=android_qq&appversion=10030010&hosteuin=oKnlNenz7i-s7c**&songmid=${mid}&type=0&appsongtype=1&_wv=1&source=qq&ADTAG=qfshare"
ArkMsgHelper.tryShareMusic(
contact,
msgId,
ArkAppInfo.QQMusic,
title.ifBlank { name },
singerName,
jumpUrl,
previewUrl,
playUrl
)
return true
}
} catch (e: Throwable) {
LogCenter.log(e.stackTraceToString(), Level.ERROR)
}
return false
}
}

View File

@ -0,0 +1,10 @@
package qq.service.lightapp
import kotlinx.serialization.Serializable
@Serializable
internal data class Region(
val adcode: Int,
val province: String?,
val city: String?
)

View File

@ -0,0 +1,78 @@
package qq.service.lightapp
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.url
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import io.ktor.http.encodeURLQueryComponent
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.tools.GlobalJson
import moe.fuqiuluo.shamrock.tools.asInt
import moe.fuqiuluo.shamrock.tools.asJsonArray
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asStringOrNull
import qq.service.QQInterfaces
import qq.service.ticket.TicketHelper
internal object WeatherHelper: QQInterfaces() {
suspend fun fetchWeatherCard(code: Int): Result<JsonObject> {
val cookie = TicketHelper.getCookie("mp.qq.com")
val resp = GlobalClient.get("https://weather.mp.qq.com/page/poster?_wv=2&&_wwv=4&adcode=$code") {
header("Cookie", cookie)
}
if (resp.status != HttpStatusCode.OK) {
LogCenter.log("fetchWeatherCard: error: ${resp.status}, cookie: $cookie", Level.ERROR)
return Result.failure(Exception("search city failed"))
}
val textJson = resp.bodyAsText()
.replace("\n", "")
.split("window.__INITIAL_STATE__ =")[1]
.split("};")[0].trim() + "}"
//LogCenter.log(textJson)
return Result.success(Json.parseToJsonElement(textJson).asJsonObject)
}
suspend fun searchCity(query: String): Result<List<Region>> {
val pskey = TicketHelper.getPSKey(app.currentAccountUin, "mp.qq.com") ?: ""
val cookie = TicketHelper.getCookie("mp.qq.com")
val gtk = TicketHelper.getCSRF(pskey)
val resp = GlobalClient.get {
url("https://weather.mp.qq.com/trpc/weather/SearchRegions?g_tk=$gtk&key=${query.encodeURLQueryComponent()}&offset=0&count=25")
header("Cookie", cookie)
}
if (resp.status != HttpStatusCode.OK) {
LogCenter.log("GetWeatherCityCode: error: ${resp.status}, cookie: $cookie, bkn: $gtk", Level.ERROR)
return Result.failure(Exception("search city failed"))
}
val json = GlobalJson.parseToJsonElement(resp.bodyAsText()).asJsonObject
val cnt = json["totalCount"].asInt
if (cnt == 0) {
return Result.success(emptyList())
}
val regions = json["regions"].asJsonArray.map {
val region = it.asJsonObject
Region(
region["adcode"].asInt,
region["province"].asStringOrNull,
region["city"].asStringOrNull
)
}
return Result.success(regions)
}
}

View File

@ -0,0 +1,229 @@
package qq.service.msg
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.msg.api.IMsgService
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.common.ForwardElement
import io.kritor.common.ForwardMessageBody
import io.kritor.common.Scene
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.auto.toByteArray
import protobuf.message.*
import protobuf.message.longmsg.*
import qq.service.QQInterfaces
import qq.service.contact.ContactHelper
import qq.service.msg.MessageHelper.getMultiMsg
import qq.service.ticket.TicketHelper
import java.util.*
import kotlin.coroutines.resume
import kotlin.random.Random
import kotlin.time.Duration.Companion.seconds
internal object ForwardMessageHelper : QQInterfaces() {
suspend fun uploadMultiMsg(
contact: Contact,
messages: List<ForwardMessageBody>,
): Result<ForwardElement> {
var i = -1
val desc = MutableList(messages.size) { "" }
val forwardMsg = mutableMapOf<String, String>()
val msgs = messages.mapNotNull { msg ->
kotlin.runCatching {
when (msg.forwardMessageCase) {
ForwardMessageBody.ForwardMessageCase.MESSAGE_ID -> {
val record: MsgRecord = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java)
suspendCancellableCoroutine { continuation ->
service.getMsgsByMsgId(
contact,
arrayListOf(msg.messageId.toLong())
) { code, _, msgRecords ->
if (code == 0 && msgRecords.isNotEmpty()) {
continuation.resume(msgRecords.first())
} else {
continuation.resume(null)
}
}
continuation.invokeOnCancellation {
continuation.resume(null)
}
}
} ?: error("合并转发消息节点消息(id = ${msg.messageId})获取失败")
PushMsgBody(
msgHead = ResponseHead(
peerUid = record.senderUid,
receiverUid = record.peerUid,
forward = ResponseForward(
friendName = record.sendNickName
),
responseGrp = if (record.chatType == MsgConstant.KCHATTYPEGROUP) ResponseGrp(
groupCode = record.peerUin.toULong(),
memberCard = record.sendMemberName,
u1 = 2
) else null
),
contentHead = ContentHead(
msgType = when (record.chatType) {
MsgConstant.KCHATTYPEC2C -> 9
MsgConstant.KCHATTYPEGROUP -> 82
else -> throw UnsupportedOperationException("Unsupported chatType: ${contact.chatType}")
},
msgSubType = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
divSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) 175 else null,
msgViaRandom = record.msgId,
sequence = record.msgSeq, // idk what this is(i++)
msgTime = record.msgTime,
u2 = 1,
u6 = 0,
u7 = 0,
msgSeq = if (record.chatType == MsgConstant.KCHATTYPEC2C) record.msgSeq else null, // seq for dm
forwardHead = ForwardHead(
u1 = 0,
u2 = 0,
u3 = 0,
ub641 = "",
avatar = ""
)
),
body = MsgBody(
richText = record.elements.toKritorReqMessages(contact).toRichText(contact).onFailure {
error("消息合成失败: ${it.stackTraceToString()}")
}.onSuccess {
desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": " + it.first
}.getOrThrow().second
)
)
}
ForwardMessageBody.ForwardMessageCase.MESSAGE -> {
val _msg = msg.message
PushMsgBody(
msgHead = if (_msg.hasSender()) ResponseHead(
peer = if (_msg.sender.hasUin()) _msg.sender.uin else TicketHelper.getUin().toLong(),
peerUid = _msg.sender.uid,
receiverUid = TicketHelper.getUid(),
forward = ResponseForward(
friendName = if (_msg.sender.hasNick()) _msg.sender.nick else TicketHelper.getNickname()
)
) else ResponseHead(
peer = TicketHelper.getUin().toLong(),
peerUid = TicketHelper.getUid(),
receiverUid = TicketHelper.getUid(),
forward = ResponseForward(
friendName = TicketHelper.getNickname()
)
),
contentHead = ContentHead(
msgType = 9,
msgSubType = 175,
divSeq = 175,
msgViaRandom = Random.nextLong(),
sequence = _msg.messageSeq,
msgTime = _msg.time.toLong(),
u2 = 1,
u6 = 0,
u7 = 0,
msgSeq = _msg.messageSeq,
forwardHead = ForwardHead(
u1 = 0,
u2 = 0,
u3 = 2,
ub641 = "",
avatar = ""
)
),
body = MsgBody(
richText = _msg.elementsList.toRichText(contact).onSuccess {
desc[++i] =
(if (_msg.hasSender() && _msg.sender.hasNick()) _msg.sender.nick else TicketHelper.getNickname()) + ": " + it.first
}.onFailure {
error("消息合成失败: ${it.stackTraceToString()}")
}.getOrThrow().second
)
)
}
else -> null
}
}.onFailure {
LogCenter.log("消息节点解析失败:${it.stackTraceToString()}", Level.WARN)
}.getOrNull()
}.ifEmpty {
return Result.failure(Exception("消息节点为空"))
}
val payload = LongMsgPayload(
action = mutableListOf(
LongMsgAction(
command = "MultiMsg",
data = LongMsgContent(
body = msgs
)
)
).apply {
forwardMsg.map { msg ->
addAll(getMultiMsg(msg.value).getOrElse { return Result.failure(Exception("无法获取嵌套转发消息: $it")) }
.map { action ->
if (action.command == "MultiMsg") LongMsgAction(
command = msg.key,
data = action.data
) else action
})
}
}
)
val req = LongMsgReq(
sendInfo = when (contact.chatType) {
MsgConstant.KCHATTYPEC2C -> SendLongMsgInfo(
type = 1,
uid = LongMsgUid(contact.peerUid),
payload = DeflateTools.gzip(payload.toByteArray())
)
MsgConstant.KCHATTYPEGROUP -> SendLongMsgInfo(
type = 3,
uid = LongMsgUid(contact.peerUid),
groupUin = contact.peerUid.toULong(),
payload = DeflateTools.gzip(payload.toByteArray())
)
else -> throw UnsupportedOperationException("Unsupported chatType: ${contact.chatType}")
},
setting = LongMsgSettings(
field1 = 4,
field2 = 2,
field3 = 9,
field4 = 0
)
).toByteArray()
val fromServiceMsg =
sendBufferAW("trpc.group.long_msg_interface.MsgService.SsoSendLongMsg", true, req, timeout = 60.seconds)
?: return Result.failure(Exception("unable to upload multi message, response timeout"))
val rsp = runCatching {
fromServiceMsg.wupBuffer.slice(4).decodeProtobuf<LongMsgRsp>()
}.getOrElse {
fromServiceMsg.wupBuffer.decodeProtobuf<LongMsgRsp>()
}
val resId = rsp.sendResult?.resId ?: return Result.failure(Exception("unable to upload multi message"))
return Result.success(ForwardElement.newBuilder().apply {
this.resId = resId
this.summary = summary
this.uniseq = UUID.randomUUID().toString()
this.description = desc.slice(0..if (i < 3) i else 3).joinToString("\n")
}.build())
}
}

View File

@ -0,0 +1,69 @@
package qq.service.msg
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import protobuf.message.RichText
@Serializable
internal data class MessageResult(
@SerialName("message_id") val msgId: Int,
@SerialName("time") val time: Long
)
@Serializable
internal data class UploadForwardMessageResult(
@SerialName("res_id") val resId: String,
@SerialName("filename") val filename: String,
@SerialName("summary") val summary: String,
@SerialName("desc") val desc: String,
)
@Serializable
internal data class SendForwardMessageResult(
@SerialName("message_id") val msgId: Int,
@SerialName("res_id") val resId: String,
@SerialName("forward_id") val forwardId: String = resId
)
@Serializable
internal data class MessageDetail(
@SerialName("time") val time: Int,
@SerialName("message_type") val msgType: Int,
@SerialName("message_id") val msgId: Int,
@SerialName("message_id_qq") val qqMsgId: Long,
@SerialName("message_seq") val msgSeq: Long,
@SerialName("real_id") val realId: Long,
@SerialName("sender") val sender: MessageSender,
@SerialName("message") val message: RichText?,
@SerialName("group_id") val groupId: Long = 0,
@SerialName("peer_id") val peerId: Long,
@SerialName("target_id") val targetId: Long = 0,
)
@Serializable
internal data class GetForwardMsgResult(
@SerialName("messages") val msgs: List<MessageDetail>
)
@Serializable
internal data class MessageSender(
@SerialName("user_id") val userId: Long,
@SerialName("nickname") val nickName: String,
@SerialName("sex") val sex: String,
@SerialName("age") val age: Int,
@SerialName("uid") val uid: String,
@SerialName("tiny_id") val tinyId: String,
)
@Serializable
internal data class EssenceMessage(
@SerialName("sender_id") val senderId: Long,
@SerialName("sender_nick") val senderNick: String,
@SerialName("sender_time") val senderTime: Long,
@SerialName("operator_id") val operatorId: Long,
@SerialName("operator_nick") val operatorNick: String,
@SerialName("operator_time") val operatorTime: Long,
@SerialName("message_seq") val messageSeq: Long,
@SerialName("message_content") val messageContent: JsonElement,
)

View File

@ -1,16 +1,210 @@
package qq.service.msg
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.mobileqq.troop.api.ITroopMemberNameService
import com.tencent.qqnt.kernel.api.IKernelService
import com.tencent.qqnt.kernel.nativeinterface.Contact
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.TempChatGameSession
import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo
import com.tencent.qqnt.msg.api.IMsgService
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.header
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.jsonObject
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
import moe.fuqiuluo.shamrock.tools.EmptyJsonArray
import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.tools.asInt
import moe.fuqiuluo.shamrock.tools.asJsonArrayOrNull
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asLong
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.asStringOrNull
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.auto.toByteArray
import protobuf.message.longmsg.LongMsgAction
import protobuf.message.longmsg.LongMsgPayload
import protobuf.message.longmsg.LongMsgReq
import protobuf.message.longmsg.LongMsgRsp
import protobuf.message.longmsg.LongMsgSettings
import protobuf.message.longmsg.LongMsgUid
import protobuf.message.longmsg.RecvLongMsgInfo
import protobuf.oidb.cmd0x9082.Oidb0x9082
import qq.service.QQInterfaces
import qq.service.contact.ContactHelper
import qq.service.internals.msgService
import qq.service.ticket.TicketHelper
import tencent.im.oidb.cmd0xeac.oidb_0xeac
import tencent.im.oidb.oidb_sso
import kotlin.coroutines.resume
typealias MessageId = Long
internal object MessageHelper: QQInterfaces() {
suspend fun getEssenceMessageList(groupId: Long, page: Int = 0, pageSize: Int = 20): Result<List<EssenceMessage>>{
val cookie = TicketHelper.getCookie("qun.qq.com")
val bkn = TicketHelper.getBkn(TicketHelper.getRealSkey(TicketHelper.getUin()))
val url = "https://qun.qq.com/cgi-bin/group_digest/digest_list?bkn=${bkn}&group_code=${groupId}&page_start=${page}&page_limit=${pageSize}"
val response = GlobalClient.get(url) {
header("Cookie", cookie)
}
val body = Json.decodeFromStream<JsonElement>(response.body())
if (body.jsonObject["retcode"].asInt == 0) {
val data = body.jsonObject["data"].asJsonObject
val list = data["msg_list"].asJsonArrayOrNull
?: // is_end
return Result.success(ArrayList())
return Result.success(list.map {
val obj = it.jsonObject
val msgSeq = obj["msg_seq"].asLong
EssenceMessage(
senderId = obj["sender_uin"].asString.toLong(),
senderNick = obj["sender_nick"].asString,
senderTime = obj["sender_time"].asLong,
operatorId = obj["add_digest_uin"].asString.toLong(),
operatorNick = obj["add_digest_nick"].asString,
operatorTime = obj["add_digest_time"].asLong,
messageSeq = msgSeq,
messageContent = obj["msg_content"] ?: EmptyJsonArray
)
})
} else {
return Result.failure(Exception(body.jsonObject["retmsg"].asStringOrNull))
}
}
fun setGroupMessageCommentFace(peer: Long, msgSeq: ULong, faceIndex: String, isSet: Boolean) {
val serviceId = if (isSet) 1 else 2
sendOidb("OidbSvcTrpcTcp.0x9082_$serviceId", 36994, serviceId, Oidb0x9082(
peer = peer.toULong(),
msgSeq = msgSeq,
faceIndex = faceIndex,
flag = 1u,
u1 = 0u,
u2 = 0u
).toByteArray())
}
suspend fun setEssenceMessage(groupId: Long, seq: Long, rand: Long): String? {
val fromServiceMsg = sendOidbAW("OidbSvc.0xeac_1", 3756, 1, oidb_0xeac.ReqBody().apply {
group_code.set(groupId)
msg_seq.set(seq.toInt())
msg_random.set(rand.toInt())
}.toByteArray())
if (fromServiceMsg?.wupBuffer == null) {
return "no response"
}
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
return if (result.wording.has()) {
LogCenter.log("设置群精华失败: ${result.wording.get()}", Level.WARN)
"设置群精华失败: ${result.wording.get()}"
} else {
LogCenter.log("设置群精华 -> $groupId: $seq")
null
}
}
suspend fun deleteEssenceMessage(groupId: Long, seq: Long, rand: Long): String? {
val fromServiceMsg = sendOidbAW("OidbSvc.0xeac_2", 3756, 2, oidb_0xeac.ReqBody().apply {
group_code.set(groupId)
msg_seq.set(seq.toInt())
msg_random.set(rand.toInt())
}.toByteArray())
if (fromServiceMsg?.wupBuffer == null) {
return "no response"
}
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val result = oidb_0xeac.RspBody().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
return if (result.wording.has()) {
LogCenter.log("移除群精华失败: ${result.wording.get()}", Level.WARN)
"移除群精华失败: ${result.wording.get()}"
} else {
LogCenter.log("移除群精华 -> $groupId: $seq")
null
}
}
private suspend fun prepareTempChatFromGroup(
groupId: String,
peerId: String
): Result<Unit> {
LogCenter.log("主动临时消息,创建临时会话。", Level.INFO)
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
?: return Result.failure(Exception("获取消息服务失败"))
msgService.prepareTempChat(
TempChatPrepareInfo(
MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
ContactHelper.getUidByUinAsync(peerId = peerId.toLong()),
app.getRuntimeService(ITroopMemberNameService::class.java, "all")
.getTroopMemberNameRemarkFirst(groupId, peerId),
groupId,
EMPTY_BYTE_ARRAY,
app.currentUid,
"",
TempChatGameSession()
)
) { code, reason ->
if (code != 0) {
LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR)
}
}
return Result.success(Unit)
}
suspend fun sendMessage(contact: Contact, msgs: ArrayList<MsgElement>, retry: Int, uniseq: Long): Result<MessageId> {
if (contact.chatType == MsgConstant.KCHATTYPETEMPC2CFROMGROUP) {
prepareTempChatFromGroup(contact.guildId, contact.peerUid).getOrThrow()
}
return withTimeoutOrNull(5000) {
suspendCancellableCoroutine {
QRoute.api(IMsgService::class.java).sendMsg(contact, uniseq, msgs) { code: Int, msg: String ->
if (code == 0) {
it.resume(uniseq)
} else {
LogCenter.log("消息发送失败: $code:$msg", Level.WARN)
it.resume(null)
}
}
}
}?.let { Result.success(it) } ?: resendMsg(contact, uniseq, retry)
}
private suspend fun resendMsg(contact: Contact, msgId: MessageId, retry: Int): Result<MessageId> {
if (retry > 0) {
return withTimeoutOrNull(5000) {
suspendCancellableCoroutine {
QRoute.api(IMsgService::class.java).resendMsg(contact, msgId) { code, msg ->
if (code == 0) {
it.resume(msgId)
} else {
LogCenter.log("消息重发失败: $code:$msg", Level.WARN)
it.resume(null)
}
}
}
}?.let { Result.success(it) } ?: resendMsg(contact, msgId, retry - 1)
} else {
return Result.failure(Exception("消息发送失败:重试已达上限"))
}
}
suspend fun getTempChatInfo(chatType: Int, uid: String): Result<TempChatInfo> {
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
?: return Result.failure(Exception("获取消息服务失败"))
@ -28,4 +222,105 @@ internal object MessageHelper: QQInterfaces() {
} ?: return Result.failure(Exception("获取临时会话信息失败"))
return Result.success(info)
}
suspend fun generateContact(record: MsgRecord): Contact {
val peerId = when (record.chatType) {
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> record.senderUid
MsgConstant.KCHATTYPEGUILD -> record.channelId
else -> record.peerUin.toString()
}
return Contact(record.chatType, peerId, if (record.chatType == MsgConstant.KCHATTYPEGUILD) {
record.guildId
} else if(record.chatType == MsgConstant.KCHATTYPETEMPC2CFROMGROUP) {
val tempInfo = getTempChatInfo(record.chatType, peerId).getOrThrow()
tempInfo.groupCode
} else {
null
})
}
suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact {
val peerId = when (chatType) {
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
if (id.startsWith("u_")) id
else ContactHelper.getUidByUinAsync(id.toLong())
}
else -> id
}
return if (chatType == MsgConstant.KCHATTYPEGUILD) {
Contact(chatType, subId, peerId)
} else {
Contact(chatType, peerId, subId)
}
}
suspend fun getMultiMsg(resId: String): Result<List<LongMsgAction>> {
val req = LongMsgReq(
recvInfo = RecvLongMsgInfo(
uid = LongMsgUid(app.currentUid),
resId = resId,
u1 = 3
),
setting = LongMsgSettings(
field1 = 2,
field2 = 2,
field3 = 9,
field4 = 0
)
)
val fromServiceMsg = sendBufferAW(
"trpc.group.long_msg_interface.MsgService.SsoRecvLongMsg",
true,
req.toByteArray()
) ?: return Result.failure(Exception("unable to get multi message"))
val rsp = fromServiceMsg.wupBuffer.slice(4).decodeProtobuf<LongMsgRsp>()
val zippedPayload = DeflateTools.ungzip(
rsp.recvResult?.payload ?: return Result.failure(Exception("payload is empty"))
)
LogCenter.log(zippedPayload.toHexString(), Level.DEBUG)
return Result.success(
zippedPayload.decodeProtobuf<LongMsgPayload>().action
?: return Result.failure(Exception("action is empty"))
)
}
suspend fun getForwardMsg(resId: String): Result<List<MessageDetail>> {
val result = getMultiMsg(resId).getOrElse { return Result.failure(it) }
result.forEach {
if (it.command == "MultiMsg") {
return Result.success(it.data?.body?.map { msg ->
val chatType = if (msg.contentHead!!.msgType == 82) MsgConstant.KCHATTYPEGROUP else MsgConstant.KCHATTYPEC2C
MessageDetail(
time = msg.contentHead?.msgTime?.toInt() ?: 0,
msgType = chatType,
msgId = 0, // msgViaRandom为空 tx不给
qqMsgId = 0,
msgSeq = msg.contentHead!!.msgSeq ?: 0,
realId = msg.contentHead!!.msgSeq ?: 0,
sender = MessageSender(
msg.msgHead?.peer ?: 0,
msg.msgHead?.responseGrp?.memberCard ?: msg.msgHead?.forward?.friendName ?: "",
"unknown",
0,
msg.msgHead?.peerUid ?: "",
msg.msgHead?.peerUid ?: ""
),
message = msg.body?.richText,
peerId = msg.msgHead?.peer ?: 0,
groupId = if (chatType == MsgConstant.KCHATTYPEGROUP) msg.msgHead?.responseGrp?.groupCode?.toLong()
?: 0 else 0,
targetId = if (chatType != MsgConstant.KCHATTYPEGROUP) msg.msgHead?.peer ?: 0 else 0
)
} ?: return Result.failure(Exception("Msg is empty")))
}
}
return Result.failure(Exception("Can't find msg"))
}
fun generateMsgId(chatType: Int): Long {
return createMessageUniseq(chatType, System.currentTimeMillis())
}
external fun createMessageUniseq(chatType: Int, time: Long): Long
}

View File

@ -0,0 +1,458 @@
package qq.service.msg
import com.google.protobuf.ByteString
import com.tencent.mobileqq.qroute.QRoute
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.msg.api.IMsgService
import io.kritor.common.*
import io.kritor.common.Element.ElementType
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
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
import kotlin.coroutines.resume
/**
* 将NT消息com.tencent.qqnt.*转换为事件消息io.kritor.event.*)推送
*/
typealias NtMessages = ArrayList<MsgElement>
typealias Convertor = suspend (MsgRecord, MsgElement) -> Result<Element>
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.type = ElementType.AT
elem.setAt(AtElement.newBuilder().apply {
this.uid = text.atNtUid
this.uin = ContactHelper.getUinByUidAsync(text.atNtUid).toLong()
})
} else {
elem.type = ElementType.TEXT
elem.setText(TextElement.newBuilder().apply {
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.type = ElementType.POKE
elem.setPoke(PokeElement.newBuilder().apply {
this.id = face.vaspokeId
this.type = face.pokeType
this.strength = face.pokeStrength
})
} else {
when (face.faceIndex) {
114 -> {
elem.type = ElementType.BASKETBALL
elem.setBasketball(BasketballElement.newBuilder().apply {
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
})
}
358 -> {
elem.type = ElementType.DICE
elem.setDice(DiceElement.newBuilder().apply {
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
})
}
359 -> {
elem.type = ElementType.RPS
elem.setRps(RpsElement.newBuilder().apply {
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
})
}
394 -> {
elem.type = ElementType.FACE
elem.setFace(FaceElement.newBuilder().apply {
this.id = face.faceIndex
this.isBig = face.faceType == 3
this.result = face.resultId.ifNullOrEmpty { "1" }?.toInt() ?: 1
})
}
else -> {
elem.type = ElementType.FACE
elem.setFace(FaceElement.newBuilder().apply {
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.type = ElementType.IMAGE
elem.setImage(ImageElement.newBuilder().apply {
this.file = ByteString.copyFromUtf8(md5)
this.fileUrl = 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) ImageElement.ImageType.FLASH else if (image.original) ImageElement.ImageType.ORIGIN else ImageElement.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.type = ElementType.VOICE
elem.setVoice(VoiceElement.newBuilder().apply {
this.fileUrl = 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 = ByteString.copyFromUtf8(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.type = ElementType.VIDEO
elem.setVideo(VideoElement.newBuilder().apply {
this.file = ByteString.copyFromUtf8(md5.toHexString())
this.fileUrl = 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.type = ElementType.MARKET_FACE
elem.setMarketFace(MarketFaceElement.newBuilder().apply {
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.type = ElementType.FORWARD
elem.setForward(ForwardElement.newBuilder().apply {
this.resId = 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.type = ElementType.CONTACT
elem.setContact(ContactElement.newBuilder().apply {
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.type = ElementType.CONTACT
elem.setContact(ContactElement.newBuilder().apply {
this.scene = Scene.FRIEND
this.peer = info["jumpUrl"].asString.split("uin=")[1]
})
}
"com.tencent.map" -> {
val info = data["meta"].asJsonObject["Location.Search"].asJsonObject
elem.type = ElementType.LOCATION
elem.setLocation(LocationElement.newBuilder().apply {
this.lat = info["lat"].asString.toFloat()
this.lon = info["lng"].asString.toFloat()
this.address = info["address"].asString
this.title = info["name"].asString
})
}
else -> {
elem.type = ElementType.JSON
elem.setJson(JsonElement.newBuilder().apply {
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.type = ElementType.REPLY
elem.setReply(ReplyElement.newBuilder().apply {
val msgSeq = reply.replayMsgSeq
val contact = MessageHelper.generateContact(record)
val sourceRecords = withTimeoutOrNull(3000) {
suspendCancellableCoroutine {
QRoute.api(IMsgService::class.java)
.getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records ->
it.resume(records)
}
}
}
if (sourceRecords.isNullOrEmpty()) {
LogCenter.log("无法查询到回复的消息ID: seq = $msgSeq", Level.WARN)
this.messageId = reply.replayMsgId.toString()
} else {
this.messageId = sourceRecords.first().msgId.toString()
}
})
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.type = ElementType.FILE
elem.setFile(FileElement.newBuilder().apply {
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.type = ElementType.MARKDOWN
elem.setMarkdown(MarkdownElement.newBuilder().apply {
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.type = ElementType.BUBBLE_FACE
elem.setBubbleFace(BubbleFaceElement.newBuilder().apply {
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.type = ElementType.BUTTON
elem.setButton(ButtonElement.newBuilder().apply {
inlineKeyboard.rows.forEach { row ->
this.addRows(ButtonRow.newBuilder().apply {
row.buttons.forEach buttonsLoop@{ button ->
if (button == null) return@buttonsLoop
this.addButtons(Button.newBuilder().apply {
this.id = button.id
this.action = ButtonAction.newBuilder().apply {
this.type = button.type
this.permission = ButtonActionPermission.newBuilder().apply {
this.type = button.permissionType
button.specifyRoleIds?.let {
this.addAllRoleIds(it)
}
button.specifyTinyids?.let {
this.addAllUserIds(it)
}
}.build()
this.unsupportedTips = button.unsupportTips ?: ""
this.data = button.data ?: ""
this.reply = button.isReply
this.enter = button.enter
}.build()
this.renderData = ButtonRender.newBuilder().apply {
this.label = button.label ?: ""
this.visitedLabel = button.visitedLabel ?: ""
this.style = button.style
}.build()
})
}
})
}
})
return Result.success(elem.build())
}
operator fun get(case: Int): Convertor? {
return convertorMap[case]
}
}
suspend fun NtMessages.toKritorEventMessages(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
}

View File

@ -0,0 +1,269 @@
@file:OptIn(ExperimentalUnsignedTypes::class)
package qq.service.msg
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import io.kritor.common.*
import io.kritor.common.Element.ElementType
import io.kritor.common.ImageElement.ImageType
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUInt
import moe.fuqiuluo.shamrock.tools.asJsonArray
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.message.Elem
import protobuf.message.element.commelem.ButtonExtra
import protobuf.message.element.commelem.MarkdownExtra
import protobuf.message.element.commelem.QFaceExtra
import qq.service.bdh.RichProtoSvc
/**
* 将合并转发PBprotobuf.message.*转请求消息io.kritor.message.*)发送
*/
suspend fun List<Elem>.toKritorResponseMessages(contact: Contact): ArrayList<Element> {
val kritorMessages = ArrayList<Element>()
forEach { element ->
if (element.text != null) {
val text = element.text!!
if (text.attr6Buf != null) {
val at = ByteReadPacket(text.attr6Buf!!)
at.discardExact(7)
val uin = at.readUInt()
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.AT
this.at = AtElement.newBuilder().apply {
this.uin = uin.toLong()
}.build()
}.build())
} else {
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.TEXT
this.text = TextElement.newBuilder().apply {
this.text = text.str ?: ""
}.build()
}.build())
}
} else if (element.face != null) {
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.FACE
this.face = FaceElement.newBuilder().apply {
this.id = element.face!!.index ?: 0
}.build()
}.build())
} else if (element.customFace != null) {
val customFace = element.customFace!!
val md5 = customFace.md5.toHexString()
val origUrl = customFace.origUrl!!
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.IMAGE
this.image = ImageElement.newBuilder().apply {
this.fileMd5 = md5
this.type = if (customFace.origin == true) ImageType.ORIGIN else ImageType.COMMON
this.fileUrl = when (contact.chatType) {
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
origUrl,
md5
)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
else -> throw UnsupportedOperationException("Not supported chat type: $contact")
}
}.build()
}.build())
} else if (element.notOnlineImage != null) {
val md5 = element.notOnlineImage!!.picMd5.toHexString()
val origUrl = element.notOnlineImage!!.origUrl!!
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.IMAGE
this.image = ImageElement.newBuilder().apply {
this.fileMd5 = md5
this.type = if (element.notOnlineImage?.original == true) ImageType.ORIGIN else ImageType.COMMON
this.fileUrl = when (contact.chatType) {
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
origUrl,
md5
)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
else -> throw UnsupportedOperationException("Not supported chat type: $contact")
}
}.build()
}.build())
} else if (element.generalFlags != null) {
// val generalFlags = element.generalFlags!!
// if (generalFlags.longTextFlag == 1u) {
// kritorMessages.add(Element.newBuilder().apply {
// this.type = ElementType.FORWARD
// this.forward = forwardElement {
// this.id = generalFlags.longTextResid ?: ""
// }
// })
// }
} else if (element.srcMsg != null) {
val srcMsg = element.srcMsg!!
val msgId = srcMsg.pbReserve?.msgRand?.toLong() ?: 0
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.REPLY
this.reply = ReplyElement.newBuilder().apply {
this.messageId = msgId.toString()
}.build()
}.build())
} else if (element.lightApp != null) {
val data = element.lightApp!!.data!!
val jsonStr =
(if (data[0].toInt() == 1) DeflateTools.uncompress(data.slice(1)) else data.slice(1)).decodeToString()
val json = jsonStr.asJsonObject
when (json["app"].asString) {
"com.tencent.multimsg" -> {
val info = json["meta"].asJsonObject["detail"].asJsonObject
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.FORWARD
this.forward = ForwardElement.newBuilder().apply {
this.resId = info["resid"].asString
this.uniseq = info["uniseq"].asString
this.summary = info["summary"].asString
this.description = info["news"].asJsonArray.joinToString("\n") {
it.asJsonObject["text"].asString
}
}.build()
}.build())
}
"com.tencent.troopsharecard" -> {
val info = json["meta"].asJsonObject["contact"].asJsonObject
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.CONTACT
this.contact = ContactElement.newBuilder().apply {
this.scene = Scene.GROUP
this.peer = info["jumpUrl"].asString.split("group_code=")[1]
}.build()
}.build())
}
"com.tencent.contact.lua" -> {
val info = json["meta"].asJsonObject["contact"].asJsonObject
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.CONTACT
this.contact = ContactElement.newBuilder().apply {
this.scene = Scene.FRIEND
this.peer = info["jumpUrl"].asString.split("uin=")[1]
}.build()
}.build())
}
"com.tencent.map" -> {
val info = json["meta"].asJsonObject["Location.Search"].asJsonObject
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.LOCATION
this.location = LocationElement.newBuilder().apply {
this.lat = info["lat"].asString.toFloat()
this.lon = info["lng"].asString.toFloat()
this.address = info["address"].asString
this.title = info["name"].asString
}.build()
}.build())
}
else -> {
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.JSON
this.json = JsonElement.newBuilder().apply {
this.json = jsonStr
}.build()
}.build())
}
}
} else if (element.commonElem != null) {
val commonElem = element.commonElem!!
when (commonElem.serviceType) {
37 -> {
val qFaceExtra = commonElem.elem!!.decodeProtobuf<QFaceExtra>()
when (qFaceExtra.faceId) {
358 -> kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.DICE
this.dice = DiceElement.newBuilder().apply {
this.id = qFaceExtra.result!!.toInt()
}.build()
}.build())
359 -> kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.RPS
this.rps = RpsElement.newBuilder().apply {
this.id = qFaceExtra.result!!.toInt()
}.build()
}.build())
else -> kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.FACE
this.face = FaceElement.newBuilder().apply {
this.id = qFaceExtra.faceId ?: 0
this.isBig = false
this.result = qFaceExtra.result?.toInt() ?: 0
}.build()
}.build())
}
}
45 -> {
val markdownExtra = commonElem.elem!!.decodeProtobuf<MarkdownExtra>()
kritorMessages.add(Element.newBuilder().apply {
this.type = ElementType.MARKDOWN
this.markdown = MarkdownElement.newBuilder().apply {
this.markdown = markdownExtra.content!!
}.build()
}.build())
}
46 -> {
val buttonExtra = commonElem.elem!!.decodeProtobuf<ButtonExtra>()
kritorMessages.add(
Element.newBuilder().setButton(ButtonElement.newBuilder().apply {
this.addAllRows(buttonExtra.field1!!.rows!!.map { row ->
ButtonRow.newBuilder().apply {
this.addAllButtons(row.buttons!!.map { button ->
Button.newBuilder().apply {
this.id = button.id
this.renderData = ButtonRender.newBuilder().apply {
this.label = button.renderData?.label ?: ""
this.visitedLabel = button.renderData?.visitedLabel ?: ""
this.style = button.renderData?.style ?: 0
}.build()
this.action = ButtonAction.newBuilder().apply {
this.type = button.action?.type?:0
this.permission = ButtonActionPermission.newBuilder().apply {
this.type = button.action?.permission?.type?:0
button.action?.permission?.specifyRoleIds?.let {
this.addAllRoleIds(it)
}
button.action?.permission?.specifyUserIds?.let {
this.addAllUserIds(it)
}
}.build()
this.unsupportedTips = button.action?.unsupportTips ?: ""
this.data = button.action?.data ?: ""
this.reply = button.action?.reply ?: false
this.enter = button.action?.enter ?: false
}.build()
}.build()
})
}.build()
})
this.botAppid = buttonExtra.field1?.appid?.toLong() ?: 0L
}.build()).build()
)
}
}
}
}
return kritorMessages
}

View File

@ -0,0 +1,916 @@
package qq.service.msg
import android.graphics.BitmapFactory
import androidx.exifinterface.media.ExifInterface
import com.tencent.mobileqq.emoticon.QQSysFaceUtil
import com.tencent.mobileqq.pb.ByteStringMicro
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qphone.base.remote.ToServiceMsg
import com.tencent.qqnt.aio.adapter.api.IAIOPttApi
import com.tencent.qqnt.kernel.nativeinterface.*
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.FaceElement
import com.tencent.qqnt.kernel.nativeinterface.MarkdownElement
import com.tencent.qqnt.kernel.nativeinterface.MarketFaceElement
import com.tencent.qqnt.kernel.nativeinterface.ReplyElement
import com.tencent.qqnt.kernel.nativeinterface.TextElement
import com.tencent.qqnt.msg.api.IMsgService
import io.kritor.common.*
import io.kritor.common.Element.ElementType
import io.kritor.common.ImageElement.ImageType
import io.kritor.common.MusicElement.MusicPlatform
import io.kritor.common.VideoElement
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.config.EnableOldBDH
import moe.fuqiuluo.shamrock.config.get
import moe.fuqiuluo.shamrock.helper.ActionMsgException
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.LogicException
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.utils.AudioUtils
import moe.fuqiuluo.shamrock.utils.DownloadUtils
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MediaType
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import mqq.app.MobileQQ
import qq.service.QQInterfaces.Companion.app
import qq.service.bdh.FileTransfer
import qq.service.bdh.PictureResource
import qq.service.bdh.Private
import qq.service.bdh.Transfer
import qq.service.bdh.Troop
import qq.service.bdh.VideoResource
import qq.service.bdh.VoiceResource
import qq.service.bdh.trans
import qq.service.bdh.with
import qq.service.contact.ContactHelper
import qq.service.contact.longPeer
import qq.service.group.GroupHelper
import qq.service.internals.NTServiceFetcher
import qq.service.internals.msgService
import qq.service.lightapp.ArkAppInfo
import qq.service.lightapp.ArkMsgHelper
import qq.service.lightapp.LbsHelper
import qq.service.lightapp.MusicHelper
import qq.service.lightapp.WeatherHelper
import tencent.im.oidb.cmd0xb77.oidb_cmd0xb77
import tencent.im.oidb.cmd0xdc2.oidb_cmd0xdc2
import tencent.im.oidb.oidb_sso
import java.io.ByteArrayInputStream
import java.io.File
import kotlin.coroutines.resume
import kotlin.math.roundToInt
import kotlin.random.Random
import kotlin.random.nextInt
/**
* 将请求消息io.kritor.message转成NT消息com.tencent.qqnt.*)发送
*/
typealias Messages = Collection<Element>
private typealias NtConvertor = suspend (Contact, Long, Element) -> Result<MsgElement>
object NtMsgConvertor {
private val ntConvertors = mapOf<ElementType, NtConvertor>(
ElementType.TEXT to ::textConvertor,
ElementType.AT to ::atConvertor,
ElementType.FACE to ::faceConvertor,
ElementType.BUBBLE_FACE to ::bubbleFaceConvertor,
ElementType.REPLY to ::replyConvertor,
ElementType.IMAGE to ::imageConvertor,
ElementType.VOICE to ::voiceConvertor,
ElementType.VIDEO to ::videoConvertor,
ElementType.BASKETBALL to ::basketballConvertor,
ElementType.DICE to ::diceConvertor,
ElementType.RPS to ::rpsConvertor,
ElementType.POKE to ::pokeConvertor,
ElementType.MUSIC to ::musicConvertor,
ElementType.WEATHER to ::weatherConvertor,
ElementType.LOCATION to ::locationConvertor,
ElementType.SHARE to ::shareConvertor,
ElementType.CONTACT to ::contactConvertor,
ElementType.JSON to ::jsonConvertor,
ElementType.FORWARD to ::forwardConvertor,
ElementType.MARKDOWN to ::markdownConvertor,
ElementType.BUTTON to ::buttonConvertor,
)
suspend fun convertToNtMsgs(contact: Contact, msgId: Long, msgs: Messages): ArrayList<MsgElement> {
val ntMsgs = ArrayList<MsgElement>()
msgs.forEach {
val convertor = ntConvertors[it.type]
if (convertor == null) {
LogCenter.log("未知的消息类型: ${it.type}", Level.WARN)
} else {
try {
ntMsgs.add(convertor(contact, msgId, it).getOrThrow())
} catch (e: Throwable) {
if (e !is ActionMsgException) {
LogCenter.log("消息转换失败: ${it.type}", Level.WARN)
}
}
}
}
return ntMsgs
}
private suspend fun textConvertor(contact: Contact, msgId: Long, text: Element): Result<MsgElement> {
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPETEXT
elem.textElement = TextElement()
elem.textElement.content = text.text.text
return Result.success(elem)
}
private suspend fun atConvertor(contact: Contact, msgId: Long, sourceAt: Element): Result<MsgElement> {
if (contact.chatType != MsgConstant.KCHATTYPEGROUP) {
LogCenter.log("暂不支持非群聊的@元素", Level.WARN)
return Result.failure(ActionMsgException)
}
val elem = MsgElement()
val at = TextElement()
val uid = sourceAt.at.uid
if (uid == "all" || uid == "0") {
at.content = "@全体成员"
at.atType = MsgConstant.ATTYPEALL
at.atNtUid = "0"
} else {
val uin = ContactHelper.getUinByUidAsync(uid)
val info = GroupHelper.getTroopMemberInfoByUinV2(contact.peerUid, uin, true).onFailure {
LogCenter.log("无法获取群成员信息: contact=$contact, id=${uin}", Level.WARN)
}.getOrNull()
at.content = "@${
info?.troopnick.ifNullOrEmpty { info?.friendnick }
?: uin
}"
at.atType = MsgConstant.ATTYPEONE
at.atNtUid = uid
}
elem.textElement = at
elem.elementType = MsgConstant.KELEMTYPETEXT
return Result.success(elem)
}
private suspend fun faceConvertor(contact: Contact, msgId: Long, sourceFace: Element): Result<MsgElement> {
val serverId = sourceFace.face.id
val big = sourceFace.face.isBig || serverId == 394
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEFACE
val face = FaceElement()
// 1 old face
// 2 normal face
// 3 super face
// 4 is market face
// 5 is vas poke
face.faceType = if (big) 3 else 2
face.faceIndex = serverId
face.faceText = QQSysFaceUtil.getFaceDescription(QQSysFaceUtil.convertToLocal(serverId))
if (serverId == 394) {
face.stickerId = "40"
face.packId = "1"
face.sourceType = 1
face.stickerType = 3
face.randomType = 1
face.resultId = Random.nextInt(1..5).toString()
} else if (big) {
face.imageType = 0
face.stickerId = "30"
face.packId = "1"
face.sourceType = 1
face.stickerType = 1
face.randomType = 1
} else {
face.imageType = 0
face.packId = "0"
}
elem.faceElement = face
return Result.success(elem)
}
private suspend fun bubbleFaceConvertor(
contact: Contact,
msgId: Long,
sourceBubbleFace: Element
): Result<MsgElement> {
val faceId = sourceBubbleFace.bubbleFace.id
val local = QQSysFaceUtil.convertToLocal(faceId)
val name = QQSysFaceUtil.getFaceDescription(local)
val count = sourceBubbleFace.bubbleFace.count
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEFACEBUBBLE
val face = FaceBubbleElement()
face.faceType = 13
face.faceCount = count
face.faceSummary = QQSysFaceUtil.getPrueFaceDescription(name)
val smallYellowFaceInfo = SmallYellowFaceInfo()
smallYellowFaceInfo.index = faceId
smallYellowFaceInfo.compatibleText = face.faceSummary
smallYellowFaceInfo.text = face.faceSummary
face.yellowFaceInfo = smallYellowFaceInfo
face.faceFlag = 0
face.content = "[${face.faceSummary}]x$count"
elem.faceBubbleElement = face
return Result.success(elem)
}
private suspend fun replyConvertor(contact: Contact, msgId: Long, sourceReply: Element): Result<MsgElement> {
val element = MsgElement()
element.elementType = MsgConstant.KELEMTYPEREPLY
val reply = ReplyElement()
reply.replayMsgId = sourceReply.reply.messageId.toLong()
reply.sourceMsgIdInRecords = reply.replayMsgId
if (reply.replayMsgId == 0L) {
LogCenter.log("无法获取被回复消息", Level.ERROR)
}
withTimeoutOrNull(3000) {
suspendCancellableCoroutine {
QRoute.api(IMsgService::class.java)
.getMsgsByMsgId(contact, arrayListOf(reply.replayMsgId)) { _, _, records ->
it.resume(records)
}
}
}?.firstOrNull()?.let {
reply.replayMsgSeq = it.msgSeq
//reply.sourceMsgText = it.elements.firstOrNull { it.elementType == MsgConstant.KELEMTYPETEXT }?.textElement?.content
reply.replyMsgTime = it.msgTime
reply.senderUidStr = it.senderUid
reply.senderUid = it.senderUin
}
element.replyElement = reply
return Result.success(element)
}
private suspend fun imageConvertor(contact: Contact, msgId: Long, sourceImage: Element): Result<MsgElement> {
val isOriginal = sourceImage.image.type == ImageType.ORIGIN
val isFlash = sourceImage.image.type == ImageType.FLASH
val file = when (sourceImage.image.dataCase!!) {
ImageElement.DataCase.FILE_NAME -> {
val fileMd5 = sourceImage.image.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "")
.split(".")[0].lowercase()
FileUtils.getFileByMd5(fileMd5)
}
ImageElement.DataCase.FILE_PATH -> {
val filePath = sourceImage.image.filePath
File(filePath).inputStream().use {
FileUtils.saveFileToCache(it)
}
}
ImageElement.DataCase.FILE -> {
FileUtils.saveFileToCache(
ByteArrayInputStream(
sourceImage.image.file.toByteArray()
)
)
}
ImageElement.DataCase.FILE_URL -> {
val tmp = FileUtils.getTmpFile()
if (DownloadUtils.download(sourceImage.image.fileUrl, tmp)) {
tmp.inputStream().use {
FileUtils.saveFileToCache(it)
}.also {
tmp.delete()
}
} else {
tmp.delete()
return Result.failure(LogicException("图片资源下载失败: ${sourceImage.image.fileUrl}"))
}
}
ImageElement.DataCase.DATA_NOT_SET -> return Result.failure(IllegalArgumentException("ImageElement data is not set"))
}
if (EnableOldBDH.get()) {
Transfer with when (contact.chatType) {
MsgConstant.KCHATTYPEGROUP -> Troop(contact.peerUid)
MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> Private(
contact.longPeer().toString()
)
MsgConstant.KCHATTYPEGUILD -> Troop(contact.peerUid)
else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for PictureMsg"))
} trans PictureResource(file)
}
val elem = MsgElement()
elem.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 = isOriginal
pic.picType = FileUtils.getPicType(file)
pic.picSubType = 0
pic.isFlashPic = isFlash
elem.picElement = pic
return Result.success(elem)
}
private suspend fun voiceConvertor(contact: Contact, msgId: Long, sourceVoice: Element): Result<MsgElement> {
var file = when (sourceVoice.voice.dataCase!!) {
VoiceElement.DataCase.FILE_NAME -> {
val fileMd5 = sourceVoice.voice.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "")
.split(".")[0].lowercase()
FileUtils.getFileByMd5(fileMd5)
}
VoiceElement.DataCase.FILE_PATH -> {
val filePath = sourceVoice.voice.filePath
File(filePath).inputStream().use {
FileUtils.saveFileToCache(it)
}
}
VoiceElement.DataCase.FILE -> {
FileUtils.saveFileToCache(
sourceVoice.voice.file.toByteArray().inputStream()
)
}
VoiceElement.DataCase.FILE_URL -> {
val tmp = FileUtils.getTmpFile()
if (DownloadUtils.download(sourceVoice.voice.fileUrl, tmp)) {
tmp.inputStream().use {
FileUtils.saveFileToCache(it)
}.also {
tmp.delete()
}
} else {
tmp.delete()
return Result.failure(LogicException("音频资源下载失败: ${sourceVoice.voice.fileUrl}"))
}
}
VoiceElement.DataCase.DATA_NOT_SET -> return Result.failure(IllegalArgumentException("VoiceElement data is not set"))
}
val isMagic = sourceVoice.voice.magic
val ptt = PttElement()
when (AudioUtils.getMediaType(file)) {
MediaType.Silk -> {
LogCenter.log({ "Silk: $file" }, Level.DEBUG)
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
ptt.duration = QRoute.api(IAIOPttApi::class.java)
.getPttFileDuration(file.absolutePath)
}
MediaType.Amr -> {
LogCenter.log({ "Amr: $file" }, Level.DEBUG)
ptt.duration = AudioUtils.getDurationSec(file)
ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR
}
MediaType.Pcm -> {
LogCenter.log({ "Pcm To Silk: $file" }, Level.DEBUG)
val result = AudioUtils.pcmToSilk(file)
ptt.duration = (result.second * 0.001).roundToInt()
file = result.first
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
}
else -> {
LogCenter.log({ "Audio To SILK: $file" }, Level.DEBUG)
val result = AudioUtils.audioToSilk(file)
ptt.duration = runCatching {
QRoute.api(IAIOPttApi::class.java)
.getPttFileDuration(result.second.absolutePath)
}.getOrElse {
result.first
}
file = result.second
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
}
}
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEPTT
ptt.md5HexStr = QQNTWrapperUtil.CppProxy.genFileMd5Hex(file.absolutePath)
if (EnableOldBDH.get()) {
if (!(Transfer with when (contact.chatType) {
MsgConstant.KCHATTYPEGROUP -> Troop(contact.peerUid)
MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> Private(
contact.longPeer().toString()
)
MsgConstant.KCHATTYPEGUILD -> Troop(contact.peerUid)
else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for VoiceMsg"))
} trans VoiceResource(file))
) {
return Result.failure(RuntimeException("上传语音失败: $file"))
}
ptt.filePath = file.absolutePath
} else {
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) != file.length()) {
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
}
if (originalPath != null) {
ptt.filePath = originalPath
} else {
ptt.filePath = file.absolutePath
}
}
ptt.canConvert2Text = true
ptt.fileId = 0
ptt.fileUuid = ""
ptt.text = ""
if (!isMagic) {
ptt.voiceType = MsgConstant.KPTTVOICETYPESOUNDRECORD
ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPENONE
} else {
ptt.voiceType = MsgConstant.KPTTVOICETYPEVOICECHANGE
ptt.voiceChangeType = MsgConstant.KPTTVOICECHANGETYPEECHO
}
elem.pttElement = ptt
return Result.success(elem)
}
private suspend fun videoConvertor(contact: Contact, msgId: Long, sourceVideo: Element): Result<MsgElement> {
val elem = MsgElement()
val video = com.tencent.qqnt.kernel.nativeinterface.VideoElement()
val file = when (sourceVideo.video.dataCase!!) {
VideoElement.DataCase.FILE -> {
FileUtils.saveFileToCache(
sourceVideo.video.file.toByteArray().inputStream()
)
}
VideoElement.DataCase.FILE_NAME -> {
val fileMd5 = sourceVideo.video.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "")
.split(".")[0].lowercase()
FileUtils.getFileByMd5(fileMd5)
}
VideoElement.DataCase.FILE_PATH -> {
val filePath = sourceVideo.video.filePath
File(filePath).inputStream().use {
FileUtils.saveFileToCache(it)
}
}
VideoElement.DataCase.FILE_URL -> {
val tmp = FileUtils.getTmpFile()
if (DownloadUtils.download(sourceVideo.video.fileUrl, tmp)) {
tmp.inputStream().use {
FileUtils.saveFileToCache(it)
}.also {
tmp.delete()
}
} else {
tmp.delete()
return Result.failure(LogicException("视频资源下载失败: ${sourceVideo.video.fileUrl}"))
}
}
VideoElement.DataCase.DATA_NOT_SET -> return Result.failure(IllegalArgumentException("VideoElement data is not set"))
}
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!!)
}
if (EnableOldBDH.get()) {
Transfer with when (contact.chatType) {
MsgConstant.KCHATTYPEGROUP -> Troop(contact.peerUid)
MsgConstant.KCHATTYPETEMPC2CFROMGROUP, MsgConstant.KCHATTYPEC2C -> Private(
contact.longPeer().toString()
)
MsgConstant.KCHATTYPEGUILD -> Troop(contact.peerUid)
else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for VideoMsg"))
} trans VideoResource(file, File(thumbPath.toString()))
}
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
elem.elementType = MsgConstant.KELEMTYPEVIDEO
return Result.success(elem)
}
private suspend fun basketballConvertor(
contact: Contact,
msgId: Long,
sourceBasketball: Element
): Result<MsgElement> {
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEFACE
val face = FaceElement()
face.faceIndex = 114
face.faceText = "/篮球"
face.faceType = 3
face.packId = "1"
face.stickerId = "13"
face.sourceType = 1
face.stickerType = 2
face.resultId = Random.nextInt(1..5).toString()
face.surpriseId = ""
face.randomType = 1
elem.faceElement = face
return Result.success(elem)
}
private suspend fun diceConvertor(contact: Contact, msgId: Long, sourceDice: Element): Result<MsgElement> {
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
val market = MarketFaceElement(
6, 1, 11464, 3, 0, 200, 200,
"[骰子]", "4823d3adb15df08014ce5d6796b76ee1", "409e2a69b16918f9",
null, null, 0, 0, 0, 1, 0,
null, null, null, // jumpurl
"", null, null,
null, null, arrayListOf(MarketFaceSupportSize(200, 200)), null
)
elem.marketFaceElement = market
return Result.success(elem)
}
private suspend fun rpsConvertor(contact: Contact, msgId: Long, sourceRps: Element): Result<MsgElement> {
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
val market = MarketFaceElement(
6, 1, 11415, 3, 0, 200, 200,
"[猜拳]", "83C8A293AE65CA140F348120A77448EE", "7de39febcf45e6db",
null, null, 0, 0, 0, 1, 0,
null, null, null,
"", null, null,
null, null, arrayListOf(MarketFaceSupportSize(200, 200)), null
)
elem.marketFaceElement = market
return Result.success(elem)
}
private suspend fun pokeConvertor(contact: Contact, msgId: Long, sourcePoke: Element): Result<MsgElement> {
val elem = MsgElement()
val face = FaceElement()
face.faceIndex = 0
face.faceText = ""
face.faceType = 5
face.packId = null
face.pokeType = sourcePoke.poke.type
face.spokeSummary = ""
face.doubleHit = 0
face.vaspokeId = sourcePoke.poke.id
face.vaspokeName = ""
face.vaspokeMinver = ""
face.pokeStrength = sourcePoke.poke.strength
face.msgType = 0
face.faceBubbleCount = 0
face.oldVersionStr = "[截一戳]请使用最新版手机QQ体验新功能。"
face.pokeFlag = 0
elem.elementType = MsgConstant.KELEMTYPEFACE
elem.faceElement = face
return Result.success(elem)
}
private suspend fun musicConvertor(contact: Contact, msgId: Long, sourceMusic: Element): Result<MsgElement> {
when (val type = sourceMusic.music.platform) {
MusicPlatform.QQ -> {
val id = sourceMusic.music.id
if (!MusicHelper.tryShareQQMusicById(contact, msgId, id)) {
LogCenter.log("无法发送QQ音乐分享", Level.ERROR)
}
}
MusicPlatform.NetEase -> {
val id = sourceMusic.music.id
if (!MusicHelper.tryShare163MusicById(contact, msgId, id)) {
LogCenter.log("无法发送网易云音乐分享", Level.ERROR)
}
}
MusicPlatform.Custom -> {
val data = sourceMusic.music.custom
ArkMsgHelper.tryShareMusic(
contact,
msgId,
ArkAppInfo.QQMusic,
data.title,
data.author,
data.url,
data.pic,
data.audio
)
}
else -> LogCenter.log("不支持的音乐分享类型: $type", Level.ERROR)
}
return Result.failure(ActionMsgException)
}
private suspend fun weatherConvertor(contact: Contact, msgId: Long, sourceWeather: Element): Result<MsgElement> {
val code = if (sourceWeather.weather.code.isNullOrEmpty()) {
val city = sourceWeather.weather.city
WeatherHelper.searchCity(city).onFailure {
LogCenter.log("无法获取城市天气: $city", Level.ERROR)
}.getOrThrow().first().adcode
} else sourceWeather.weather.code.toInt()
WeatherHelper.fetchWeatherCard(code).onSuccess {
val element = MsgElement()
element.elementType = MsgConstant.KELEMTYPEARKSTRUCT
val share = it["weekStore"]
.asJsonObject["share"]
.asJsonObject["data"].toString()
element.arkElement =
ArkElement(share, null, MsgConstant.ARKSTRUCTELEMENTSUBTYPEUNKNOWN)
return Result.success(element)
}.onFailure {
return Result.failure(it)
}
return Result.failure(ActionMsgException)
}
private suspend fun locationConvertor(contact: Contact, msgId: Long, sourceLocation: Element): Result<MsgElement> {
LbsHelper.tryShareLocation(
contact,
sourceLocation.location.lat.toDouble(),
sourceLocation.location.lon.toDouble()
).onFailure {
LogCenter.log("无法发送位置分享", Level.ERROR)
}
return Result.failure(ActionMsgException)
}
private suspend fun shareConvertor(contact: Contact, msgId: Long, sourceShare: Element): Result<MsgElement> {
val url = sourceShare.share.url
val image = sourceShare.share.image.ifNullOrEmpty {
val startWithPrefix = url.startsWith("http://") || url.startsWith("https://")
val endWithPrefix = url.startsWith("/")
"http://" + url.split("/")[if (startWithPrefix) 2 else 0] + if (!endWithPrefix) {
"/favicon.ico"
} else {
"favicon.ico"
}
}!!
val title = sourceShare.share.title
val content = sourceShare.share.content
val reqBody = oidb_cmd0xdc2.ReqBody()
val info = oidb_cmd0xb77.ReqBody()
info.appid.set(100446242L)
info.app_type.set(1)
info.msg_style.set(0)
info.recv_uin.set(contact.longPeer())
val clientInfo = oidb_cmd0xb77.ClientInfo()
clientInfo.platform.set(1)
info.client_info.set(clientInfo)
val richMsgBody = oidb_cmd0xb77.RichMsgBody()
richMsgBody.using_ark.set(true)
richMsgBody.title.set(title)
richMsgBody.summary.set(content ?: url)
richMsgBody.brief.set("[分享] $title")
richMsgBody.url.set(url)
richMsgBody.picture_url.set(image)
info.ext_info.set(oidb_cmd0xb77.ExtInfo().also {
it.msg_seq.set(msgId)
})
info.rich_msg_body.set(richMsgBody)
reqBody.msg_body.set(info)
val sendTo = oidb_cmd0xdc2.BatchSendReq()
when (contact.chatType) {
MsgConstant.KCHATTYPEGROUP -> sendTo.send_type.set(1)
MsgConstant.KCHATTYPEC2C -> sendTo.send_type.set(0)
else -> return Result.failure(Exception("Not supported chatType(${contact.chatType}) for ShareMsg"))
}
sendTo.recv_uin.set(contact.peerUid.toLong())
reqBody.batch_send_req.add(sendTo)
val to = ToServiceMsg("mobileqq.service", app.currentAccountUin, "OidbSvc.0xdc2_34")
val oidb = oidb_sso.OIDBSSOPkg()
oidb.uint32_command.set(0xdc2)
oidb.uint32_service_type.set(34)
oidb.bytes_bodybuffer.set(ByteStringMicro.copyFrom(reqBody.toByteArray()))
oidb.str_client_version.set(PlatformUtils.getClientVersion(MobileQQ.getContext()))
to.putWupBuffer(oidb.toByteArray())
to.addAttribute("req_pb_protocol_flag", true)
app.sendToService(to)
return Result.failure(ActionMsgException)
}
private suspend fun contactConvertor(contact: Contact, msgId: Long, sourceContact: Element): Result<MsgElement> {
val elem = MsgElement()
when (val scene = sourceContact.contact.scene) {
Scene.FRIEND -> {
val ark = ArkElement(ContactHelper.getSharePrivateArkMsg(contact.longPeer()), null, null)
elem.arkElement = ark
}
Scene.GROUP -> {
val ark = ArkElement(ContactHelper.getShareTroopArkMsg(contact.longPeer()), null, null)
elem.arkElement = ark
}
else -> return Result.failure(LogicException("不支持的联系人分享类型: $scene"))
}
elem.elementType = MsgConstant.KELEMTYPEARKSTRUCT
return Result.success(elem)
}
private suspend fun jsonConvertor(contact: Contact, msgId: Long, sourceJson: Element): Result<MsgElement> {
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEARKSTRUCT
val ark = ArkElement(sourceJson.json.json, null, null)
elem.arkElement = ark
return Result.success(elem)
}
private suspend fun markdownConvertor(contact: Contact, msgId: Long, sourceMarkdown: Element): Result<MsgElement> {
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEMARKDOWN
val markdownElement = MarkdownElement(sourceMarkdown.markdown.markdown)
elem.markdownElement = markdownElement
return Result.success(elem)
}
private suspend fun buttonConvertor(contact: Contact, msgId: Long, sourceButton: Element): Result<MsgElement> {
fun tryNewKeyboardButton(button: Button): InlineKeyboardButton {
val renderData = button.renderData
val action = button.action
val permission = action.permission
return runCatching {
InlineKeyboardButton(
button.id, renderData.label, renderData.visitedLabel, renderData.style,
action.type, 0,
action.unsupportedTips,
action.data, false,
permission.type,
ArrayList(permission.roleIdsList),
ArrayList(permission.userIdsList),
false, 0, false, arrayListOf()
)
}.getOrElse {
InlineKeyboardButton(
button.id, renderData.label, renderData.visitedLabel, renderData.style,
action.type, 0,
action.unsupportedTips,
action.data, false,
permission.type,
ArrayList(permission.roleIdsList),
ArrayList(permission.userIdsList)
)
}
}
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEINLINEKEYBOARD
val rows = arrayListOf<InlineKeyboardRow>()
val keyboard = sourceButton.button
keyboard.rowsList.forEach { row ->
val buttons = arrayListOf<InlineKeyboardButton>()
row.buttonsList.forEach { button ->
buttons.add(tryNewKeyboardButton(button))
}
rows.add(InlineKeyboardRow(buttons))
}
elem.inlineKeyboardElement = InlineKeyboardElement(rows, 0)
return Result.success(elem)
}
private suspend fun forwardConvertor(contact: Contact, msgId: Long, sourceForward: Element): Result<MsgElement> {
val resId = sourceForward.forward.resId
val filename = sourceForward.forward.uniseq
var summary = sourceForward.forward.summary
val descriptions = sourceForward.forward.description
var news = descriptions?.split("\n")?.map { "text" to it }
if (news == null || summary == null) {
val forwardMsg = MessageHelper.getForwardMsg(resId).getOrElse { return Result.failure(it) }
if (news == null) {
news = forwardMsg.map {
"text" to it.sender.nickName + ": " + descriptions
}
}
if (summary == null) {
summary = "查看${forwardMsg.size}条转发消息"
}
}
val json = mapOf(
"app" to "com.tencent.multimsg",
"config" to mapOf(
"autosize" to 1,
"forward" to 1,
"round" to 1,
"type" to "normal",
"width" to 300
),
"desc" to "[聊天记录]",
"extra" to mapOf(
"filename" to filename,
"tsum" to 2
).json.toString(),
"meta" to mapOf(
"detail" to mapOf(
"news" to news,
"resid" to resId,
"source" to "群聊的聊天记录",
"summary" to summary,
"uniseq" to filename
)
),
"prompt" to "[聊天记录]",
"ver" to "0.0.0.5",
"view" to "contact"
)
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEARKSTRUCT
val ark = ArkElement(json.json.toString(), null, null)
elem.arkElement = ark
return Result.success(elem)
}
}

View File

@ -0,0 +1,422 @@
package qq.service.msg
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.msg.api.IMsgService
import io.kritor.common.*
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
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
import qq.service.contact.longPeer
import kotlin.coroutines.resume
/**
* 将NT消息com.tencent.qqnt.*转换为请求消息io.kritor.message.*)推送
*/
private typealias ReqConvertor = suspend (Contact, MsgElement) -> Result<Element>
private object ReqMsgConvertor {
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(contact: Contact, element: MsgElement): Result<Element> {
val text = element.textElement
val elem = Element.newBuilder()
if (text.atType != MsgConstant.ATTYPEUNKNOWN) {
elem.setAt(AtElement.newBuilder().apply {
this.uid = text.atNtUid
this.uin = ContactHelper.getUinByUidAsync(text.atNtUid).toLong()
})
} else {
elem.setText(TextElement.newBuilder().apply {
this.text = text.content
})
}
return Result.success(elem.build())
}
suspend fun convertFace(contact: Contact, element: MsgElement): Result<Element> {
val face = element.faceElement
val elem = Element.newBuilder()
if (face.faceType == 5) {
elem.setPoke(PokeElement.newBuilder().apply {
this.id = face.vaspokeId
this.type = face.pokeType
this.strength = face.pokeStrength
})
} else {
when (face.faceIndex) {
114 -> elem.setBasketball(BasketballElement.newBuilder().apply {
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
})
358 -> elem.setDice(DiceElement.newBuilder().apply {
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
})
359 -> elem.setRps(RpsElement.newBuilder().apply {
this.id = face.resultId.ifNullOrEmpty { "0" }?.toInt() ?: 0
})
394 -> elem.setFace(FaceElement.newBuilder().apply {
this.id = face.faceIndex
this.isBig = face.faceType == 3
this.result = face.resultId.ifNullOrEmpty { "1" }?.toInt() ?: 1
})
else -> elem.setFace(FaceElement.newBuilder().apply {
this.id = face.faceIndex
this.isBig = face.faceType == 3
})
}
}
return Result.success(elem.build())
}
suspend fun convertImage(contact: Contact, 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 = contact.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.newBuilder().apply {
this.fileMd5 = md5
this.fileUrl = when (contact.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 = contact.longPeer().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 = contact.longPeer().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 = contact.longPeer().toString(),
subPeer = "0"
)
else -> throw UnsupportedOperationException("Not supported chat type: ${contact.chatType}")
}
this.type =
if (image.isFlashPic == true) ImageElement.ImageType.FLASH else if (image.original) ImageElement.ImageType.ORIGIN else ImageElement.ImageType.COMMON
this.subType = image.picSubType
})
return Result.success(elem.build())
}
suspend fun convertVoice(contact: Contact, 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.newBuilder().apply {
this.fileUrl = when (contact.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: ${contact.chatType}")
}
this.fileMd5 = md5
this.magic = ptt.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE
})
return Result.success(elem.build())
}
suspend fun convertVideo(contact: Contact, 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.newBuilder().apply {
this.fileMd5 = md5.toHexString()
this.fileUrl = when (contact.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: ${contact.chatType}")
}
})
return Result.success(elem.build())
}
suspend fun convertMarketFace(contact: Contact, element: MsgElement): Result<Element> {
val marketFace = element.marketFaceElement
val elem = Element.newBuilder()
elem.setMarketFace(MarketFaceElement.newBuilder().apply {
this.id = marketFace.emojiId
})
// TODO
return Result.success(elem.build())
}
suspend fun convertStructJson(contact: Contact, 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.newBuilder().apply {
this.resId = 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.newBuilder().apply {
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.newBuilder().apply {
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.newBuilder().apply {
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.newBuilder().apply {
this.json = data.toString()
})
}
return Result.success(elem.build())
}
suspend fun convertReply(contact: Contact, element: MsgElement): Result<Element> {
val reply = element.replyElement
val elem = Element.newBuilder()
elem.setReply(ReplyElement.newBuilder().apply {
val msgSeq = reply.replayMsgSeq
val sourceRecords = withTimeoutOrNull(3000) {
suspendCancellableCoroutine {
QRoute.api(IMsgService::class.java)
.getMsgsBySeqAndCount(contact, msgSeq, 1, true) { _, _, records ->
it.resume(records)
}
}
}
if (sourceRecords.isNullOrEmpty()) {
LogCenter.log("无法查询到回复的消息ID: seq = $msgSeq", Level.WARN)
this.messageId = reply.replayMsgId.toString()
} else {
this.messageId = sourceRecords.first().msgId.toString()
}
})
return Result.success(elem.build())
}
suspend fun convertFile(contact: Contact, 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 (contact.chatType) {
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(
contact.guildId,
contact.longPeer().toString(),
fileId,
bizId
)
else -> RichProtoSvc.getGroupFileDownUrl(contact.longPeer(), fileId, bizId)
}
val elem = Element.newBuilder()
elem.setFile(FileElement.newBuilder().apply {
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(contact: Contact, element: MsgElement): Result<Element> {
val markdown = element.markdownElement
val elem = Element.newBuilder()
elem.setMarkdown(MarkdownElement.newBuilder().apply {
this.markdown = markdown.content
})
return Result.success(elem.build())
}
suspend fun convertBubbleFace(contact: Contact, element: MsgElement): Result<Element> {
val bubbleFace = element.faceBubbleElement
val elem = Element.newBuilder()
elem.setBubbleFace(BubbleFaceElement.newBuilder().apply {
this.id = bubbleFace.yellowFaceInfo.index
this.count = bubbleFace.faceCount ?: 1
})
return Result.success(elem.build())
}
suspend fun convertInlineKeyboard(contact: Contact, element: MsgElement): Result<Element> {
val inlineKeyboard = element.inlineKeyboardElement
val elem = Element.newBuilder()
elem.setButton(ButtonElement.newBuilder().apply {
this.addAllRows(inlineKeyboard.rows.map { row ->
ButtonRow.newBuilder().apply {
this.addAllButtons(row.buttons.map { button ->
Button.newBuilder().apply {
this.id = button.id
this.renderData = ButtonRender.newBuilder().apply {
this.label = button.label ?: ""
this.visitedLabel = button.visitedLabel ?: ""
this.style = button.style
}.build()
this.action = ButtonAction.newBuilder().apply {
this.type = button.type
this.permission = ButtonActionPermission.newBuilder().apply {
this.type = button.permissionType
button.specifyRoleIds?.let {
this.addAllRoleIds(it)
}
button.specifyTinyids?.let {
this.addAllUserIds(it)
}
}.build()
this.unsupportedTips = button.unsupportTips ?: ""
this.data = button.data ?: ""
this.reply = button.isReply
this.enter = button.enter
}.build()
}.build()
})
}.build()
})
this.botAppid = inlineKeyboard.botAppid
})
return Result.success(elem.build())
}
operator fun get(case: Int): ReqConvertor? {
return convertorMap[case]
}
}
suspend fun NtMessages.toKritorReqMessages(contact: Contact): ArrayList<Element> {
val result = arrayListOf<Element>()
forEach {
ReqMsgConvertor[it.elementType]?.invoke(contact, it)?.onSuccess {
result.add(it)
}?.onFailure {
if (it !is ActionMsgException) {
LogCenter.log("消息转换异常: " + it.stackTraceToString(), Level.WARN)
}
}
}
return result
}

View File

@ -0,0 +1,566 @@
package qq.service.msg
import android.graphics.BitmapFactory
import android.util.Base64
import androidx.exifinterface.media.ExifInterface
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.msg.api.IMsgService
import io.kritor.common.Element
import io.kritor.common.ImageElement
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.LogicException
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.json
import moe.fuqiuluo.shamrock.tools.putBuf32Long
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.utils.DownloadUtils
import moe.fuqiuluo.shamrock.utils.FileUtils
import protobuf.auto.toByteArray
import protobuf.message.Elem
import protobuf.message.RichText
import protobuf.message.element.*
import protobuf.message.element.commelem.Action
import protobuf.message.element.commelem.Button
import protobuf.message.element.commelem.ButtonExtra
import protobuf.message.element.commelem.MarkdownExtra
import protobuf.message.element.commelem.Object1
import protobuf.message.element.commelem.Permission
import protobuf.message.element.commelem.PokeExtra
import protobuf.message.element.commelem.QFaceExtra
import protobuf.message.element.commelem.RenderData
import protobuf.message.element.commelem.Row
import protobuf.oidb.cmd0x11c5.C2CUserInfo
import protobuf.oidb.cmd0x11c5.GroupUserInfo
import qq.service.QQInterfaces
import qq.service.bdh.NtV2RichMediaSvc
import qq.service.bdh.NtV2RichMediaSvc.fetchGroupResUploadTo
import qq.service.contact.ContactHelper
import qq.service.contact.longPeer
import qq.service.group.GroupHelper
import qq.service.lightapp.WeatherHelper
import java.io.ByteArrayInputStream
import java.io.File
import java.nio.ByteBuffer
import java.util.UUID
import kotlin.coroutines.resume
import kotlin.random.Random
import kotlin.random.nextULong
import kotlin.time.Duration.Companion.seconds
/**
* 请求消息io.kritor.message.*)转换合并转发消息
*/
suspend fun List<Element>.toRichText(contact: Contact): Result<Pair<String, RichText>> {
val summary = StringBuilder()
val elems = ArrayList<Elem>()
forEach {
try {
when(it.type!!) {
Element.ElementType.TEXT -> {
val text = it.text.text
val elem = Elem(
text = TextMsg(text)
)
elems.add(elem)
summary.append(text)
}
Element.ElementType.AT -> {
when (contact.chatType) {
MsgConstant.KCHATTYPEGROUP -> {
val qq = ContactHelper.getUinByUidAsync(it.at.uid)
val type: Int
val nick = if (it.at.uid == "all" || it.at.uin == 0L) {
type = 1
"@全体成员"
} else {
type = 0
"@" + (GroupHelper.getTroopMemberInfoByUinV2(contact.longPeer().toString(), qq, true).let {
val info = it.getOrNull()
if (info == null)
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
else info.troopnick
.ifNullOrEmpty { info.friendnick }
.ifNullOrEmpty { qq }
})
}
val attr6 = ByteBuffer.allocate(6)
attr6.put(byteArrayOf(0, 1, 0, 0, 0))
attr6.put(nick.length.toByte())
attr6.putChar(type.toChar())
attr6.putBuf32Long(qq.toLong())
attr6.put(byteArrayOf(0, 0))
val elem = Elem(
text = TextMsg(str = nick, attr6Buf = attr6.array())
)
elems.add(elem)
summary.append(nick)
}
MsgConstant.KCHATTYPEC2C -> {
val qq = ContactHelper.getUinByUidAsync(it.at.uid)
val display = "@" + (ContactHelper.getProfileCard(qq.toLong()).onSuccess {
it.strNick.ifNullOrEmpty { qq }
}.onFailure {
LogCenter.log("无法获取QQ信息: $qq", Level.WARN)
})
val elem = Elem(
text = TextMsg(str = display)
)
elems.add(elem)
summary.append(display)
}
else -> throw UnsupportedOperationException("Unsupported chatType($contact) for AtMsg")
}
}
Element.ElementType.FACE -> {
val faceId = it.face.id
val elem = if (it.face.isBig) {
Elem(
commonElem = CommonElem(
serviceType = 37,
elem = QFaceExtra(
packId = "1",
stickerId = "1",
faceId = faceId,
field4 = 1,
field5 = 1,
result = "",
faceText = "", //todo 表情名字
field9 = 1
).toByteArray(),
businessType = 1
)
)
} else {
Elem(
face = FaceMsg(
index = faceId
)
)
}
elems.add(elem)
summary.append("[表情]")
}
Element.ElementType.BUBBLE_FACE -> throw UnsupportedOperationException("Unsupported Element.ElementType.BUBBLE_FACE")
Element.ElementType.REPLY -> {
val msgId = it.reply.messageId
withTimeoutOrNull(3000) {
suspendCancellableCoroutine {
QRoute.api(IMsgService::class.java).getMsgsByMsgId(contact, arrayListOf(msgId.toLong())) { _, _, records ->
it.resume(records)
}
}
}?.firstOrNull()?.let {
val sourceContact = MessageHelper.generateContact(it)
elems.add(Elem(
srcMsg = SourceMsg(
origSeqs = listOf(it.msgSeq.toInt()),
senderUin = it.senderUin.toULong(),
time = it.msgTime.toULong(),
flag = 1u,
elems = it.elements
.toKritorReqMessages(sourceContact)
.toRichText(contact).getOrThrow().second.elements,
type = 0u,
pbReserve = SourceMsg.Companion.PbReserve(
msgRand = Random.nextULong(),
senderUid = it.senderUid,
receiverUid = QQInterfaces.app.currentUid,
field8 = Random.nextInt(0, 10000)
),
)
))
}
summary.append("[回复消息]")
}
Element.ElementType.IMAGE -> {
val type = it.image.type
val isOriginal = type == ImageElement.ImageType.ORIGIN
val file = when(it.image.dataCase!!) {
ImageElement.DataCase.FILE_NAME -> {
val fileMd5 = it.image.fileName.replace(regex = "[{}\\-]".toRegex(), replacement = "").split(".")[0].lowercase()
FileUtils.getFileByMd5(fileMd5)
}
ImageElement.DataCase.FILE_PATH -> {
val filePath = it.image.filePath
File(filePath).inputStream().use {
FileUtils.saveFileToCache(it)
}
}
ImageElement.DataCase.FILE -> {
FileUtils.saveFileToCache(
ByteArrayInputStream(
it.image.file.toByteArray()
)
)
}
ImageElement.DataCase.FILE_URL -> {
val tmp = FileUtils.getTmpFile()
if(DownloadUtils.download(it.image.fileUrl, tmp)) {
tmp.inputStream().use {
FileUtils.saveFileToCache(it)
}.also {
tmp.delete()
}
} else {
tmp.delete()
throw LogicException("图片资源下载失败: ${it.image.fileUrl}")
}
}
ImageElement.DataCase.DATA_NOT_SET -> throw IllegalArgumentException("ImageElement data is not set")
}
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(file.absolutePath, options)
val exifInterface = ExifInterface(file.absolutePath)
val orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED
)
val picWidth: Int
val picHeight: Int
if (orientation != ExifInterface.ORIENTATION_ROTATE_90 && orientation != ExifInterface.ORIENTATION_ROTATE_270) {
picWidth = options.outWidth
picHeight = options.outHeight
} else {
picWidth = options.outHeight
picHeight = options.outWidth
}
val fileInfo = NtV2RichMediaSvc.tryUploadResourceByNt(
chatType = contact.chatType,
elementType = MsgConstant.KELEMTYPEPIC,
resources = arrayListOf(file),
timeout = 30.seconds
).getOrThrow().first()
runCatching {
fileInfo.uuid.toUInt()
}.onFailure {
NtV2RichMediaSvc.requestUploadNtPic(file, fileInfo.md5, fileInfo.sha, fileInfo.fileName, picWidth.toUInt(), picHeight.toUInt(), 5, contact.chatType) {
when(contact.chatType) {
MsgConstant.KCHATTYPEGROUP -> {
sceneType = 2u
grp = GroupUserInfo(fetchGroupResUploadTo().toULong())
}
MsgConstant.KCHATTYPEC2C -> {
sceneType = 1u
c2c = C2CUserInfo(
accountType = 2u,
uid = contact.peerUid
)
}
else -> error("不支持的合并转发图片类型")
}
}.onFailure {
LogCenter.log("获取MultiMedia图片信息失败: $it", Level.ERROR)
}.onSuccess {
//LogCenter.log({ "获取MultiMedia图片信息成功: ${it.hashCode()}" }, Level.INFO)
elems.add(Elem(
commonElem = CommonElem(
serviceType = 48,
businessType = 10,
elem = it.msgInfo!!.toByteArray()
)
))
}
}.onSuccess { uuid ->
elems.add(when (contact.chatType) {
MsgConstant.KCHATTYPEGROUP -> Elem(
customFace = CustomFace(
filePath = fileInfo.fileName,
fileId = uuid,
serverIp = 0u,
serverPort = 0u,
fileType = FileUtils.getPicType(file).toUInt(),
useful = 1u,
md5 = fileInfo.md5.hex2ByteArray(),
bizType = 0u,
imageType = FileUtils.getPicType(file).toUInt(),
width = picWidth.toUInt(),
height = picHeight.toUInt(),
size = fileInfo.fileSize.toUInt(),
origin = isOriginal,
thumbWidth = 0u,
thumbHeight = 0u,
pbReserve = CustomFace.Companion.PbReserve(
field1 = 0,
field3 = 0,
field4 = 0,
field10 = 0,
field21 = CustomFace.Companion.Object1(
field1 = 0,
field2 = "",
field3 = 0,
field4 = 0,
field5 = 0,
md5Str = fileInfo.md5
)
)
)
)
MsgConstant.KCHATTYPEC2C -> Elem(
notOnlineImage = NotOnlineImage(
filePath = fileInfo.fileName,
fileLen = fileInfo.fileSize.toUInt(),
downloadPath = fileInfo.uuid,
imgType = FileUtils.getPicType(file).toUInt(),
picMd5 = fileInfo.md5.hex2ByteArray(),
picHeight = picWidth.toUInt(),
picWidth = picHeight.toUInt(),
resId = fileInfo.uuid,
original = isOriginal, // true
pbReserve = NotOnlineImage.Companion.PbReserve(
field1 = 0,
field3 = 0,
field4 = 0,
field10 = 0,
field20 = NotOnlineImage.Companion.Object1(
field1 = 0,
field2 = "",
field3 = 0,
field4 = 0,
field5 = 0,
field7 = "",
),
md5Str = fileInfo.md5
)
)
)
else -> throw LogicException("Not supported chatType($contact) for PictureMsg")
})
}
summary.append("[图片]")
}
Element.ElementType.VOICE -> throw UnsupportedOperationException("Unsupported Element.ElementType.VOICE")
Element.ElementType.VIDEO -> throw UnsupportedOperationException("Unsupported Element.ElementType.VIDEO")
Element.ElementType.BASKETBALL -> throw UnsupportedOperationException("Unsupported Element.ElementType.BASKETBALL")
Element.ElementType.DICE -> {
val elem = Elem(
commonElem = CommonElem(
serviceType = 37,
elem = QFaceExtra(
packId = "1",
stickerId = "33",
faceId = 358,
field4 = 1,
field5 = 2,
result = "",
faceText = "/骰子",
field9 = 1
).toByteArray(),
businessType = 2
)
)
elems.add(elem)
summary .append( "[骰子]" )
}
Element.ElementType.RPS -> {
val elem = Elem(
commonElem = CommonElem(
serviceType = 37,
elem = QFaceExtra(
packId = "1",
stickerId = "34",
faceId = 359,
field4 = 1,
field5 = 2,
result = "",
faceText = "/包剪锤",
field9 = 1
).toByteArray(),
businessType = 1
)
)
elems.add(elem)
summary .append( "[包剪锤]" )
}
Element.ElementType.POKE -> {
val elem = Elem(
commonElem = CommonElem(
serviceType = 2,
elem = PokeExtra(
type = it.poke.type,
field7 = 0,
field8 = 0
).toByteArray(),
businessType = it.poke.id
)
)
elems.add(elem)
summary .append( "[戳一戳]" )
}
Element.ElementType.MUSIC -> throw UnsupportedOperationException("Unsupported Element.ElementType.MUSIC")
Element.ElementType.WEATHER -> {
var code = it.weather.code.toIntOrNull()
if (code == null) {
val city = it.weather.city
WeatherHelper.searchCity(city).onFailure {
LogCenter.log("无法获取城市天气: $city", Level.ERROR)
}.getOrNull()?.firstOrNull()?.let {
code = it.adcode
}
}
if (code != null) {
val weatherCard = WeatherHelper.fetchWeatherCard(code!!).getOrThrow()
val elem = Elem(
lightApp = LightAppElem(
data = byteArrayOf(1) + DeflateTools.compress(
weatherCard["weekStore"]
.asJsonObject["share"].asString.toByteArray()
)
)
)
elems.add(elem)
summary .append( "[天气卡片]" )
} else {
throw LogicException("无法获取城市天气")
}
}
Element.ElementType.LOCATION -> throw UnsupportedOperationException("Unsupported Element.ElementType.LOCATION")
Element.ElementType.SHARE -> throw UnsupportedOperationException("Unsupported Element.ElementType.SHARE")
Element.ElementType.GIFT -> throw UnsupportedOperationException("Unsupported Element.ElementType.GIFT")
Element.ElementType.MARKET_FACE -> throw UnsupportedOperationException("Unsupported Element.ElementType.MARKET_FACE")
Element.ElementType.FORWARD -> {
val resId = it.forward.resId
val filename = UUID.randomUUID().toString().uppercase()
var content = it.forward.summary
val descriptions = it.forward.description
var news = descriptions?.split("\n")?.map { "text" to it }
if (news == null || content == null) {
val forwardMsg = MessageHelper.getForwardMsg(resId).getOrThrow()
if (news == null) {
news = forwardMsg.map {
"text" to it.sender.nickName + ": " + descriptions
}
}
if (content == null) {
content = "查看${forwardMsg.size}条转发消息"
}
}
val json = mapOf(
"app" to "com.tencent.multimsg",
"config" to mapOf(
"autosize" to 1,
"forward" to 1,
"round" to 1,
"type" to "normal",
"width" to 300
),
"desc" to "[聊天记录]",
"extra" to mapOf(
"filename" to filename,
"tsum" to 2
).json.toString(),
"meta" to mapOf(
"detail" to mapOf(
"news" to news,
"resid" to resId,
"source" to "群聊的聊天记录",
"summary" to content,
"uniseq" to filename
)
),
"prompt" to "[聊天记录]",
"ver" to "0.0.0.5",
"view" to "contact"
)
val elem = Elem(
lightApp = LightAppElem(
data = byteArrayOf(1) + DeflateTools.compress(json.json.toString().toByteArray())
)
)
elems.add(elem)
summary.append( "[聊天记录]" )
}
Element.ElementType.CONTACT -> throw UnsupportedOperationException("Unsupported Element.ElementType.CONTACT")
Element.ElementType.JSON -> {
val elem = Elem(
lightApp = LightAppElem(
data = byteArrayOf(1) + DeflateTools.compress(it.json.json.toByteArray())
)
)
elems.add(elem)
summary .append( "[Json消息]" )
}
Element.ElementType.XML -> throw UnsupportedOperationException("Unsupported Element.ElementType.XML")
Element.ElementType.FILE -> throw UnsupportedOperationException("Unsupported Element.ElementType.FILE")
Element.ElementType.MARKDOWN -> {
val elem = Elem(
commonElem = CommonElem(
serviceType = 45,
elem = MarkdownExtra(it.markdown.markdown).toByteArray(),
businessType = 1
)
)
elems.add(elem)
summary.append("[Markdown消息]")
}
Element.ElementType.BUTTON -> {
val elem = Elem(
commonElem = CommonElem(
serviceType = 46,
elem = ButtonExtra(
field1 = Object1(
rows = it.button.rowsList.map { row ->
Row(buttons = row.buttonsList.map { button ->
val renderData = button.renderData
val action = button.action
val permission = action.permission
Button(
id = button.id,
renderData = RenderData(
label = renderData.label,
visitedLabel = renderData.visitedLabel,
style = renderData.style
),
action = Action(
type = action.type,
permission = Permission(
type = permission.type,
specifyRoleIds = permission.roleIdsList,
specifyUserIds = permission.userIdsList
),
unsupportTips = action.unsupportedTips,
data = action.data,
reply = action.reply,
enter = action.enter
)
)
})
},
appid = it.button.botAppid.toULong()
)
).toByteArray(),
businessType = 1
)
)
elems.add(elem)
summary.append("[Button消息]")
}
Element.ElementType.UNRECOGNIZED -> throw UnsupportedOperationException("Unsupported Element.ElementType.UNRECOGNIZED")
}
} catch (e: Throwable) {
LogCenter.log("转换消息失败(Multi): ${e.stackTraceToString()}", Level.ERROR)
}
}
return Result.success(summary.toString() to RichText(
elements = elems
))
}

View File

@ -0,0 +1,195 @@
package qq.service.ticket
import com.tencent.guild.api.transfile.IGuildTransFileApi
import com.tencent.mobileqq.app.QQAppInterface
import com.tencent.mobileqq.pskey.oidb.cmd0x102a.oidb_cmd0x102a
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernel.nativeinterface.BigDataTicket
import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.request.get
import io.ktor.client.request.header
import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.tools.slice
import mqq.app.MobileQQ
import mqq.manager.TicketManager
import oicq.wlogin_sdk.request.Ticket
import qq.service.QQInterfaces
import tencent.im.oidb.oidb_sso
internal object TicketHelper: QQInterfaces() {
object SigType {
const val WLOGIN_A5 = 2
const val WLOGIN_RESERVED = 16
const val WLOGIN_STWEB = 32 // TLV 103
const val WLOGIN_A2 = 64
const val WLOGIN_ST = 128
const val WLOGIN_AQSIG = 2097152
const val WLOGIN_D2 = 262144
const val WLOGIN_DA2 = 33554432
const val WLOGIN_LHSIG = 4194304
const val WLOGIN_LSKEY = 512
const val WLOGIN_OPENKEY = 16384
const val WLOGIN_PAYTOKEN = 8388608
const val WLOGIN_PF = 16777216
const val WLOGIN_PSKEY = 1048576
const val WLOGIN_PT4Token = 134217728
const val WLOGIN_QRPUSH = 67108864
const val WLOGIN_SID = 524288
const val WLOGIN_SIG64 = 8192
const val WLOGIN_SKEY = 4096
const val WLOGIN_TOKEN = 32768
const val WLOGIN_VKEY = 131072
val ALL_TICKET = arrayOf(
WLOGIN_A5, WLOGIN_RESERVED, WLOGIN_STWEB, WLOGIN_A2, WLOGIN_ST, WLOGIN_AQSIG, WLOGIN_D2, WLOGIN_DA2,
WLOGIN_LHSIG, WLOGIN_LSKEY, WLOGIN_OPENKEY, WLOGIN_PAYTOKEN, WLOGIN_PF, WLOGIN_PSKEY, WLOGIN_PT4Token,
WLOGIN_QRPUSH, WLOGIN_SID, WLOGIN_SIG64, WLOGIN_SKEY, WLOGIN_TOKEN, WLOGIN_VKEY
)
}
inline fun getUin(): String {
return app.currentUin.ifBlank { "0" }
}
fun getUid(): String {
return app.currentUid.ifBlank { "u_" }
}
inline fun getNickname(): String {
return app.currentNickname
}
fun getCookie(): String {
val uin = getUin()
val skey = getRealSkey(uin)
val pskey = getPSKey(uin)
return "uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey"
}
suspend fun getCookie(domain: String): String {
val uin = getUin()
val skey = getRealSkey(uin)
val pskey = getPSKey(uin, domain) ?: ""
val pt4token = getPt4Token(uin, domain) ?: ""
return "uin=o$uin; skey=$skey; p_uin=o$uin; p_skey=$pskey; pt4_token=$pt4token"
}
fun getBigdataTicket(): BigDataTicket? {
return runCatching {
QRoute.api(IGuildTransFileApi::class.java).bigDataTicket?.let {
BigDataTicket(it.getSessionKey(), it.getSessionSig())
}
}.getOrNull()
}
fun getCSRF(pskey: String = getPSKey(getUin())): String {
if (pskey.isEmpty()) {
return "0"
}
var v = 5381
for (element in pskey) {
v += ((v shl 5) + element.code.toLong()).toInt()
}
return (v and Int.MAX_VALUE).toString()
}
suspend fun getCSRF(uin: String, domain: String): String {
// 是不是要用Skey
return getBkn(getPSKey(uin, domain) ?: getSKey(uin))
}
fun getBkn(arg: String): String {
var v: Long = 5381
for (element in arg) {
v += (v shl 5 and 2147483647L) + element.code.toLong()
}
return (v and 2147483647L).toString()
}
fun getTicket(uin: String, id: Int): Ticket? {
require(app is QQAppInterface)
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getLocalTicket(uin, id)
}
fun getStWeb(uin: String): String {
require(app is QQAppInterface)
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getStweb(uin)
}
fun getSKey(uin: String): String {
require(app is QQAppInterface)
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getRealSkey(uin)
}
fun getRealSkey(uin: String): String {
require(app is QQAppInterface)
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getRealSkey(uin)
}
fun getPSKey(uin: String): String {
require(app is QQAppInterface)
val manager = (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager)
manager.reloadCache(MobileQQ.getContext())
return manager.getSuperkey(uin) ?: ""
}
suspend fun getLessPSKey(vararg domain: String): Result<List<oidb_cmd0x102a.PSKey>> {
val req = oidb_cmd0x102a.GetPSkeyRequest()
req.domains.set(domain.toList())
val fromServiceMsg = sendOidbAW("OidbSvcTcp.0x102a", 4138, 0, req.toByteArray())
?: return Result.failure(Exception("getLessPSKey failed"))
if (fromServiceMsg.wupBuffer == null) return Result.failure(Exception("getLessPSKey failed: no response"))
val body = oidb_sso.OIDBSSOPkg()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val rsp = oidb_cmd0x102a.GetPSkeyResponse().mergeFrom(body.bytes_bodybuffer.get().toByteArray())
return Result.success(rsp.private_keys.get())
}
suspend fun getPSKey(uin: String, domain: String): String? {
require(app is QQAppInterface)
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getPskey(uin, domain).let {
if (it.isNullOrBlank())
getLessPSKey(domain).getOrNull()?.firstOrNull()?.key?.get()
else it
}
}
fun getPt4Token(uin: String, domain: String): String? {
require(app is QQAppInterface)
return (app.getManager(QQAppInterface.TICKET_MANAGER) as TicketManager).getPt4Token(uin, domain)
}
suspend fun getHttpCookies(appid: String, daid: String, jumpurl: String): String? {
val client = HttpClient {
followRedirects = false
install(HttpTimeout) {
requestTimeoutMillis = 15000
connectTimeoutMillis = 15000
socketTimeoutMillis = 15000
}
}
val uin = getUin()
val clientkey = getStWeb(uin)
var url = "https://ui.ptlogin2.qq.com/cgi-bin/login?pt_hide_ad=1&style=9&appid=$appid&pt_no_auth=1&pt_wxtest=1&daid=$daid&s_url=$jumpurl"
var cookie = client.get(url).headers.getAll("Set-Cookie")?.joinToString(";")
url = "https://ssl.ptlogin2.qq.com/jump?u1=$jumpurl&pt_report=1&daid=$daid&style=9&keyindex=19&clientuin=$uin&clientkey=$clientkey"
client.get(url) {
header("Cookie", cookie)
}.let {
cookie = it.headers.getAll("Set-Cookie")?.joinToString(";")
url = it.headers["Location"].toString()
}
cookie = client.get(url).headers.getAll("Set-Cookie")?.joinToString(";")
val extractedCookie = StringBuilder()
val cookies = cookie?.split(";")
cookies?.filter { cookie ->
val cookiePair = cookie.trim().split("=")
cookiePair.size == 2 && cookiePair[1].isNotBlank() && cookiePair[0].trim() in listOf("uin", "skey", "p_uin", "p_skey", "pt4_token")
}?.forEach {
extractedCookie.append("$it; ")
}
return extractedCookie.toString().trim()
}
}