63 Commits

Author SHA1 Message Date
40d2911135 修复资源rkey获取异常 2024-07-16 17:47:39 +08:00
823d9911a0 fix #338 2024-07-16 17:11:48 +08:00
4adbc12a0b fix /get_group_root_files 2024-07-08 09:53:15 +08:00
114fbfdd23 fix /get_group_root_files 2024-07-08 09:44:26 +08:00
0a563d60a1 fix crash in QQ9.0.71 2024-07-08 09:17:56 +08:00
5fb1d0aeb9 fix ping-pong 2024-07-08 09:11:49 +08:00
012ecaa85d fix #335 2024-07-08 08:55:49 +08:00
7ea127a279 update shamrock 2024-07-08 08:15:05 +08:00
02f83a3c48 fix: remove debug log 2024-07-07 15:26:30 +08:00
4016c05882 fix: 9.0.71 compatible issues (part of) 2024-07-07 15:04:30 +08:00
59d762eecf fix https://github.com/KarinJS/kritor/issues/10 2024-04-11 12:37:54 +08:00
e891bc8512 fix build 2024-04-11 01:17:13 +08:00
494c70f2f8 update kritor 2024-04-11 00:58:36 +08:00
7baf459b2a Shamrock: fix scene and group code 2024-04-10 21:09:10 +08:00
36a09ca088 update kritor 2024-04-08 20:00:44 +08:00
926c4659f6 Shamrock: add kritor metadata 2024-04-07 16:51:21 +08:00
cb7c68f36c fix: build error 2024-04-07 16:27:35 +08:00
72af39208c update kritor 2024-04-07 16:08:33 +08:00
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
119 changed files with 10021 additions and 1751 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,37 @@
## 简介
☘ 基于 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/)
>
> 社区地址:[discord](https://discord.gg/MKR2wz863h)
## 兼容|迁移|替代 说明
- 一键移植:本项目基于 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

@ -17,7 +17,7 @@ android {
minSdk = 27
targetSdk = 34
versionCode = getVersionCode()
versionName = "1.1.0" + ".r${getGitCommitCount()}." + getVersionName()
versionName = "1.1.1" + ".r${getGitCommitCount()}." + getVersionName()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

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}]。")
@ -164,7 +162,7 @@ private fun FunctionCard(
Function(
title = "主动RPC",
desc = "Kritor协议实现RPC",
desc = "Kritor协议实现RPC由Shamrock放出rpc服务",
isSwitch = ShamrockConfig[ctx, ActiveRPC]
) {
ShamrockConfig[ctx, ActiveRPC] = it
@ -173,7 +171,7 @@ private fun FunctionCard(
Function(
title = "被动RPC",
desc = "Kritor协议实现RPC",
desc = "Kritor协议实现RPC由客户端提供反向的rpc服务",
isSwitch = ShamrockConfig[ctx, PassiveRPC]
) {
ShamrockConfig[ctx, PassiveRPC] = it

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 3dec747a8e

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

@ -8,7 +8,9 @@ import moe.fuqiuluo.symbols.Protobuf
data class TrpcOidb(
@ProtoNumber(1) val cmd: Int = Int.MIN_VALUE,
@ProtoNumber(2) val service: Int = Int.MIN_VALUE,
@ProtoNumber(4) val buffer: ByteArray,
@ProtoNumber(3) val result: UInt? = null,
@ProtoNumber(4) val buffer: ByteArray? = null,
@ProtoNumber(5) val msg: String? = null,
//@ProtoNumber(11) val traceParams: Map<String, String> = mapOf(),
@ProtoNumber(12) val flag: Int = Int.MIN_VALUE,
): Protobuf<TrpcOidb>

View File

@ -94,7 +94,8 @@ data class DeleteReq(
@Serializable
data class DownloadRkeyReq(
@ProtoNumber(1) val types: List<Int>
@ProtoNumber(1) val types: List<Int>,
@ProtoNumber(2) val downloadType: Int
)
@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?,
@ -52,11 +52,11 @@ data class DownloadRkeyRsp(
@Serializable
data class RKeyInfo(
@ProtoNumber(1) val rkey: String?,
@ProtoNumber(1) val rkey: String,
@ProtoNumber(2) val rkeyTtlSec: ULong?,
@ProtoNumber(3) val storeId: UInt = 0u,
@ProtoNumber(4) val rkeyCreateTime: UInt?,
@ProtoNumber(4) val type: UInt?,
@ProtoNumber(4) val type: UInt,
)
@Serializable

View File

@ -1,7 +1,9 @@
package com.tencent.qphone.base.util;
public abstract class CodecWarpper {
public abstract void onResponse(int i2, Object obj, int i3);
// public abstract void onResponse(int i2, Object obj, int i3);
public abstract void onInvalidData(int i2, int i3, String str);
public abstract void onResponse(int i2, Object obj, int i3, byte[] bArr);

View File

@ -1,7 +1,7 @@
package com.tencent.qqnt.aio.api;
import com.tencent.mobileqq.qroute.QRouteApi;
import com.tencent.qqnt.kernel.nativeinterface.Contact;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback;
public interface IAIOFileTransfer extends QRouteApi {

View File

@ -0,0 +1,4 @@
package com.tencent.qqnt.kernel.api.impl;
public class GroupService {
}

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

@ -1,5 +1,7 @@
package com.tencent.qqnt.kernel.nativeinterface;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
import java.util.ArrayList;

View File

@ -0,0 +1,4 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class GProGuildTopFeedMsg {
}

View File

@ -1,5 +1,7 @@
package com.tencent.qqnt.kernel.nativeinterface;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
import java.util.ArrayList;

View File

@ -0,0 +1,36 @@
package com.tencent.qqnt.kernel.nativeinterface;
import java.util.ArrayList;
public class GroupMemberCommonListResult {
public long groupCode;
public int identifyFlag;
public long startUin;
public ArrayList<MemberCommonInfo> memberList = new ArrayList<>();
public String strErrorInfo = "";
public long getGroupCode() {
return this.groupCode;
}
public int getIdentifyFlag() {
return this.identifyFlag;
}
public ArrayList<MemberCommonInfo> getMemberList() {
return this.memberList;
}
public long getStartUin() {
return this.startUin;
}
public String getStrErrorInfo() {
return this.strErrorInfo;
}
public String toString() {
return "GroupMemberCommonListResult{groupCode=" + this.groupCode + ",startUin=" + this.startUin + ",identifyFlag=" + this.identifyFlag + ",memberList=" + this.memberList + ",strErrorInfo=" + this.strErrorInfo + ",}";
}
}

View File

@ -0,0 +1,101 @@
package com.tencent.qqnt.kernel.nativeinterface;
import java.util.ArrayList;
public class GroupMemberCommonReq {
public long groupCode;
public int sourceType;
public String startUin = "";
public String identifyFlag = "";
public ArrayList<Long> uinList = new ArrayList<>();
public MemberCommonInfoFilter memberCommonFilter = new MemberCommonInfoFilter();
public String memberNum = "";
public String filterMethod = "";
public String onlineFlag = "";
public String realSpecialTitleFlag = "";
public String getFilterMethod() {
return this.filterMethod;
}
public long getGroupCode() {
return this.groupCode;
}
public String getIdentifyFlag() {
return this.identifyFlag;
}
public MemberCommonInfoFilter getMemberCommonFilter() {
return this.memberCommonFilter;
}
public String getMemberNum() {
return this.memberNum;
}
public String getOnlineFlag() {
return this.onlineFlag;
}
public String getRealSpecialTitleFlag() {
return this.realSpecialTitleFlag;
}
public int getSourceType() {
return this.sourceType;
}
public String getStartUin() {
return this.startUin;
}
public ArrayList<Long> getUinList() {
return this.uinList;
}
public void setFilterMethod(String str) {
this.filterMethod = str;
}
public void setGroupCode(long j2) {
this.groupCode = j2;
}
public void setIdentifyFlag(String str) {
this.identifyFlag = str;
}
public void setMemberCommonFilter(MemberCommonInfoFilter memberCommonInfoFilter) {
this.memberCommonFilter = memberCommonInfoFilter;
}
public void setMemberNum(String str) {
this.memberNum = str;
}
public void setOnlineFlag(String str) {
this.onlineFlag = str;
}
public void setRealSpecialTitleFlag(String str) {
this.realSpecialTitleFlag = str;
}
public void setSourceType(int i2) {
this.sourceType = i2;
}
public void setStartUin(String str) {
this.startUin = str;
}
public void setUinList(ArrayList<Long> arrayList) {
this.uinList = arrayList;
}
public String toString() {
return "GroupMemberCommonReq{groupCode=" + this.groupCode + ",startUin=" + this.startUin + ",identifyFlag=" + this.identifyFlag + ",uinList=" + this.uinList + ",memberCommonFilter=" + this.memberCommonFilter + ",memberNum=" + this.memberNum + ",filterMethod=" + this.filterMethod + ",onlineFlag=" + this.onlineFlag + ",realSpecialTitleFlag=" + this.realSpecialTitleFlag + ",sourceType=" + this.sourceType + ",}";
}
}

View File

@ -0,0 +1,81 @@
package com.tencent.qqnt.kernel.nativeinterface;
import java.util.ArrayList;
public class GroupMemberExtListResult {
public long dataTime;
public long endUin;
public long groupCode;
public int levelNameSeq;
public int memberInfoSeq;
public int sysShowFlag;
public int timeToUpdate;
public int userShowFlag;
public int userShowFlagNew;
public ArrayList<MemberExtInfo> memberLevelInfo = new ArrayList<>();
public ArrayList<MemberLevelName> msgLevelName = new ArrayList<>();
public String strOwnerName = "";
public String strAdminName = "";
public ArrayList<MemberLevelName> msgLevelNameNew = new ArrayList<>();
public long getDataTime() {
return this.dataTime;
}
public long getEndUin() {
return this.endUin;
}
public long getGroupCode() {
return this.groupCode;
}
public int getLevelNameSeq() {
return this.levelNameSeq;
}
public int getMemberInfoSeq() {
return this.memberInfoSeq;
}
public ArrayList<MemberExtInfo> getMemberLevelInfo() {
return this.memberLevelInfo;
}
public ArrayList<MemberLevelName> getMsgLevelName() {
return this.msgLevelName;
}
public ArrayList<MemberLevelName> getMsgLevelNameNew() {
return this.msgLevelNameNew;
}
public String getStrAdminName() {
return this.strAdminName;
}
public String getStrOwnerName() {
return this.strOwnerName;
}
public int getSysShowFlag() {
return this.sysShowFlag;
}
public int getTimeToUpdate() {
return this.timeToUpdate;
}
public int getUserShowFlag() {
return this.userShowFlag;
}
public int getUserShowFlagNew() {
return this.userShowFlagNew;
}
public String toString() {
return "GroupMemberExtListResult{groupCode=" + this.groupCode + ",memberLevelInfo=" + this.memberLevelInfo + ",msgLevelName=" + this.msgLevelName + ",endUin=" + this.endUin + ",dataTime=" + this.dataTime + ",userShowFlag=" + this.userShowFlag + ",sysShowFlag=" + this.sysShowFlag + ",timeToUpdate=" + this.timeToUpdate + ",strOwnerName=" + this.strOwnerName + ",strAdminName=" + this.strAdminName + ",levelNameSeq=" + this.levelNameSeq + ",userShowFlagNew=" + this.userShowFlagNew + ",msgLevelNameNew=" + this.msgLevelNameNew + ",memberInfoSeq=" + this.memberInfoSeq + ",}";
}
}

View File

@ -0,0 +1,101 @@
package com.tencent.qqnt.kernel.nativeinterface;
import java.util.ArrayList;
public class GroupMemberExtReq {
public long groupCode;
public int sourceType;
public String beginUin = "";
public String dataTime = "";
public ArrayList<Long> uinList = new ArrayList<>();
public MemberExtInfoFilter memberExtFilter = new MemberExtInfoFilter();
public String seq = "";
public String uinNum = "";
public String groupType = "";
public String richCardNameVer = "";
public String getBeginUin() {
return this.beginUin;
}
public String getDataTime() {
return this.dataTime;
}
public long getGroupCode() {
return this.groupCode;
}
public String getGroupType() {
return this.groupType;
}
public MemberExtInfoFilter getMemberExtFilter() {
return this.memberExtFilter;
}
public String getRichCardNameVer() {
return this.richCardNameVer;
}
public String getSeq() {
return this.seq;
}
public int getSourceType() {
return this.sourceType;
}
public ArrayList<Long> getUinList() {
return this.uinList;
}
public String getUinNum() {
return this.uinNum;
}
public void setBeginUin(String str) {
this.beginUin = str;
}
public void setDataTime(String str) {
this.dataTime = str;
}
public void setGroupCode(long j2) {
this.groupCode = j2;
}
public void setGroupType(String str) {
this.groupType = str;
}
public void setMemberExtFilter(MemberExtInfoFilter memberExtInfoFilter) {
this.memberExtFilter = memberExtInfoFilter;
}
public void setRichCardNameVer(String str) {
this.richCardNameVer = str;
}
public void setSeq(String str) {
this.seq = str;
}
public void setSourceType(int i2) {
this.sourceType = i2;
}
public void setUinList(ArrayList<Long> arrayList) {
this.uinList = arrayList;
}
public void setUinNum(String str) {
this.uinNum = str;
}
public String toString() {
return "GroupMemberExtReq{groupCode=" + this.groupCode + ",beginUin=" + this.beginUin + ",dataTime=" + this.dataTime + ",uinList=" + this.uinList + ",memberExtFilter=" + this.memberExtFilter + ",seq=" + this.seq + ",uinNum=" + this.uinNum + ",groupType=" + this.groupType + ",richCardNameVer=" + this.richCardNameVer + ",sourceType=" + this.sourceType + ",}";
}
}

View File

@ -0,0 +1,32 @@
package com.tencent.qqnt.kernel.nativeinterface;
import java.util.ArrayList;
import java.util.HashMap;
public class GroupMemberListResult {
public boolean finish;
public boolean hasRobot;
public ArrayList<GroupMemberInfoListId> ids = new ArrayList<>();
public HashMap<String, MemberInfo> infos = new HashMap<>();
public boolean getFinish() {
return this.finish;
}
public boolean getHasRobot() {
return this.hasRobot;
}
public ArrayList<GroupMemberInfoListId> getIds() {
return this.ids;
}
public HashMap<String, MemberInfo> getInfos() {
return this.infos;
}
public String toString() {
return "GroupMemberListResult{ids=" + this.ids + ",infos=" + this.infos + ",finish=" + this.finish + ",hasRobot=" + this.hasRobot + ",}";
}
}

View File

@ -0,0 +1,6 @@
package com.tencent.qqnt.kernel.nativeinterface;
public interface IGroupMemberCommonCallback {
void onResult(int i2, String str, GroupMemberCommonListResult groupMemberCommonListResult);
}

View File

@ -0,0 +1,5 @@
package com.tencent.qqnt.kernel.nativeinterface;
public interface IGroupMemberExtCallback {
void onResult(int i2, String str, GroupMemberExtListResult groupMemberExtListResult);
}

View File

@ -0,0 +1,6 @@
package com.tencent.qqnt.kernel.nativeinterface;
public interface IGroupMemberListCallback {
void onResult(int result, String str, GroupMemberListResult groupMemberListResult);
}

View File

@ -1,7 +1,25 @@
package com.tencent.qqnt.kernel.nativeinterface;
import java.util.ArrayList;
public interface IKernelGroupService {
void getTransferableMemberInfo(long uin, IGetTransferableMemberCallback cb);
long addKernelGroupListener(IKernelGroupListener ln);
void getAllMemberList(long groupCode, boolean refresh, IGroupMemberListCallback iGroupMemberListCallback);
void getMemberCommonInfo(GroupMemberCommonReq groupMemberCommonReq, IGroupMemberCommonCallback iGroupMemberCommonCallback);
void getMemberExtInfo(GroupMemberExtReq groupMemberExtReq, IGroupMemberExtCallback iGroupMemberExtCallback);
void getMemberInfo(long j2, ArrayList<String> arrayList, boolean z, IOperateCallback iOperateCallback);
void getMemberInfoForMqq(long groupCode, ArrayList<String> uids, boolean z, IGroupMemberListCallback iGroupMemberListCallback);
void getNextMemberList(String sceneId, GroupMemberInfoListId groupMemberInfoListId, int i2, IGroupMemberListCallback iGroupMemberListCallback);
void getPrevMemberList(String sceneId, GroupMemberInfoListId groupMemberInfoListId, int i2, IGroupMemberListCallback iGroupMemberListCallback);
}

View File

@ -1,5 +1,7 @@
package com.tencent.qqnt.kernel.nativeinterface;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
import java.util.ArrayList;
import java.util.HashMap;
@ -16,7 +18,7 @@ public interface IKernelMsgListener {
void onCustomWithdrawConfigUpdate(CustomWithdrawConfig customWithdrawConfig);
void onDraftUpdate(Contact contact, ArrayList<MsgElement> arrayList, long j2);
void onDraftUpdate(Contact contact, ArrayList<MsgElement> arrayList, long j);
void onEmojiDownloadComplete(EmojiNotifyInfo emojiNotifyInfo);
@ -30,7 +32,7 @@ public interface IKernelMsgListener {
void onFirstViewGroupGuildMapping(ArrayList<FirstViewGroupGuildInfo> arrayList);
void onGrabPasswordRedBag(int i2, String str, int i3, RecvdOrder recvdOrder, MsgRecord msgRecord);
void onGrabPasswordRedBag(int i, String str, int i2, RecvdOrder recvdOrder, MsgRecord msgRecord);
void onGroupFileInfoAdd(GroupItem groupItem);
@ -48,6 +50,8 @@ public interface IKernelMsgListener {
void onGuildNotificationAbstractUpdate(GuildNotificationAbstractInfo guildNotificationAbstractInfo);
void onGuildTopFeedUpdate(GProGuildTopFeedMsg gProGuildTopFeedMsg);
void onHitCsRelatedEmojiResult(DownloadRelateEmojiResultInfo downloadRelateEmojiResultInfo);
void onHitEmojiKeywordResult(HitRelatedEmojiWordsResult hitRelatedEmojiWordsResult);
@ -62,7 +66,7 @@ public interface IKernelMsgListener {
void onLineDev(ArrayList<DevInfo> arrayList);
void onLogLevelChanged(long j2);
void onLogLevelChanged(long j);
void onMsgAbstractUpdate(ArrayList<MsgAbstract> arrayList);
@ -76,14 +80,16 @@ public interface IKernelMsgListener {
void onMsgInfoListUpdate(ArrayList<MsgRecord> arrayList);
void onMsgQRCodeStatusChanged(int i2);
void onMsgQRCodeStatusChanged(int i);
void onMsgRecall(int i2, String str, long j2);
void onMsgRecall(int i, String str, long j);
void onMsgSecurityNotify(MsgRecord msgRecord);
void onMsgSettingUpdate(MsgSetting msgSetting);
void onMsgWithRichLinkInfoUpdate(ArrayList<MsgRecord> arrayList);
void onNtFirstViewMsgSyncEnd();
void onNtMsgSyncEnd();
@ -92,11 +98,11 @@ public interface IKernelMsgListener {
void onReadFeedEventUpdate(FirstViewDirectMsgNotifyInfo firstViewDirectMsgNotifyInfo);
void onRecvGroupGuildFlag(int i2);
void onRecvGroupGuildFlag(int i);
void onRecvMsg(ArrayList<MsgRecord> arrayList);
void onRecvMsgSvrRspTransInfo(long j2, Contact contact, int i2, int i3, String str, byte[] bArr);
void onRecvMsgSvrRspTransInfo(long j, Contact contact, int i, int i2, String str, byte[] bArr);
void onRecvOnlineFileMsg(ArrayList<MsgRecord> arrayList);
@ -104,7 +110,9 @@ public interface IKernelMsgListener {
void onRecvSysMsg(ArrayList<Byte> arrayList);
void onRecvUDCFlag(int i2);
void onRecvUDCFlag(int i);
void onRedTouchChanged();
void onRichMediaDownloadComplete(FileTransNotifyInfo fileTransNotifyInfo);
@ -114,9 +122,9 @@ public interface IKernelMsgListener {
void onSearchGroupFileInfoUpdate(SearchGroupFileResult searchGroupFileResult);
void onSendMsgError(long j2, Contact contact, int i2, String str);
void onSendMsgError(long j, Contact contact, int i, String str);
void onSysMsgNotification(int i2, long j2, long j3, ArrayList<Byte> arrayList);
void onSysMsgNotification(int i, long j, long j2, boolean z, ArrayList<Byte> arrayList);
void onTempChatInfoUpdate(TempChatInfo tempChatInfo);
@ -128,9 +136,11 @@ public interface IKernelMsgListener {
void onUserOnlineStatusChanged(boolean z);
void onUserSecQualityChanged(QueryUserSecQualityRsp queryUserSecQualityRsp);
void onUserTabStatusChanged(ArrayList<TabStatusInfo> arrayList);
void onlineStatusBigIconDownloadPush(int i2, long j2, String str);
void onlineStatusBigIconDownloadPush(int i, long j, String str);
void onlineStatusSmallIconDownloadPush(int i2, long j2, String str);
void onlineStatusSmallIconDownloadPush(int i, long j, String str);
}

View File

@ -1,5 +1,7 @@
package com.tencent.qqnt.kernel.nativeinterface;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;

View File

@ -1,5 +1,7 @@
package com.tencent.qqnt.kernel.nativeinterface;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
import java.util.ArrayList;
public interface IKernelRichMediaService {

View File

@ -0,0 +1,63 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class MemberCommonInfo {
public long memberUin;
public int privilege;
public int shutUpTime;
public int uinFlag;
public int uinFlagExt;
public int uinMobileFlag;
public long getMemberUin() {
return this.memberUin;
}
public int getPrivilege() {
return this.privilege;
}
public int getShutUpTime() {
return this.shutUpTime;
}
public int getUinFlag() {
return this.uinFlag;
}
public int getUinFlagExt() {
return this.uinFlagExt;
}
public int getUinMobileFlag() {
return this.uinMobileFlag;
}
public void setMemberUin(long j2) {
this.memberUin = j2;
}
public void setPrivilege(int i2) {
this.privilege = i2;
}
public void setShutUpTime(int i2) {
this.shutUpTime = i2;
}
public void setUinFlag(int i2) {
this.uinFlag = i2;
}
public void setUinFlagExt(int i2) {
this.uinFlagExt = i2;
}
public void setUinMobileFlag(int i2) {
this.uinMobileFlag = i2;
}
public String toString() {
return "MemberCommonInfo{memberUin=" + this.memberUin + ",uinFlag=" + this.uinFlag + ",uinFlagExt=" + this.uinFlagExt + ",uinMobileFlag=" + this.uinMobileFlag + ",shutUpTime=" + this.shutUpTime + ",privilege=" + this.privilege + ",}";
}
}

View File

@ -0,0 +1,63 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class MemberCommonInfoFilter {
public int memberUin;
public int privilege;
public int shutUpTime;
public int uinFlag;
public int uinFlagExt;
public int uinMobileFlag;
public int getMemberUin() {
return this.memberUin;
}
public int getPrivilege() {
return this.privilege;
}
public int getShutUpTime() {
return this.shutUpTime;
}
public int getUinFlag() {
return this.uinFlag;
}
public int getUinFlagExt() {
return this.uinFlagExt;
}
public int getUinMobileFlag() {
return this.uinMobileFlag;
}
public void setMemberUin(int i2) {
this.memberUin = i2;
}
public void setPrivilege(int i2) {
this.privilege = i2;
}
public void setShutUpTime(int i2) {
this.shutUpTime = i2;
}
public void setUinFlag(int i2) {
this.uinFlag = i2;
}
public void setUinFlagExt(int i2) {
this.uinFlagExt = i2;
}
public void setUinMobileFlag(int i2) {
this.uinMobileFlag = i2;
}
public String toString() {
return "MemberCommonInfoFilter{memberUin=" + this.memberUin + ",uinFlag=" + this.uinFlag + ",uinFlagExt=" + this.uinFlagExt + ",uinMobileFlag=" + this.uinMobileFlag + ",shutUpTime=" + this.shutUpTime + ",privilege=" + this.privilege + ",}";
}
}

View File

@ -0,0 +1,110 @@
package com.tencent.qqnt.kernel.nativeinterface;
import java.util.ArrayList;
public class MemberExtInfo {
public int activeDay;
public int cmdUinFlagExt3Grocery;
public int level;
public int point;
public long specialTitleExpireTime;
public long uin;
public String strName = "";
public String nickName = "";
public String specialTitle = "";
public byte[] msgNeedField = new byte[0];
public ArrayList<MemberIcon> memberIconList = new ArrayList<>();
public int getActiveDay() {
return this.activeDay;
}
public int getCmdUinFlagExt3Grocery() {
return this.cmdUinFlagExt3Grocery;
}
public int getLevel() {
return this.level;
}
public ArrayList<MemberIcon> getMemberIconList() {
return this.memberIconList;
}
public byte[] getMsgNeedField() {
return this.msgNeedField;
}
public String getNickName() {
return this.nickName;
}
public int getPoint() {
return this.point;
}
public String getSpecialTitle() {
return this.specialTitle;
}
public long getSpecialTitleExpireTime() {
return this.specialTitleExpireTime;
}
public String getStrName() {
return this.strName;
}
public long getUin() {
return this.uin;
}
public void setActiveDay(int i2) {
this.activeDay = i2;
}
public void setCmdUinFlagExt3Grocery(int i2) {
this.cmdUinFlagExt3Grocery = i2;
}
public void setLevel(int i2) {
this.level = i2;
}
public void setMemberIconList(ArrayList<MemberIcon> arrayList) {
this.memberIconList = arrayList;
}
public void setMsgNeedField(byte[] bArr) {
this.msgNeedField = bArr;
}
public void setNickName(String str) {
this.nickName = str;
}
public void setPoint(int i2) {
this.point = i2;
}
public void setSpecialTitle(String str) {
this.specialTitle = str;
}
public void setSpecialTitleExpireTime(long j2) {
this.specialTitleExpireTime = j2;
}
public void setStrName(String str) {
this.strName = str;
}
public void setUin(long j2) {
this.uin = j2;
}
public String toString() {
return "MemberExtInfo{uin=" + this.uin + ",point=" + this.point + ",activeDay=" + this.activeDay + ",level=" + this.level + ",strName=" + this.strName + ",nickName=" + this.nickName + ",specialTitle=" + this.specialTitle + ",specialTitleExpireTime=" + this.specialTitleExpireTime + ",msgNeedField=" + this.msgNeedField + ",cmdUinFlagExt3Grocery=" + this.cmdUinFlagExt3Grocery + ",memberIconList=" + this.memberIconList + ",}";
}
}

View File

@ -0,0 +1,171 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class MemberExtInfoFilter {
public int cmdUinFlagExt3Grocery;
public int dataTime;
public int levelName;
public int levelNameNew;
public int memberIcon;
public int memberInfoSeq;
public int memberLevelInfoActiveDay;
public int memberLevelInfoLevel;
public int memberLevelInfoName;
public int memberLevelInfoPoint;
public int memberLevelInfoUin;
public int msgNeedField;
public int nickName;
public int specialTitle;
public int sysShowFlag;
public int timeToUpdate;
public int userShowFlag;
public int userShowFlagNew;
public int getCmdUinFlagExt3Grocery() {
return this.cmdUinFlagExt3Grocery;
}
public int getDataTime() {
return this.dataTime;
}
public int getLevelName() {
return this.levelName;
}
public int getLevelNameNew() {
return this.levelNameNew;
}
public int getMemberIcon() {
return this.memberIcon;
}
public int getMemberInfoSeq() {
return this.memberInfoSeq;
}
public int getMemberLevelInfoActiveDay() {
return this.memberLevelInfoActiveDay;
}
public int getMemberLevelInfoLevel() {
return this.memberLevelInfoLevel;
}
public int getMemberLevelInfoName() {
return this.memberLevelInfoName;
}
public int getMemberLevelInfoPoint() {
return this.memberLevelInfoPoint;
}
public int getMemberLevelInfoUin() {
return this.memberLevelInfoUin;
}
public int getMsgNeedField() {
return this.msgNeedField;
}
public int getNickName() {
return this.nickName;
}
public int getSpecialTitle() {
return this.specialTitle;
}
public int getSysShowFlag() {
return this.sysShowFlag;
}
public int getTimeToUpdate() {
return this.timeToUpdate;
}
public int getUserShowFlag() {
return this.userShowFlag;
}
public int getUserShowFlagNew() {
return this.userShowFlagNew;
}
public void setCmdUinFlagExt3Grocery(int i2) {
this.cmdUinFlagExt3Grocery = i2;
}
public void setDataTime(int i2) {
this.dataTime = i2;
}
public void setLevelName(int i2) {
this.levelName = i2;
}
public void setLevelNameNew(int i2) {
this.levelNameNew = i2;
}
public void setMemberIcon(int i2) {
this.memberIcon = i2;
}
public void setMemberInfoSeq(int i2) {
this.memberInfoSeq = i2;
}
public void setMemberLevelInfoActiveDay(int i2) {
this.memberLevelInfoActiveDay = i2;
}
public void setMemberLevelInfoLevel(int i2) {
this.memberLevelInfoLevel = i2;
}
public void setMemberLevelInfoName(int i2) {
this.memberLevelInfoName = i2;
}
public void setMemberLevelInfoPoint(int i2) {
this.memberLevelInfoPoint = i2;
}
public void setMemberLevelInfoUin(int i2) {
this.memberLevelInfoUin = i2;
}
public void setMsgNeedField(int i2) {
this.msgNeedField = i2;
}
public void setNickName(int i2) {
this.nickName = i2;
}
public void setSpecialTitle(int i2) {
this.specialTitle = i2;
}
public void setSysShowFlag(int i2) {
this.sysShowFlag = i2;
}
public void setTimeToUpdate(int i2) {
this.timeToUpdate = i2;
}
public void setUserShowFlag(int i2) {
this.userShowFlag = i2;
}
public void setUserShowFlagNew(int i2) {
this.userShowFlagNew = i2;
}
public String toString() {
return "MemberExtInfoFilter{memberLevelInfoUin=" + this.memberLevelInfoUin + ",memberLevelInfoPoint=" + this.memberLevelInfoPoint + ",memberLevelInfoActiveDay=" + this.memberLevelInfoActiveDay + ",memberLevelInfoLevel=" + this.memberLevelInfoLevel + ",memberLevelInfoName=" + this.memberLevelInfoName + ",levelName=" + this.levelName + ",dataTime=" + this.dataTime + ",userShowFlag=" + this.userShowFlag + ",sysShowFlag=" + this.sysShowFlag + ",timeToUpdate=" + this.timeToUpdate + ",nickName=" + this.nickName + ",specialTitle=" + this.specialTitle + ",levelNameNew=" + this.levelNameNew + ",userShowFlagNew=" + this.userShowFlagNew + ",msgNeedField=" + this.msgNeedField + ",cmdUinFlagExt3Grocery=" + this.cmdUinFlagExt3Grocery + ",memberIcon=" + this.memberIcon + ",memberInfoSeq=" + this.memberInfoSeq + ",}";
}
}

View File

@ -0,0 +1,29 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class MemberIcon {
public int bizId;
public byte[] exInfo = new byte[0];
public long expireTime;
public int resId;
public int getBizId() {
return this.bizId;
}
public byte[] getExInfo() {
return this.exInfo;
}
public long getExpireTime() {
return this.expireTime;
}
public int getResId() {
return this.resId;
}
public String toString() {
return "MemberIcon{resId=" + this.resId + ",expireTime=" + this.expireTime + ",bizId=" + this.bizId + ",exInfo=" + this.exInfo + ",}";
}
}

View File

@ -1,51 +1,146 @@
package com.tencent.qqnt.kernel.nativeinterface;
public final class MemberInfo implements IKernelModel {
String avatarPath;
String cardName;
int cardType;
boolean isDelete;
boolean isSpecialConcerned;
String nick;
String qid;
String remark;
MemberRole role;
long serialVersionUID;
int shutUpTime;
String uid;
long uin;
import com.tencent.qqnt.kernelpublic.nativeinterface.MemberRole;
public MemberInfo() {
this.serialVersionUID = 1L;
this.uid = "";
this.qid = "";
this.nick = "";
this.remark = "";
this.cardName = "";
this.role = MemberRole.values()[0];
this.avatarPath = "";
public final class MemberInfo implements IKernelModel {
public int bigClubFlag;
public int bigClubLevel;
public int cardNameId;
public int cardType;
public int creditLevel;
public int globalGroupLevel;
public int globalGroupPoint;
public boolean isDelete;
public boolean isRobot;
public boolean isSpecialConcerned;
public boolean isSpecialShielded;
public int joinTime;
public int lastSpeakTime;
public int memberFlag;
public int memberFlagExt;
public int memberFlagExt2;
public int memberLevel;
public int memberMobileFlag;
public int memberTitleId;
public int mssVipType;
public int richFlag;
public int shutUpTime;
public long specialTitleExpireTime;
public long uin;
public int userShowFlag;
public int userShowFlagNew;
long serialVersionUID = 1;
public String uid = "";
public String qid = "";
public String nick = "";
public String remark = "";
public String cardName = "";
public MemberRole role = MemberRole.values()[0];
public String avatarPath = "";
public byte[] groupHonor = new byte[0];
public String memberSpecialTitle = "";
public String autoRemark = "";
public String getAutoRemark() {
return this.autoRemark;
}
public String getAvatarPath() {
return this.avatarPath;
}
public int getBigClubFlag() {
return this.bigClubFlag;
}
public int getBigClubLevel() {
return this.bigClubLevel;
}
public String getCardName() {
return this.cardName;
}
public int getCardNameId() {
return this.cardNameId;
}
public int getCardType() {
return this.cardType;
}
public int getCreditLevel() {
return this.creditLevel;
}
public int getGlobalGroupLevel() {
return this.globalGroupLevel;
}
public int getGlobalGroupPoint() {
return this.globalGroupPoint;
}
public byte[] getGroupHonor() {
return this.groupHonor;
}
public boolean getIsDelete() {
return this.isDelete;
}
public boolean getIsRobot() {
return this.isRobot;
}
public boolean getIsSpecialConcerned() {
return this.isSpecialConcerned;
}
public boolean getIsSpecialShielded() {
return this.isSpecialShielded;
}
public int getJoinTime() {
return this.joinTime;
}
public int getLastSpeakTime() {
return this.lastSpeakTime;
}
public int getMemberFlag() {
return this.memberFlag;
}
public int getMemberFlagExt() {
return this.memberFlagExt;
}
public int getMemberFlagExt2() {
return this.memberFlagExt2;
}
public int getMemberLevel() {
return this.memberLevel;
}
public int getMemberMobileFlag() {
return this.memberMobileFlag;
}
public String getMemberSpecialTitle() {
return this.memberSpecialTitle;
}
public int getMemberTitleId() {
return this.memberTitleId;
}
public int getMssVipType() {
return this.mssVipType;
}
public String getNick() {
return this.nick;
}
@ -58,6 +153,10 @@ public final class MemberInfo implements IKernelModel {
return this.remark;
}
public int getRichFlag() {
return this.richFlag;
}
public MemberRole getRole() {
return this.role;
}
@ -66,6 +165,10 @@ public final class MemberInfo implements IKernelModel {
return this.shutUpTime;
}
public long getSpecialTitleExpireTime() {
return this.specialTitleExpireTime;
}
public String getUid() {
return this.uid;
}
@ -74,26 +177,114 @@ public final class MemberInfo implements IKernelModel {
return this.uin;
}
public int getUserShowFlag() {
return this.userShowFlag;
}
public int getUserShowFlagNew() {
return this.userShowFlagNew;
}
public void setAutoRemark(String str) {
this.autoRemark = str;
}
public void setAvatarPath(String str) {
this.avatarPath = str;
}
public void setBigClubFlag(int i2) {
this.bigClubFlag = i2;
}
public void setBigClubLevel(int i2) {
this.bigClubLevel = i2;
}
public void setCardName(String str) {
this.cardName = str;
}
public void setCardNameId(int i2) {
this.cardNameId = i2;
}
public void setCardType(int i2) {
this.cardType = i2;
}
public void setCreditLevel(int i2) {
this.creditLevel = i2;
}
public void setGlobalGroupLevel(int i2) {
this.globalGroupLevel = i2;
}
public void setGlobalGroupPoint(int i2) {
this.globalGroupPoint = i2;
}
public void setGroupHonor(byte[] bArr) {
this.groupHonor = bArr;
}
public void setIsDelete(boolean z) {
this.isDelete = z;
}
public void setIsRobot(boolean z) {
this.isRobot = z;
}
public void setIsSpecialConcerned(boolean z) {
this.isSpecialConcerned = z;
}
public void setIsSpecialShielded(boolean z) {
this.isSpecialShielded = z;
}
public void setJoinTime(int i2) {
this.joinTime = i2;
}
public void setLastSpeakTime(int i2) {
this.lastSpeakTime = i2;
}
public void setMemberFlag(int i2) {
this.memberFlag = i2;
}
public void setMemberFlagExt(int i2) {
this.memberFlagExt = i2;
}
public void setMemberFlagExt2(int i2) {
this.memberFlagExt2 = i2;
}
public void setMemberLevel(int i2) {
this.memberLevel = i2;
}
public void setMemberMobileFlag(int i2) {
this.memberMobileFlag = i2;
}
public void setMemberSpecialTitle(String str) {
this.memberSpecialTitle = str;
}
public void setMemberTitleId(int i2) {
this.memberTitleId = i2;
}
public void setMssVipType(int i2) {
this.mssVipType = i2;
}
public void setNick(String str) {
this.nick = str;
}
@ -106,6 +297,10 @@ public final class MemberInfo implements IKernelModel {
this.remark = str;
}
public void setRichFlag(int i2) {
this.richFlag = i2;
}
public void setRole(MemberRole memberRole) {
this.role = memberRole;
}
@ -114,6 +309,10 @@ public final class MemberInfo implements IKernelModel {
this.shutUpTime = i2;
}
public void setSpecialTitleExpireTime(long j2) {
this.specialTitleExpireTime = j2;
}
public void setUid(String str) {
this.uid = str;
}
@ -122,30 +321,16 @@ public final class MemberInfo implements IKernelModel {
this.uin = j2;
}
public String toString() {
return "MemberInfo{uid=" + this.uid + ",qid=" + this.qid + ",uin=" + this.uin + ",nick=" + this.nick + ",remark=" + this.remark + ",cardType=" + this.cardType + ",cardName=" + this.cardName + ",role=" + this.role + ",avatarPath=" + this.avatarPath + ",shutUpTime=" + this.shutUpTime + ",isDelete=" + this.isDelete + ",isSpecialConcerned=" + this.isSpecialConcerned + ",}";
public void setUserShowFlag(int i2) {
this.userShowFlag = i2;
}
public MemberInfo(String str, String str2, long j2, String str3, String str4, int i2, String str5, MemberRole memberRole, String str6, int i3, boolean z, boolean z2) {
this.serialVersionUID = 1L;
this.uid = "";
this.qid = "";
this.nick = "";
this.remark = "";
this.cardName = "";
this.role = MemberRole.values()[0];
this.avatarPath = "";
this.uid = str;
this.qid = str2;
this.uin = j2;
this.nick = str3;
this.remark = str4;
this.cardType = i2;
this.cardName = str5;
this.role = memberRole;
this.avatarPath = str6;
this.shutUpTime = i3;
this.isDelete = z;
this.isSpecialConcerned = z2;
public void setUserShowFlagNew(int i2) {
this.userShowFlagNew = i2;
}
public String toString() {
return "MemberInfo{uid=" + this.uid + ",qid=" + this.qid + ",uin=" + this.uin + ",nick=" + this.nick + ",remark=" + this.remark + ",cardType=" + this.cardType + ",cardName=" + this.cardName + ",role=" + this.role + ",avatarPath=" + this.avatarPath + ",shutUpTime=" + this.shutUpTime + ",isDelete=" + this.isDelete + ",isSpecialConcerned=" + this.isSpecialConcerned + ",isRobot=" + this.isRobot + ",groupHonor=" + this.groupHonor + ",memberLevel=" + this.memberLevel + ",globalGroupLevel=" + this.globalGroupLevel + ",globalGroupPoint=" + this.globalGroupPoint + ",memberTitleId=" + this.memberTitleId + ",memberSpecialTitle=" + this.memberSpecialTitle + ",specialTitleExpireTime=" + this.specialTitleExpireTime + ",userShowFlag=" + this.userShowFlag + ",userShowFlagNew=" + this.userShowFlagNew + ",richFlag=" + this.richFlag + ",mssVipType=" + this.mssVipType + ",bigClubLevel=" + this.bigClubLevel + ",bigClubFlag=" + this.bigClubFlag + ",autoRemark=" + this.autoRemark + ",creditLevel=" + this.creditLevel + ",joinTime=" + this.joinTime + ",lastSpeakTime=" + this.lastSpeakTime + ",memberFlag=" + this.memberFlag + ",memberFlagExt=" + this.memberFlagExt + ",memberMobileFlag=" + this.memberMobileFlag + ",memberFlagExt2=" + this.memberFlagExt2 + ",isSpecialShielded=" + this.isSpecialShielded + ",cardNameId=" + this.cardNameId + ",}";
}
}

View File

@ -0,0 +1,19 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class MemberLevelName {
public int level;
public String strName = "";
public int getLevel() {
return this.level;
}
public String getStrName() {
return this.strName;
}
public String toString() {
return "MemberLevelName{level=" + this.level + ",strName=" + this.strName + ",}";
}
}

View File

@ -1,5 +1,7 @@
package com.tencent.qqnt.kernel.nativeinterface;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
import java.io.Serializable;
import java.util.ArrayList;

View File

@ -0,0 +1,4 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class QueryUserSecQualityRsp {
}

View File

@ -1,6 +1,7 @@
package com.tencent.qqnt.kernel.nativeinterface;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
public final class UnreadCntInfo {
UnreadCnt allUnreadCnt;

View File

@ -1,4 +1,6 @@
package com.tencent.qqnt.kernel.nativeinterface;
package com.tencent.qqnt.kernelpublic.nativeinterface;
import com.tencent.qqnt.kernel.nativeinterface.IKernelModel;
import java.io.Serializable;

View File

@ -0,0 +1,9 @@
package com.tencent.qqnt.kernelpublic.nativeinterface;
public enum MemberRole {
UNSPECIFIED,
STRANGER,
MEMBER,
ADMIN,
OWNER
}

View File

@ -2,6 +2,7 @@ package com.tencent.qqnt.msg.api;
import com.tencent.mobileqq.qroute.QRouteApi;
import com.tencent.qqnt.kernel.nativeinterface.*;
import com.tencent.qqnt.kernelpublic.nativeinterface.Contact;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@ -9,7 +10,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import kotlin.Pair;
import kotlinx.coroutines.flow.Flow;
public interface IMsgService extends QRouteApi {

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"
":qqinterface",
":protobuf",
":processor",
":annotations",
":kritor"
)
include(":protobuf")
include(":processor")
include(":annotations")
include(":kritor")
project(":kritor").projectDir = file("kritor/protos")

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"
}
@ -64,8 +63,7 @@ dependencies {
compileOnly("de.robv.android.xposed:api:82")
compileOnly(project(":qqinterface"))
protobuf(project(":kritor"))
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,164 @@
@file:OptIn(DelicateCoroutinesApi::class)
package kritor.client
import com.google.protobuf.ByteString
import io.grpc.CallOptions
import io.grpc.Channel
import io.grpc.ClientCall
import io.grpc.ClientInterceptor
import io.grpc.ForwardingClientCall
import io.grpc.Metadata
import io.grpc.ManagedChannel
import io.grpc.ManagedChannelBuilder
import io.grpc.MethodDescriptor
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 moe.fuqiuluo.shamrock.tools.ShamrockVersion
import qq.service.ticket.TicketHelper
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()
}
val interceptor = object : ClientInterceptor {
override fun <ReqT, RespT> interceptCall(method: MethodDescriptor<ReqT, RespT>, callOptions: CallOptions, next: Channel): ClientCall<ReqT, RespT> {
return object : ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
override fun start(responseListener: Listener<RespT>, headers: Metadata) {
headers.merge(Metadata().apply {
put(Metadata.Key.of("kritor-self-uin", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUin())
put(Metadata.Key.of("kritor-self-uid", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUid())
put(Metadata.Key.of("kritor-self-version", Metadata.ASCII_STRING_MARSHALLER), "OpenShamrock-$ShamrockVersion")
})
super.start(responseListener, headers)
}
}
}
}
channel = ManagedChannelBuilder
.forAddress(host, port)
.usePlaintext()
.enableRetry() // 允许尝试
.executor(Dispatchers.IO.asExecutor()) // 使用协程的调度器
.intercept(interceptor)
.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

@ -2,7 +2,12 @@
package kritor.server
import io.grpc.Grpc
import io.grpc.Metadata
import io.grpc.InsecureServerCredentials
import io.grpc.ServerCall
import io.grpc.ServerCallHandler
import io.grpc.ServerInterceptor
import io.grpc.ForwardingServerCall.SimpleForwardingServerCall;
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -10,22 +15,45 @@ import kotlinx.coroutines.asExecutor
import kritor.auth.AuthInterceptor
import kritor.service.*
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
import qq.service.ticket.TicketHelper
import kotlin.coroutines.CoroutineContext
class KritorServer(
private val port: Int
): CoroutineScope {
private val serverInterceptor = object : ServerInterceptor {
override fun <ReqT, RespT> interceptCall(
call: ServerCall<ReqT, RespT>, headers: Metadata, next: ServerCallHandler<ReqT, RespT>
): ServerCall.Listener<ReqT> {
return next.startCall(object : SimpleForwardingServerCall<ReqT, RespT>(call) {
override fun sendHeaders(headers: Metadata?) {
headers?.apply {
put(Metadata.Key.of("kritor-self-uin", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUin())
put(Metadata.Key.of("kritor-self-uid", Metadata.ASCII_STRING_MARSHALLER), TicketHelper.getUid())
put(Metadata.Key.of("kritor-self-version", Metadata.ASCII_STRING_MARSHALLER), "OpenShamrock-$ShamrockVersion")
}
super.sendHeaders(headers)
}
}, headers)
}
}
private val server = Grpc.newServerBuilderForPort(port, InsecureServerCredentials.create())
.executor(Dispatchers.IO.asExecutor())
.intercept(AuthInterceptor)
.addService(Authentication)
.addService(ContactService)
.addService(KritorService)
.intercept(serverInterceptor)
.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"
@ -82,7 +57,8 @@ object KritorService: KritorServiceGrpcKt.KritorServiceCoroutineImplBase() {
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!!) {
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,67 @@
package kritor.service
import com.google.protobuf.ByteString
import io.kritor.developer.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.utils.FileUtils
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import qq.service.QQInterfaces
import java.io.File
internal object DeveloperService: DeveloperServiceGrpcKt.DeveloperServiceCoroutineImplBase() {
@Grpc("DeveloperService", "Shell")
override suspend fun shell(request: ShellRequest): ShellResponse {
if (request.commandList.isEmpty()) return ShellResponse.newBuilder().setIsSuccess(false).build()
val runtime = Runtime.getRuntime()
val result = withTimeoutOrNull(5000L) {
withContext(Dispatchers.IO) {
runtime.exec(request.commandList.toTypedArray(), null, File(request.directory)).apply { waitFor() }
}
}
return ShellResponse.newBuilder().apply {
if (result == null) {
isSuccess = false
} else {
isSuccess = true
result.inputStream.use {
data = it.readBytes().toString(Charsets.UTF_8)
}
}
}.build()
}
@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
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
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.hasNick()) {
bundle.putString(IProfileProtocolConst.KEY_NICK, request.nick)
}
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: GetUidByUinRequest): GetUidByUinResponse {
return GetUidByUinResponse.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

@ -3,6 +3,7 @@ package kritor.service
import io.grpc.Status
import io.grpc.StatusRuntimeException
import io.kritor.file.*
import moe.fuqiuluo.shamrock.tools.decodeToOidb
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.auto.toByteArray
@ -15,7 +16,6 @@ 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() {
@ -31,42 +31,42 @@ 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()
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val oidbPkg = fromServiceMsg.decodeToOidb()
val rsp = oidbPkg.bytes_bodybuffer.get()
.toByteArray()
.decodeProtobuf<Oidb0x6d7RespBody>()
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(
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) {
).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()
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val oidbPkg = fromServiceMsg.decodeToOidb()
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
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,57 +82,52 @@ 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()
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val oidbPkg = fromServiceMsg.decodeToOidb()
val rsp = oidb_0x6d6.RspBody().apply {
mergeFrom(oidbPkg.bytes_bodybuffer.get().toByteArray())
}
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(
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) {
).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()
oidbPkg.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val oidbPkg = fromServiceMsg.decodeToOidb()
val rsp = oidbPkg.bytes_bodybuffer.get().toByteArray().decodeProtobuf<Oidb0x6d7RespBody>()
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,188 @@ 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.LogCenter
import moe.fuqiuluo.shamrock.helper.TroopHonorHelper.decodeHonor
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
import qq.service.contact.ContactHelper
import qq.service.group.GroupHelper
import tencent.im.troop.honor.troop_honor
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
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.banMember(request.groupId, when(request.targetCase!!) {
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
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.duration)
}, 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!!) {
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
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
})
return pokeMemberResponse { }
}
)
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
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!!) {
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
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
})
return kickMemberResponse { }
}
)
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
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.modifyGroupMemberCard(request.groupId, when(request.targetCase!!) {
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
ModifyMemberCardRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
.toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.card)
return modifyMemberCardResponse { }
}, 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
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
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.setGroupAdmin(request.groupId, when(request.targetCase!!) {
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
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.isAdmin)
}, 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
throw StatusRuntimeException(
Status.PERMISSION_DENIED
.withDescription("You are not admin of this group")
)
}
GroupHelper.setGroupUniqueTitle(request.groupId, when(request.targetCase!!) {
GroupHelper.setGroupUniqueTitle(
request.groupId.toString(), when (request.targetCase!!) {
SetGroupUniqueTitleRequest.TargetCase.TARGET_UIN -> request.targetUin
SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid).toLong()
else -> throw StatusRuntimeException(Status.INVALID_ARGUMENT
SetGroupUniqueTitleRequest.TargetCase.TARGET_UID -> ContactHelper.getUinByUidAsync(request.targetUid)
.toLong()
else -> throw StatusRuntimeException(
Status.INVALID_ARGUMENT
.withDescription("target not set")
)
}, request.uniqueTitle)
}.toString(), request.uniqueTitle
)
return setGroupUniqueTitleResponse { }
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
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 +191,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 +212,46 @@ 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 {
groupId = groupInfo.troopcode.toLong()
groupName = groupInfo.troopname.ifNullOrEmpty { groupInfo.troopRemark }.ifNullOrEmpty { groupInfo.newTroopName } ?: ""
this.addGroupsInfo(GroupInfo.newBuilder().apply {
groupId = groupInfo.troopcode.ifNullOrEmpty { groupInfo.uin }.ifNullOrEmpty { groupInfo.troopuin }?.toLong() ?: 0
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
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")
)
}).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member info").withCause(it))
}.toString()
).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 }
@ -272,73 +259,78 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
.ifNullOrEmpty { memberInfo.friendnick } ?: ""
age = memberInfo.age.toInt()
uniqueTitle = memberInfo.mUniqueTitle ?: ""
uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire
uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire.toLong()
card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: ""
joinTime = memberInfo.join_time
lastActiveTime = memberInfo.last_active_time
level = memberInfo.level
shutUpTimestamp = memberInfo.gagTimeStamp
shutUpTime = 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,
if (request.hasRefresh()) request.refresh else false
).onFailure {
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get group member list").withCause(it)
)
}.getOrThrow()
return getGroupMemberListResponse {
memberList.forEach { memberInfo ->
this.groupMemberInfo.add(groupMemberInfo {
uid = ContactHelper.getUidByUinAsync(memberInfo.memberuin?.toLong() ?: 0)
uin = memberInfo.memberuin?.toLong() ?: 0
nick = memberInfo.troopnick
.ifNullOrEmpty { memberInfo.hwName }
.ifNullOrEmpty { memberInfo.troopColorNick }
.ifNullOrEmpty { memberInfo.friendnick } ?: ""
age = memberInfo.age.toInt()
uniqueTitle = memberInfo.mUniqueTitle ?: ""
uniqueTitleExpireTime = memberInfo.mUniqueTitleExpire
card = memberInfo.troopnick.ifNullOrEmpty { memberInfo.friendnick } ?: ""
joinTime = memberInfo.join_time
lastActiveTime = memberInfo.last_active_time
level = memberInfo.level
shutUpTimestamp = memberInfo.gagTimeStamp
return GetGroupMemberListResponse.newBuilder().apply {
memberList.values.forEach { memberInfo ->
this.addGroupMembersInfo(GroupMemberInfo.newBuilder().apply {
uid = memberInfo.uid
uin = memberInfo.uin
nick = memberInfo.nick ?: ""
age = 0
uniqueTitle = memberInfo.memberSpecialTitle ?: ""
uniqueTitleExpireTime = memberInfo.specialTitleExpireTime
card = memberInfo.cardName.ifNullOrEmpty { memberInfo.nick } ?: ""
joinTime = memberInfo.joinTime.toLong()
lastActiveTime = memberInfo.lastSpeakTime.toLong()
level = memberInfo.memberLevel
shutUpTime = memberInfo.shutUpTime.toLong()
distance = memberInfo.distance
honor.addAll((memberInfo.honorList ?: "")
.split("|")
.filter { it.isNotBlank() }
.map { it.toInt() })
distance = 0
addAllHonors(memberInfo.groupHonor.let { bytes ->
val honor = troop_honor.GroupUserCardHonor()
honor.mergeFrom(bytes)
honor.id.get()
})
unfriendly = false
cardChangeable = GroupHelper.isAdmin(request.groupId.toString())
cardChangeable = memberInfo.role == com.tencent.qqnt.kernelpublic.nativeinterface.MemberRole.ADMIN
})
}
}
}.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
prohibitedTime = it.shutuptimestap.toLong()
})
}
}
}.build()
}
@Grpc("GroupService", "GetRemainCountAtAll")
@ -346,52 +338,57 @@ 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
maxMemberCount = groupInfo.maxMember
memberCount = groupInfo.memberCount
groupDesc = groupInfo.groupDesc
createTime = groupInfo.createTime.toInt()
createTime = groupInfo.createTime
groupFlag = groupInfo.groupFlag
groupFlagExt = groupInfo.groupFlagExt
}
}
}.build()
}.build()
}
@Grpc("GroupService", "GetGroupHonor")
override suspend fun getGroupHonor(request: GetGroupHonorRequest): GetGroupHonorResponse {
return getGroupHonorResponse {
GroupHelper.getGroupMemberList(request.groupId.toString(), true).onFailure {
throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to get group member list").withCause(it))
return GetGroupHonorResponse.newBuilder().apply {
GroupHelper.getGroupMemberList(request.groupId, true).onFailure {
throw StatusRuntimeException(
Status.INTERNAL.withDescription("unable to get group member list").withCause(it)
)
}.onSuccess { memberList ->
memberList.forEach { member ->
(member.honorList ?: "").split("|")
.filter { it.isNotBlank() }
.map { it.toInt() }.forEach {
val honor = decodeHonor(member.memberuin.toLong(), it, member.mHonorRichFlag)
memberList.values.forEach { member ->
member.groupHonor.let { bytes ->
val honor = troop_honor.GroupUserCardHonor()
honor.mergeFrom(bytes)
honor.id.get()
}.forEach {
val honor = decodeHonor(member.uin, it, 0)
if (honor != null) {
groupHonorInfo.add(groupHonorInfo {
uid = ContactHelper.getUidByUinAsync(member.memberuin.toLong())
uin = member.memberuin.toLong()
nick = member.troopnick
.ifNullOrEmpty { member.hwName }
.ifNullOrEmpty { member.troopColorNick }
.ifNullOrEmpty { member.friendnick } ?: ""
addGroupHonorsInfo(GroupHonorInfo.newBuilder().apply {
uid = member.uid
uin = member.uin
nick = member.nick.ifEmpty {
member.cardName
} ?: ""
honorName = honor.honorName
avatar = honor.honorIconUrl
id = honor.honorId
@ -401,6 +398,6 @@ internal object GroupService: GroupServiceGrpcKt.GroupServiceCoroutineImplBase()
}
}
}
}
}.build()
}
}

View File

@ -1,7 +1,475 @@
package kritor.service
import io.kritor.message.MessageServiceGrpcKt
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.kernelpublic.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() {
@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.toLong()
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(
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
this.senderNick = it.senderNick
this.senderUin = it.senderId
this.operationTime = it.operatorTime
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.isSet
)
return ReactMessageWithEmojiResponse.newBuilder().build()
}
}

View File

@ -0,0 +1,33 @@
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()
}
@Grpc("QsignService", "GetCmdWhitelist")
override suspend fun getCmdWhitelist(request: GetCmdWhitelistRequest): GetCmdWhitelistResponse {
return GetCmdWhitelistResponse.newBuilder().apply {
addAllCommands(FEKit.getInstance().cmdWhiteList)
}.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

@ -20,6 +20,7 @@ private val configKeys = setOf(
ResourceGroup,
RPCAddress,
RPCPort,
AliveReply,
)
internal object ShamrockConfig: Properties() {

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,62 @@
@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.Scene
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.contact.ContactHelper
import qq.service.msg.toKritorEventMessages
internal object GlobalEventTransmitter : QQInterfaces() {
private val messageEventFlow by lazy {
MutableSharedFlow<Pair<MsgRecord, MessageEvent>>()
private val MessageEventFlow by lazy {
MutableSharedFlow<Pair<MsgRecord, PushMessageBody>>()
}
private val noticeEventFlow by lazy {
MutableSharedFlow<NoticeEvent>()
}
private val requestEventFlow by lazy {
MutableSharedFlow<RequestEvent>()
}
//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: RequestEvent) = 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 {
this.time = record.msgTime.toInt()
this.scene = Scene.GROUP
this.messageId = record.msgId
transMessageEvent(record, PushMessageBody.newBuilder().apply {
this.time = record.msgTime
this.messageId = record.msgId.toString()
this.messageSeq = record.msgSeq
this.contact = contact {
this.scene = scene
this.contact = Contact.newBuilder().apply {
this.scene = Scene.GROUP
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 +64,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
this.messageId = record.msgId.toString()
this.messageSeq = record.msgSeq
this.contact = Contact.newBuilder().apply {
this.scene = Scene.FRIEND
this.peer = record.senderUid
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 +89,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
this.messageId = record.msgId.toString()
this.messageSeq = record.msgSeq
this.contact = Contact.newBuilder().apply {
this.scene = if (groupCode > 0) Scene.STRANGER_FROM_GROUP else Scene.STRANGER
this.peer = record.senderUid
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,24 +112,37 @@ 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
this.messageId = record.msgId.toString()
this.messageSeq = record.msgSeq
this.contact = Contact.newBuilder().apply {
this.scene = Scene.GUILD
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 {
/**
* 推送私聊文件事件
*/
suspend fun transPrivateFileEvent(
msgTime: Long,
userId: Long,
senderUid: String,
senderUin: Long,
fileId: String,
fileSubId: String,
fileName: String,
@ -106,23 +150,20 @@ 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.PRIVATE_FILE_UPLOADED
this.time = msgTime
this.privateFileUploaded = PrivateFileUploadedNotice.newBuilder().apply {
this.fileId = fileId
this.fileName = fileName
this.operatorUid = senderUid
this.operatorUin = senderUin
this.fileSize = fileSize
this.expireTime = expireTime
this.fileSubId = fileSubId.toInt() // todo(这玩意真的是一个数字?)
this.fileUrl = url
}.build()
}.build())
return true
}
@ -131,7 +172,8 @@ internal object GlobalEventTransmitter: QQInterfaces() {
*/
suspend fun transGroupFileEvent(
msgTime: Long,
userId: Long,
senderUid: String,
senderUin: Long,
groupId: Long,
uuid: String,
fileName: String,
@ -139,22 +181,20 @@ 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_UPLOADED
this.time = msgTime
this.groupFileUploaded = GroupFileUploadedNotice.newBuilder().apply {
this.groupId = groupId
this.operatorUid = senderUid
this.operatorUin = senderUin
this.fileId = uuid
this.fileName = fileName
this.fileSize = fileSize
this.fileSubId = bizId
this.fileUrl = url
}.build()
}.build())
return true
}
}
@ -163,69 +203,98 @@ 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_IN
this.time = time
this.groupSignIn = GroupSignInNotice.newBuilder().apply {
this.groupId = groupCode
this.targetUid = ContactHelper.getUidByUinAsync(target)
this.targetUin = target
this.action = action
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
this.groupPoke = GroupPokeNotice.newBuilder().apply {
this.groupId = groupCode
this.action = action
this.targetUid = ContactHelper.getUidByUinAsync(target)
this.targetUin = target
this.operatorUid = ContactHelper.getUidByUinAsync(operator)
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
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_DECREASE
this.time = time
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 +305,41 @@ 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
this.groupAdminChanged = GroupAdminChangedNotice.newBuilder().apply {
this.groupId = groupCode
this.targetUid = targetUid
this.targetUin = target
this.isAdmin = setAdmin
}.build()
}.build())
return true
}
suspend fun transGroupWholeBan(
msgTime: Long,
groupCode: Long,
operatorUid: String,
operator: Long,
isOpen: Boolean
): Boolean {
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.GROUP_WHOLE_BAN
this.time = msgTime
this.groupWholeBan = GroupWholeBanNotice.newBuilder().apply {
this.groupId = groupCode
this.isBan = isOpen
this.operatorUid = operatorUid
this.operatorUin = operator
}.build()
}.build())
return true
}
suspend fun transGroupBan(
msgTime: Long,
subType: NoticeSubType,
operator: Long,
operatorUid: String,
target: Long,
@ -262,82 +347,83 @@ 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_BAN
this.time = msgTime
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
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
this.groupCardChanged = GroupCardChangedNotice.newBuilder().apply {
this.groupId = groupId
this.targetUin = targetId
this.newCard = newCard
}.build()
}.build())
return true
}
suspend fun transTitleChange(
time: Long,
targetId: Long,
targetUin: Long,
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
this.groupMemberUniqueTitleChanged = GroupUniqueTitleChangedNotice.newBuilder().apply {
this.groupId = groupId
this.targetUid = ContactHelper.getUidByUinAsync(targetUin)
this.targetUin = targetUin
this.title = title
}.build()
}.build())
return true
}
@ -345,21 +431,23 @@ 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
this.groupEssenceChanged = GroupEssenceMessageNotice.newBuilder().apply {
this.groupId = groupId
this.messageId = msgId.toString()
this.targetUid = ContactHelper.getUidByUinAsync(targetUin)
this.targetUin = senderUin
this.operatorUid = ContactHelper.getUidByUinAsync(operatorUin)
this.operatorUin = operatorUin
this.isSet = subType.toInt() == 1
}.build()
}.build())
return true
}
}
@ -368,37 +456,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,
action: String?,
suffix: String?,
actionImg: String?
): Boolean {
pushNotice(NoticeEvent.newBuilder().apply {
this.type = NoticeEvent.NoticeType.PRIVATE_POKE
this.time = msgTime
this.privatePoke = PrivatePokeNotice.newBuilder().apply {
this.action = action ?: ""
this.operatorUid = ContactHelper.getUidByUinAsync(operator)
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.PRIVATE_RECALL
this.time = time
this.privateRecall = PrivateRecallNotice.newBuilder().apply {
this.operatorUin = operator
this.messageId = msgId.toString()
this.tipText = tipText
}.build()
}.build())
return true
}
@ -408,53 +496,71 @@ 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, applierUid: String, operator: Long, tipText: String, flag: String): Boolean {
pushRequest(RequestEvent.newBuilder().apply {
this.type = RequestEvent.RequestType.FRIEND_APPLY
this.time = time
this.requestId = flag
this.friendApply = FriendApplyRequest.newBuilder().apply {
this.applierUid = applierUid
this.applierUin = operator
this.message = tipText
}.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(RequestEvent.newBuilder().apply {
this.type = RequestEvent.RequestType.GROUP_APPLY
this.time = time
this.requestId = flag
this.groupApply = GroupApplyRequest.newBuilder().apply {
this.applierUid = applierUid
this.applierUin = applierUin
this.groupId = groupCode
this.reason = reason
}.build()
}.build())
return true
}
}*/
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, MessageEvent>>) {
messageEventFlow.collect {
suspend fun transGroupInvite(
time: Long,
inviterUid: String,
inviterUin: Long,
groupCode: Long,
flag: String
): Boolean {
pushRequest(RequestEvent.newBuilder().apply {
this.type = RequestEvent.RequestType.GROUP_APPLY
this.time = time
this.requestId = flag
this.invitedGroup = InvitedJoinGroupRequest.newBuilder().apply {
this.inviterUid = inviterUid
this.inviterUin = inviterUin
this.groupId = groupCode
}.build()
}.build())
return true
}
}
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 {
@ -469,5 +575,5 @@ internal object GlobalEventTransmitter: QQInterfaces() {
collector.emit(it)
}
}
}*/
}
}

View File

@ -0,0 +1,33 @@
package moe.fuqiuluo.shamrock.tools
import com.tencent.qphone.base.remote.FromServiceMsg
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.oidb.TrpcOidb
import tencent.im.oidb.oidb_sso
fun FromServiceMsg.decodeToOidb(): oidb_sso.OIDBSSOPkg {
return kotlin.runCatching {
oidb_sso.OIDBSSOPkg().mergeFrom(wupBuffer.slice(4).let {
if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it
})
}.getOrElse {
oidb_sso.OIDBSSOPkg().mergeFrom(wupBuffer.let {
if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it
})
}
}
fun FromServiceMsg.decodeToTrpcOidb(): TrpcOidb {
return kotlin.runCatching {
wupBuffer.slice(4).let {
if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it
}.decodeProtobuf<TrpcOidb>()
}.getOrElse {
wupBuffer.let {
if (it[0] == 0x78.toByte()) DeflateTools.uncompress(it) else it
}.decodeProtobuf<TrpcOidb>()
}
}

View File

@ -6,17 +6,21 @@ import android.content.Context
import android.content.Context.BATTERY_SERVICE
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.os.BatteryManager
import android.os.Build
import android.os.Process
import android.provider.Settings
import kotlinx.serialization.Serializable
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
import mqq.app.MobileQQ
import kotlin.random.Random
internal object PlatformUtils {
const val QQ_9_0_8_VER = 5540
const val QQ_9_0_65_VER = 6566
fun getQUA(): String {
return "V1_AND_SQ_${getQQVersion(MobileQQ.getContext())}_${getQQVersionCode()}_YYB_D"
@ -69,6 +73,15 @@ internal object PlatformUtils {
return MobileQQ.getMobileQQ().qqProcessName == "com.tencent.tim"
}
fun isApkInDebug(context: Context): Boolean {
try {
val info = context.applicationInfo
return (info.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
} catch (e: Exception) {
return false
}
}
fun killProcess(context: Context, processName: String) {
for (processInfo in (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).runningAppProcesses) {
if (processInfo.processName == processName) {

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) {
if (app.isLogin) {
AppTalker.talk("switch_status") {
put("account", app.currentAccountUin)
put("nickname", if (app is QQAppInterface) app.currentNickname else "unknown")
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,556 @@
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.kernelpublic.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.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.decodeToTrpcOidb
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.tools.toHexString
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.DownloadRkeyReq
import protobuf.oidb.cmd0x11c5.DownloadRkeyRsp
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)
}
suspend fun getTempNtRKey(): Result<DownloadRkeyRsp> {
runCatching {
val req = NtV2RichMediaReq(
head = MultiMediaReqHead(
commonHead = CommonHead(
requestId = requestIdSeq.incrementAndGet().toULong(),
cmd = 202u
),
sceneInfo = SceneInfo(
requestType = 2u,
businessType = 1u,
sceneType = 0u,
),
clientMeta = ClientMeta(2u)
),
downloadRkey = DownloadRkeyReq(
types = listOf(10, 20),
downloadType = 2
)
).toByteArray()
val fromServiceMsg = sendOidbAW("OidbSvcTrpcTcp.0x9067_202", 0x9067, 202, req, true)
if (fromServiceMsg == null || fromServiceMsg.wupBuffer == null) {
return Result.failure(Exception("failed to fetch NtTempRKey: ${fromServiceMsg?.wupBuffer?.toHexString()}"))
}
val trpc = fromServiceMsg.decodeToTrpcOidb()
if (trpc.buffer == null) {
return Result.failure(Exception("failed to fetch NtTempRKey: ${trpc.msg}"))
}
trpc.buffer?.decodeProtobuf<NtV2RichMediaRsp>()?.downloadRkeyRsp?.let {
return Result.success(it)
}
}.onFailure {
return Result.failure(it)
}
return Result.failure(Exception("failed to fetch NtTempRKey"))
}
/**
* 获取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?.toHexString()}"))
}
val trpc = fromServiceMsg.decodeToTrpcOidb()
if (trpc.buffer == null) {
return Result.failure(Exception("unable to get multimedia pic info: ${trpc.msg}"))
}
trpc.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 trpc = fromServiceMsg.decodeToTrpcOidb()
if (trpc.buffer == null) {
return Result.failure(Exception("unable to request upload nt pic: ${trpc.msg}"))
}
val rsp = trpc.buffer!!.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,499 @@
@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 com.tencent.qqnt.kernel.nativeinterface.Image
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.kernel.nativeinterface.PicElement
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.decodeToOidb
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
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 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 = fromServiceMsg.decodeToOidb()
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 = fromServiceMsg.decodeToOidb()
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 = fromServiceMsg.decodeToOidb()
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 getTempPicDownloadUrl(
chatType: Int,
originalUrl: String,
md5: String,
image: PicElement,
storeId: Int = 0,
peer: String? = null,
subPeer: String? = null,
): String {
val isNtServer = originalUrl.startsWith("/download")
if (isNtServer) {
val tmpRKey = NtV2RichMediaSvc.getTempNtRKey()
if (tmpRKey.isSuccess) {
val tmpRKeyRsp = tmpRKey.getOrThrow()
val tmpRKeyMap = hashMapOf<UInt, String>()
tmpRKeyRsp.rkeys?.forEach { rKeyInfo ->
tmpRKeyMap[rKeyInfo.type] = rKeyInfo.rkey
}
val rkey = tmpRKeyMap[when(chatType) {
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> 10u
MsgConstant.KCHATTYPEC2C -> 20u
MsgConstant.KCHATTYPEGUILD -> 10u
else -> 0u
}]
if (rkey != null) {
return "https://$MULTIMEDIA_DOMAIN$originalUrl$rkey"
}
}
}
return when (chatType) {
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> getGroupPicDownUrl(
originalUrl = originalUrl,
md5 = md5,
fileId = image.fileUuid,
width = image.picWidth.toUInt(),
height = image.picHeight.toUInt(),
sha = "",
fileSize = image.fileSize.toULong(),
peer = peer ?: "0"
)
MsgConstant.KCHATTYPEC2C -> getC2CPicDownUrl(
originalUrl = originalUrl,
md5 = md5,
fileId = image.fileUuid,
width = image.picWidth.toUInt(),
height = image.picHeight.toUInt(),
sha = "",
fileSize = image.fileSize.toULong(),
peer = peer ?: "0",
storeId = storeId
)
MsgConstant.KCHATTYPEGUILD -> getGuildPicDownUrl(
originalUrl = originalUrl,
md5 = md5,
fileId = image.fileUuid,
width = image.picWidth.toUInt(),
height = image.picHeight.toUInt(),
sha = "",
fileSize = image.fileSize.toULong(),
peer = peer ?: "0",
subPeer = subPeer ?: "0"
)
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
}
}
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.kernelpublic.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,16 @@ 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.decodeToOidb
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 +182,30 @@ 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 = fromServiceMsg.decodeToOidb()
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

@ -1,23 +1,15 @@
@file:OptIn(ExperimentalStdlibApi::class)
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
import moe.fuqiuluo.shamrock.tools.decodeToOidb
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.DeflateTools
import qq.service.QQInterfaces
import tencent.im.oidb.cmd0x6d8.oidb_0x6d8
@ -33,18 +25,15 @@ 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
val limitCnt: Int
if (fromServiceMsg.wupBuffer != null) {
oidb_0x6d8.RspBody().mergeFrom(
oidb_sso.OIDBSSOPkg()
.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
.bytes_bodybuffer.get()
.toByteArray()
).group_file_cnt_rsp.apply {
val oidb1 = fromServiceMsg.decodeToOidb()
oidb_0x6d8.RspBody().mergeFrom(oidb1.bytes_bodybuffer.get().toByteArray()).group_file_cnt_rsp.apply {
fileCnt = uint32_all_file_count.get()
limitCnt = uint32_limit_count.get()
}
@ -61,11 +50,9 @@ internal object GroupFileHelper: QQInterfaces() {
val totalSpace: Long
val usedSpace: Long
if (fromServiceMsg2.isSuccess && fromServiceMsg2.wupBuffer != null) {
oidb_0x6d8.RspBody().mergeFrom(
oidb_sso.OIDBSSOPkg()
.mergeFrom(fromServiceMsg2.wupBuffer.slice(4))
.bytes_bodybuffer.get()
.toByteArray()).group_space_rsp.apply {
val oidb2 = fromServiceMsg2.decodeToOidb()
oidb_0x6d8.RspBody().mergeFrom(oidb2.bytes_bodybuffer.get().toByteArray()).group_space_rsp.apply {
totalSpace = uint64_total_space.get()
usedSpace = uint64_used_space.get()
}
@ -73,15 +60,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 {
@ -103,48 +90,46 @@ 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) {
}.toByteArray(), timeout = 30.seconds) ?: throw StatusRuntimeException(Status.INTERNAL.withDescription("unable to send oidb request"))
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
})
val oidb = fromServiceMsg.decodeToOidb()
oidb_0x6d8.RspBody().mergeFrom(oidb.bytes_bodybuffer.get().toByteArray())
.file_list_info_rsp.apply {
rpt_item_list.get().forEach { file ->
if (file.uint32_type.get() == oidb_0x6d8.GetFileListRspBody.TYPE_FILE) {
val fileInfo = file.file_info
files.add(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()
this.busId = fileInfo.uint32_bus_id.get()
this.uploadTime = fileInfo.uint32_upload_time.get()
this.deadTime = fileInfo.uint32_dead_time.get()
this.modifyTime = fileInfo.uint32_modify_time.get()
this.uploadTime = fileInfo.uint32_upload_time.get().toLong()
this.expireTime = fileInfo.uint32_dead_time.get().toLong()
this.modifyTime = fileInfo.uint32_modify_time.get().toLong()
this.downloadTimes = fileInfo.uint32_download_times.get()
this.uploader = fileInfo.uint64_uploader_uin.get()
this.uploaderName = fileInfo.str_uploader_name.get()
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.createTime = folderInfo.uint32_create_time.get().toLong()
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 +139,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

@ -19,10 +19,13 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.decodeToOidb
import qq.service.internals.NTServiceFetcher
import moe.fuqiuluo.shamrock.tools.ifNullOrEmpty
import moe.fuqiuluo.shamrock.tools.putBuf32Long
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import moe.fuqiuluo.shamrock.utils.PlatformUtils.QQ_9_0_65_VER
import protobuf.auto.toByteArray
import protobuf.oidb.cmd0xf16.Oidb0xf16
import protobuf.oidb.cmd0xf16.SetGroupRemarkReq
@ -37,10 +40,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 +103,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 +277,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 { "" }
}))
@ -241,22 +401,46 @@ internal object GroupHelper: QQInterfaces() {
sendOidb("OidbSvc.0x89a_0", 2202, 0, reqBody.toByteArray())
}
suspend fun getGroupMemberList(groupId: String, refresh: Boolean): Result<List<TroopMemberInfo>> {
val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
var memberList = service.getAllTroopMembers(groupId)
if (refresh || memberList == null) {
memberList = requestTroopMemberInfo(service, groupId).onFailure {
return Result.failure(Exception("获取群成员列表失败"))
}.getOrThrow()
suspend fun getGroupMemberList(groupId: Long, refresh: Boolean): Result<HashMap<String, MemberInfo>> {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val service = sessionService.groupService
val uids = suspendCancellableCoroutine { continuation ->
service.getAllMemberList(groupId, refresh) { _, _, groupMemberListResult ->
continuation.resume(groupMemberListResult?.ids?.map {
it.uid
})
}
}
val memberMap = suspendCancellableCoroutine { continuation ->
service.getMemberInfoForMqq(groupId, ArrayList(uids ?: emptyList()), refresh) { _, _, groupMemberListResult ->
continuation.resume(groupMemberListResult.infos)
}
}
// val extInfo = suspendCancellableCoroutine { continuation ->
// service.getMemberExtInfo(GroupMemberExtReq().apply {
// this.groupCode = groupId
// this.beginUin = 0.toString()
// this.groupType = ""
// this.memberExtFilter = MemberExtInfoFilter().apply {
// this.memberLevelInfoName = 1
// this.memberLevelInfoUin = 1
// this.nickName = 1
// this.specialTitle = 1
// this.memberLevelInfoActiveDay = 1
// }
// this.richCardNameVer = "1"
// this.sourceType = 1
// this.uinList = ArrayList(memberMap.values.toList().map {
// it.uin
// })
// }) { _, _, groupMemberExtListResult ->
// continuation.resume(groupMemberExtListResult)
// }
// }
getGroupInfo(groupId, true).onSuccess {
if(it.wMemberNum > memberList.size) {
return getGroupMemberList(groupId, true)
}
}
return Result.success(memberMap)
return Result.success(memberList)
}
suspend fun getProhibitedMemberList(groupId: Long): Result<List<ProhibitedMemberInfo>> {
@ -269,11 +453,10 @@ 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()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val body = fromServiceMsg.decodeToOidb()
if(body.uint32_result.get() != 0) {
return Result.failure(RuntimeException(body.str_error_msg.get()))
}
@ -291,11 +474,10 @@ 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()
body.mergeFrom(fromServiceMsg.wupBuffer.slice(4))
val body = fromServiceMsg.decodeToOidb()
if(body.uint32_result.get() != 0) {
return Result.failure(RuntimeException(body.str_error_msg.get()))
}
@ -311,7 +493,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 +549,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 +569,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 +604,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 +614,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 +637,21 @@ 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> {
if(PlatformUtils.getQQVersionCode() >= QQ_9_0_65_VER) {
return Result.failure(Exception("当前版本不支持该API"))
}
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 +661,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 +673,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 +688,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,32 @@
@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 com.tencent.qqnt.kernel.nativeinterface.TextElement
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import moe.fuqiuluo.shamrock.config.AliveReply
import moe.fuqiuluo.shamrock.config.get
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.internals.GlobalEventTransmitter
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.file.GroupFileHelper
import qq.service.group.GroupHelper
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)
@ -54,9 +37,87 @@ object AioListener: IKernelMsgListener {
}
}
private suspend fun debugTest(record: MsgRecord, text: String) {
if (record.chatType == MsgConstant.KCHATTYPEGROUP && text == ".shamrock.members") {
val contact = MessageHelper.generateContact(record)
GroupHelper.getGroupMemberList(record.peerUin, true).onSuccess {
MessageHelper.sendMessage(contact, arrayListOf(
MsgElement().apply {
elementType = MsgConstant.KELEMTYPETEXT
textElement = TextElement().apply {
content = "memberCount: ${it.size}"
}
}
), 3, MessageHelper.generateMsgId(record.chatType))
}.onFailure {
LogCenter.log("获取群成员列表失败: $it", Level.ERROR)
}
} else if (record.chatType == MsgConstant.KCHATTYPEGROUP && text == ".shamrock.root_files") {
val contact = MessageHelper.generateContact(record)
val files = GroupFileHelper.getGroupFiles(record.peerUin)
MessageHelper.sendMessage(contact, arrayListOf(
MsgElement().apply {
elementType = MsgConstant.KELEMTYPETEXT
textElement = TextElement().apply {
content = "foldersCount: ${files.foldersCount}\nfilesCount: ${files.filesCount}"
}
}
), 3, MessageHelper.generateMsgId(record.chatType))
} else if (record.chatType == MsgConstant.KCHATTYPEGROUP && text == ".shamrock.pic_url") {
val contact = MessageHelper.generateContact(record)
val pic = record.elements.filter {
it.elementType == MsgConstant.KELEMTYPEPIC
}.map {
val image = it.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
}
val originalUrl = image.originImageUrl ?: ""
return@map RichProtoSvc.getTempPicDownloadUrl(record.chatType, originalUrl, md5, image, storeId)
}
MessageHelper.sendMessage(contact, arrayListOf(
MsgElement().apply {
elementType = MsgConstant.KELEMTYPETEXT
textElement = TextElement().apply {
content = "picUrl: \n${
pic.joinToString("\n")
}"
}
}
), 3, MessageHelper.generateMsgId(record.chatType))
}
}
private suspend fun onMsg(record: MsgRecord) {
if (AliveReply.get()) {
val texts = record.elements.filter { it.elementType == MsgConstant.KELEMTYPETEXT }
val text = texts.joinToString { it.textElement.content }
if (texts.isNotEmpty() && text == "ping") {
val contact = MessageHelper.generateContact(record)
MessageHelper.sendMessage(contact, arrayListOf(
MsgElement().apply {
elementType = MsgConstant.KELEMTYPETEXT
textElement = TextElement().apply {
content = "pong"
}
}
), 3, MessageHelper.generateMsgId(record.chatType))
return
}
debugTest(record, text)
}
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 +146,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 +170,66 @@ 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 fileMsg = record.elements.firstOrNull {
it.elementType == MsgConstant.KELEMTYPEFILE
}?.fileElement ?: kotlin.run {
LogCenter.log("消息为私聊文件消息但不包含文件消息,来自:${record.peerUin}", Level.WARN)
return
}
override fun onFirstViewGroupGuildMapping(arrayList: ArrayList<FirstViewGroupGuildInfo>?) {
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)
}
override fun onGrabPasswordRedBag(
i2: Int,
str: String?,
i3: Int,
recvdOrder: RecvdOrder?,
msgRecord: MsgRecord?
if (!GlobalEventTransmitter.FileNoticeTransmitter
.transPrivateFileEvent(record.msgTime, record.senderUid, record.senderUin, fileId, fileSubId, fileName, fileSize, expireTime, url)
) {
}
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?) {
LogCenter.log("私聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
}
}
private suspend fun onGroupFileMsg(record: MsgRecord) {
val groupId = record.peerUin
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, record.senderUid, record.senderUin, groupId, uuid, fileName, fileSize, bizId, url)
) {
LogCenter.log("群聊文件消息推送失败 -> FileNoticeTransmitter", Level.WARN)
}
}
@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>) {
}
}

Some files were not shown because too many files have changed in this diff Show More