25 Commits

Author SHA1 Message Date
2c8094c8c8 Shamrock: 临时修补rkey缺失导致的qqnt图片无法获取 #236 2024-02-17 19:39:26 +08:00
62385d6f62 Shamrock: 修复合并转发获取图片错误 #236 2024-02-17 15:43:12 +08:00
3b210d7ed0 Shamrock: fix #236 2024-02-17 09:37:01 +08:00
63ce2d40bd Shamrock: fix get role by nt crash 2024-02-16 10:50:00 +08:00
36f8b6e54b Shamrock: Change the image upload source to a camera 2024-02-16 09:50:31 +08:00
58413044e9 Shamrock: typo log 2024-02-16 09:16:05 +08:00
3395cd9d95 Shamrock: 支持群临时消息推送携带群号以及群名称 2024-02-16 09:12:45 +08:00
494b1f1fd0 Shamrock: 允许禁止QQ启动无关紧要的进程服务 2024-02-16 00:09:35 +08:00
cf943fd13a Shamrock: atメッセージ優先nameパラメータ 2024-02-15 13:18:43 +08:00
9608b46799 Shamrock: 是正メッセージプッシュアイデンティティの取得が遅い 2024-02-15 13:14:37 +08:00
502956e3ec Shamrock: エイト・メッセージにニックネームの迅速なクエリを許可する 2024-02-15 13:01:34 +08:00
27b4c26da7 Shamrock: fix GlobalEventTransmitter x2 2024-02-11 14:28:07 +08:00
65f54360f8 Shamrock: not fix GlobalEventTransmitter x2 2024-02-11 14:06:10 +08:00
9a9fad975f Shamrock: not fix GlobalEventTransmitter 2024-02-11 13:57:58 +08:00
7153b21cd4 Shamrock: fix GlobalEventTransmitter 2024-02-11 13:42:28 +08:00
fdb2486090 Shamrock: Disable lost connection detection 2024-02-10 00:41:38 +08:00
d60b2a25d1 Update SECURITY.md 2024-02-09 08:04:40 +08:00
2d8dde6951 add history msg to database 2024-02-08 23:52:21 +08:00
78fd60dade Merge pull request #228 from Mythologyli/master
feat: get group applier uin from request msg
2024-02-08 22:34:40 +08:00
80dbf6af28 feat: get group applier uin from request msg 2024-02-08 22:27:55 +08:00
1e53753b5a Shamrock: fix #227 2024-02-08 20:17:51 +08:00
e727877268 Merge pull request #225 from MrXiaoM/fix-guild-message
修复 频道消息事件不符合 go-cqhttp 规范
2024-02-08 20:16:01 +08:00
63b69df3ea fix missing guild_id and channel_id 2024-02-08 14:51:14 +08:00
b03e02675b Shamrock: add timeout #223 2024-02-05 22:16:12 +08:00
e68a1ffd37 Shamrock: fix guild sync 2024-02-05 22:12:20 +08:00
50 changed files with 876 additions and 148 deletions

View File

@ -1,11 +1,19 @@
# Security Policy
## Support Version
## 支持的版本
| Version | Supported |
| 版本 | 支持状态 |
| ------- | ------------------ |
| 9.0.15 | :white_check_mark: |
| 8.9.75 | :white_check_mark: |
| 8.9.73 | :white_check_mark: |
| 8.9.98 | :white_check_mark: |
| < 8.9.68| :x: |
## 频道支持性说明
如果需要使用`频道`相关功能请升级QQ到9.0.8版本
## Riru检测问题
QQ自`9.0.8`开始将会检测riru可能作为封号因素

View File

@ -229,6 +229,16 @@ object ShamrockConfig {
return preferences.getBoolean("anti_qq_trace", true)
}
fun isForbidUselessProcess(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("forbid_useless_process", false)
}
fun setForbidUselessProcess(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("forbid_useless_process", v).apply()
}
fun setAntiTrace(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("anti_qq_trace", v).apply()
@ -333,6 +343,7 @@ object ShamrockConfig {
"alive_reply" to preferences.getBoolean("alive_reply", false),
"enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false),
"disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false),
"forbid_useless_process" to preferences.getBoolean("forbid_useless_process", false)
)
}

View File

@ -100,17 +100,16 @@ fun LabFragment() {
thickness = 0.2.dp
)
/*
Function(
title = "自动清理QQ垃圾",
desc = "也许会导致奇怪的问题(无效)。",
title = "禁止无用进程",
desc = "禁止QQ生成无用进程浪费内存",
descColor = color,
isSwitch = ShamrockConfig.isAutoClean(ctx)
isSwitch = ShamrockConfig.isForbidUselessProcess(ctx)
) {
ShamrockConfig.setAutoClean(ctx, it)
ShamrockConfig.setForbidUselessProcess(ctx, it)
ShamrockConfig.pushUpdate(ctx)
return@Function false
}*/
return@Function true
}
Function(
title = "自回复测试",

View File

@ -0,0 +1,19 @@
package com.tencent.guild.api.transfile;
import androidx.annotation.Nullable;
import com.tencent.mobileqq.qroute.QRouteApi;
import com.tencent.qqnt.kernel.nativeinterface.BigDataTicket;
public interface IGuildTransFileApi extends QRouteApi {
//@Nullable
//ArrayList<ServerAddress> getBigDataIpList(boolean z, @Nullable IpType ipType);
@Nullable
BigDataTicket getBigDataTicket();
//@Nullable
//ArrayList<ServerAddress> getIpDirectList(@Nullable String str, @Nullable IpType ipType);
void pullConfigIfNeed();
}

View File

@ -0,0 +1,26 @@
package com.tencent.libra.download;
import androidx.annotation.NonNull;
import com.tencent.libra.request.Option;
public interface ILibraDownloader {
class PicDownLoadListener {
Option mOption;
public PicDownLoadListener(@NonNull Option option) {
this.mOption = option;
}
public void onResult(boolean success, int code) {
}
}
boolean canDownload(Option option);
void cancel(Option option);
void downLoad(Option option, PicDownLoadListener picDownLoadListener);
boolean needDownloadOnWorkThread();
}

View File

@ -0,0 +1,4 @@
package com.tencent.libra.request;
public class Option {
}

View File

@ -0,0 +1,29 @@
package com.tencent.mobileqq.perf.block;
import android.os.Bundle;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import epic.EIPCClient;
import epic.EIPCResult;
import kotlin.Metadata;
import kotlin.jvm.JvmStatic;
public final class BinderMethodProxy {
@NotNull
public static final BinderMethodProxy INSTANCE;
static {
INSTANCE = new BinderMethodProxy();
}
@JvmStatic
public static EIPCResult callServer(@NotNull EIPCClient client, @Nullable String module, @Nullable String action, @Nullable Bundle bundle) {
//MainBlockMethodMonitor.onMethodStart();
//EIPCResult callServer = client.callServer(str, str2, bundle);
//MainBlockMethodMonitor.onMethodEnd();
//return callServer;
return null;
}
}

View File

@ -0,0 +1,13 @@
package com.tencent.mobileqq.qipc;
import epic.EIPCClient;
public class QIPCClientHelper {
public static synchronized QIPCClientHelper getInstance() {
return null;
}
public EIPCClient getClient() {
return null;
}
}

View File

@ -0,0 +1,7 @@
package com.tencent.mobileqq.qmmkv;
public class MMKVOptionEntity {
public String decodeString(String str, String str2) {
return "";
}
}

View File

@ -0,0 +1,9 @@
package com.tencent.mobileqq.qmmkv;
import android.content.Context;
public class QMMKV {
public static MMKVOptionEntity from(Context context, String str) {
return null;
}
}

View File

@ -0,0 +1,40 @@
package com.tencent.mobileqq.transfile.dns;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class IpData implements Parcelable {
public int mFailedCount;
public String mIp;
public int mPort;
public int mType;
/**
* Describe the kinds of special objects contained in this Parcelable
* instance's marshaled representation. For example, if the object will
* include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
* the return value of this method must include the
* {@link #CONTENTS_FILE_DESCRIPTOR} bit.
*
* @return a bitmask indicating the set of special object types marshaled
* by this Parcelable object instance.
*/
@Override
public int describeContents() {
return 0;
}
/**
* Flatten this object in to a Parcel.
*
* @param dest The Parcel in which the object should be written.
* @param flags Additional flags about how the object should be written.
* May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
*/
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
}
}

View File

@ -0,0 +1,11 @@
package com.tencent.qqnt.aio.api;
import com.tencent.libra.download.ILibraDownloader;
import com.tencent.mobileqq.qroute.QRouteApi;
import org.jetbrains.annotations.NotNull;
public interface IAIOPicDownloaderProvider extends QRouteApi {
@NotNull
ILibraDownloader provideDownloader();
}

View File

@ -1,7 +1,9 @@
package com.tencent.qqnt.kernel.api.impl;
import com.tencent.qqnt.kernel.nativeinterface.IGetTempChatInfoCallback;
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener;
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback;
import com.tencent.qqnt.kernel.nativeinterface.RichMediaElementGetReq;
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo;
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo;
@ -9,6 +11,10 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class MsgService {
void getRichMediaElement(@NotNull RichMediaElementGetReq req) {
}
public void addMsgListener(IKernelMsgListener listener) {
}
@ -24,4 +30,8 @@ public class MsgService {
public void prepareTempChat(TempChatPrepareInfo tempChatPrepareInfo, IOperateCallback cb) {
}
public void getTempChatInfo(int chatType, @Nullable String uid, @Nullable IGetTempChatInfoCallback cb) {
}
}

View File

@ -0,0 +1,30 @@
package com.tencent.qqnt.kernel.nativeinterface;
public final class BigDataTicket {
public String sessionKey;
public String sessionSig;
public BigDataTicket() {
this.sessionSig = "";
this.sessionKey = "";
}
public String getSessionKey() {
return this.sessionKey;
}
public String getSessionSig() {
return this.sessionSig;
}
public String toString() {
return "BigDataTicket{sessionSig=" + this.sessionSig + ",sessionKey=" + this.sessionKey + ",}";
}
public BigDataTicket(String str, String str2) {
this.sessionSig = "";
this.sessionKey = "";
this.sessionSig = str;
this.sessionKey = str2;
}
}

View File

@ -0,0 +1,5 @@
package com.tencent.qqnt.kernel.nativeinterface;
public interface IGetTempChatInfoCallback {
void onResult(int code, String msg, TempChatInfo info);
}

View File

@ -264,6 +264,10 @@ public final class PicElement implements IKernelModel {
this.transferStatus = num;
}
public int getStoreID() {
return 0;
}
public String toString() {
return "PicElement{picSubType=" + this.picSubType + ",fileName=" + this.fileName + ",fileSize=" + this.fileSize + ",picWidth=" + this.picWidth + ",picHeight=" + this.picHeight + ",original=" + this.original + ",md5HexStr=" + this.md5HexStr + ",sourcePath=" + this.sourcePath + ",thumbPath=" + this.thumbPath + ",transferStatus=" + this.transferStatus + ",progress=" + this.progress + ",picType=" + this.picType + ",invalidState=" + this.invalidState + ",fileUuid=" + this.fileUuid + ",fileSubId=" + this.fileSubId + ",thumbFileSize=" + this.thumbFileSize + ",fileBizId=" + this.fileBizId + ",downloadIndex=" + this.downloadIndex + ",summary=" + this.summary + ",emojiFrom=" + this.emojiFrom + ",emojiWebUrl=" + this.emojiWebUrl + ",emojiAd=" + this.emojiAd + ",emojiMall=" + this.emojiMall + ",emojiZplan=" + this.emojiZplan + ",originImageMd5=" + this.originImageMd5 + ",originImageUrl=" + this.originImageUrl + ",importRichMediaContext=" + this.importRichMediaContext + ",isFlashPic=" + this.isFlashPic + ",}";
}

View File

@ -0,0 +1,118 @@
package com.tencent.qqnt.kernel.nativeinterface;
public final class RichMediaElementGetReq implements IKernelModel {
public int chatType;
public int downSourceType;
public int downloadType;
public long elementId;
public long fileModelId;
public String filePath;
public long msgId;
public String peerUid;
public int thumbSize;
public int triggerType;
public RichMediaElementGetReq() {
this.peerUid = "";
this.filePath = "";
}
public int getChatType() {
return this.chatType;
}
public int getDownSourceType() {
return this.downSourceType;
}
public int getDownloadType() {
return this.downloadType;
}
public long getElementId() {
return this.elementId;
}
public long getFileModelId() {
return this.fileModelId;
}
public String getFilePath() {
return this.filePath;
}
public long getMsgId() {
return this.msgId;
}
public String getPeerUid() {
return this.peerUid;
}
public int getThumbSize() {
return this.thumbSize;
}
public int getTriggerType() {
return this.triggerType;
}
public void setChatType(int i2) {
this.chatType = i2;
}
public void setDownSourceType(int i2) {
this.downSourceType = i2;
}
public void setDownloadType(int i2) {
this.downloadType = i2;
}
public void setElementId(long j2) {
this.elementId = j2;
}
public void setFileModelId(long j2) {
this.fileModelId = j2;
}
public void setFilePath(String str) {
this.filePath = str;
}
public void setMsgId(long j2) {
this.msgId = j2;
}
public void setPeerUid(String str) {
this.peerUid = str;
}
public void setThumbSize(int i2) {
this.thumbSize = i2;
}
public void setTriggerType(int i2) {
this.triggerType = i2;
}
public String toString() {
return "RichMediaElementGetReq{msgId=" + this.msgId + ",peerUid=" + this.peerUid + ",chatType=" + this.chatType + ",elementId=" + this.elementId + ",downloadType=" + this.downloadType + ",thumbSize=" + this.thumbSize + ",filePath=" + this.filePath + ",fileModelId=" + this.fileModelId + ",downSourceType=" + this.downSourceType + ",triggerType=" + this.triggerType + ",}";
}
public RichMediaElementGetReq(long j2, String str, int i2, long j3, int i3, int i4, String str2, long j4, int i5, int i6) {
this.peerUid = "";
this.filePath = "";
this.msgId = j2;
this.peerUid = str;
this.chatType = i2;
this.elementId = j3;
this.downloadType = i3;
this.thumbSize = i4;
this.filePath = str2;
this.fileModelId = j4;
this.downSourceType = i5;
this.triggerType = i6;
}
}

View File

@ -0,0 +1,4 @@
package epic;
public class EIPCClient {
}

View File

@ -0,0 +1,11 @@
package epic;
import android.os.Bundle;
public class EIPCResult {
public Bundle data;
public boolean isSuccess() {
return false;
}
}

View File

@ -59,6 +59,7 @@ import moe.fuqiuluo.shamrock.remote.service.data.EssenceMessage
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncement
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessage
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessageImage
import moe.fuqiuluo.shamrock.remote.service.data.push.MemberRole
import moe.fuqiuluo.shamrock.tools.EmptyJsonArray
import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.tools.asInt
@ -96,6 +97,8 @@ import java.nio.ByteBuffer
import kotlin.coroutines.resume
internal object GroupSvc: BaseSvc() {
private const val GET_MEMBER_ROLE_BY_NT = false
private val RefreshTroopMemberInfoLock by lazy {
Mutex()
}
@ -394,6 +397,27 @@ internal object GroupSvc: BaseSvc() {
.filter { it != 0L }
}
suspend fun getMemberRole(groupId: Long, memberUin: Long): MemberRole {
if (!GET_MEMBER_ROLE_BY_NT) {
return when (memberUin) {
getOwner(groupId.toString()) -> MemberRole.Owner
in getAdminList(groupId.toString()) -> MemberRole.Admin
else -> MemberRole.Member
}
}
return when(getTroopMemberInfoByUinViaNt(groupId.toString(), memberUin, 3000).getOrNull()?.role) {
com.tencent.qqnt.kernel.nativeinterface.MemberRole.STRANGER -> MemberRole.Stranger
com.tencent.qqnt.kernel.nativeinterface.MemberRole.MEMBER -> MemberRole.Member
com.tencent.qqnt.kernel.nativeinterface.MemberRole.ADMIN -> MemberRole.Admin
com.tencent.qqnt.kernel.nativeinterface.MemberRole.OWNER -> MemberRole.Owner
com.tencent.qqnt.kernel.nativeinterface.MemberRole.UNSPECIFIED, null -> when (memberUin) {
getOwner(groupId.toString()) -> MemberRole.Owner
in getAdminList(groupId.toString()) -> MemberRole.Admin
else -> MemberRole.Member
}
}
}
fun getOwner(groupId: String): Long {
val groupInfo = getGroupInfo(groupId)
return groupInfo.troopowneruin?.toLong() ?: 0
@ -566,24 +590,53 @@ internal object GroupSvc: BaseSvc() {
}
}
private suspend fun getTroopMemberInfoByUinViaNt(groupId: String, qq: Long): Result<MemberInfo> {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val groupService = sessionService.groupService
val info = suspendCancellableCoroutine {
groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data ->
if (code != 0) {
it.resume(null)
return@getTransferableMemberInfo
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.toLong(), uin.toLong(), timeout = 2000).getOrNull()
}
if (info == null) {
info = getTroopMemberInfoByUinViaNt(groupId, uin.toLong(), timeout = 2000L).getOrNull()?.let {
TroopMemberInfo().apply {
troopnick = it.cardName
friendnick = it.nick
}
data.forEach { (_, info) ->
if (info.uin == qq) {
it.resume(info)
return@forEach
}
}
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 respBuffer = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray(), timeout = 2000)
if (respBuffer != null) {
val rsp = group_member_info.RspBody()
rsp.mergeFrom(respBuffer.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
}
}
it.resume(null)
}
} catch (err: Throwable) {
LogCenter.log(err.stackTraceToString(), Level.WARN)
}
return if (info != null) {
Result.success(info)
@ -592,6 +645,40 @@ internal object GroupSvc: BaseSvc() {
}
}
suspend fun getTroopMemberInfoByUinViaNt(
groupId: String,
qq: Long,
timeout: Long = 5000L
): Result<MemberInfo> {
return runCatching {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val groupService = sessionService.groupService
val info = withTimeoutOrNull(timeout) {
suspendCancellableCoroutine {
groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data ->
if (code != 0) {
it.resume(null)
return@getTransferableMemberInfo
}
data.forEach { (_, info) ->
if (info.uin == qq) {
it.resume(info)
return@forEach
}
}
it.resume(null)
}
}
}
return if (info != null) {
Result.success(info)
} else {
Result.failure(Exception("获取群成员信息失败"))
}
}
}
suspend fun getTroopMemberInfoByUid(groupId: Long, uid: String): Result<MemberInfo> {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
@ -748,7 +835,7 @@ internal object GroupSvc: BaseSvc() {
}
}
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long): Result<TroopMemberInfo> {
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long, timeout: Long = 10_000): Result<TroopMemberInfo> {
val info = RefreshTroopMemberInfoLock.withLock {
val groupIdStr = groupId.toString()
val memberUinStr = memberUin.toString()
@ -758,7 +845,7 @@ internal object GroupSvc: BaseSvc() {
requestMemberInfoV2(groupId, memberUin)
requestMemberInfo(groupId, memberUin)
withTimeoutOrNull(10000) {
withTimeoutOrNull(timeout) {
while (!service.isMemberInCache(groupIdStr, memberUinStr)) {
delay(200)
}

View File

@ -9,6 +9,7 @@ import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession
import com.tencent.qqnt.kernel.nativeinterface.TempChatInfo
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo
import com.tencent.qqnt.msg.api.IMsgService
import kotlinx.coroutines.DelicateCoroutinesApi
@ -42,9 +43,12 @@ internal object MsgSvc: BaseSvc() {
msgService.prepareTempChat(TempChatPrepareInfo(
MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
ContactHelper.getUidByUinAsync(peerId = peerId.toLong()),
app.getRuntimeService(ITroopMemberNameService::class.java, "all")
.getTroopMemberNameRemarkFirst(groupId, peerId),
groupId, EMPTY_BYTE_ARRAY, app.currentUid, "", TempChatGameSession()
app.getRuntimeService(ITroopMemberNameService::class.java, "all").getTroopMemberNameRemarkFirst(groupId, peerId),
groupId,
EMPTY_BYTE_ARRAY,
app.currentUid,
"",
TempChatGameSession()
)) { code, reason ->
if (code != 0) {
LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR)
@ -53,6 +57,24 @@ internal object MsgSvc: BaseSvc() {
return Result.success(Unit)
}
suspend fun getTempChatInfo(chatType: Int, uid: String): Result<TempChatInfo> {
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
?: return Result.failure(Exception("获取消息服务失败"))
val info: TempChatInfo = withTimeoutOrNull(5000) {
suspendCancellableCoroutine {
msgService.getTempChatInfo(chatType, uid) { code, msg, tempChatInfo ->
if (code == 0) {
it.resume(tempChatInfo)
} else {
LogCenter.log("获取临时会话信息失败: $code:$msg", Level.ERROR)
it.resume(null)
}
}
}
} ?: return Result.failure(Exception("获取临时会话信息失败"))
return Result.success(info)
}
/**
* 正常获取
*/
@ -183,8 +205,8 @@ internal object MsgSvc: BaseSvc() {
}
}
val result = MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
result.onFailure {
LogCenter.log("sendToAio: " + it.stackTraceToString(), Level.ERROR)
if (result.isFailure) {
LogCenter.log("sendToAio: " + result.exceptionOrNull()?.stackTraceToString(), Level.ERROR)
return result
}
val sendResult = result.getOrThrow()

View File

@ -13,8 +13,11 @@ import tencent.im.oidb.oidb_sso
internal object TicketSvc: BaseSvc() {
object SigType {
const val WLOGIN_A2 = 64
const val WLOGIN_A5 = 2
const val WLOGIN_RESERVED = 16
const val WLOGIN_STWEB = 32 // TLV 103
const val WLOGIN_A2 = 64
const val WLOGIN_ST = 128
const val WLOGIN_AQSIG = 2097152
const val WLOGIN_D2 = 262144
const val WLOGIN_DA2 = 33554432
@ -26,14 +29,17 @@ internal object TicketSvc: BaseSvc() {
const val WLOGIN_PSKEY = 1048576
const val WLOGIN_PT4Token = 134217728
const val WLOGIN_QRPUSH = 67108864
const val WLOGIN_RESERVED = 16
const val WLOGIN_SID = 524288
const val WLOGIN_SIG64 = 8192
const val WLOGIN_SKEY = 4096
const val WLOGIN_ST = 128
const val WLOGIN_STWEB = 32 // TLV 103
const val WLOGIN_TOKEN = 32768
const val WLOGIN_VKEY = 131072
val ALL_TICKET = arrayOf(
WLOGIN_A5, WLOGIN_RESERVED, WLOGIN_STWEB, WLOGIN_A2, WLOGIN_ST, WLOGIN_AQSIG, WLOGIN_D2, WLOGIN_DA2,
WLOGIN_LHSIG, WLOGIN_LSKEY, WLOGIN_OPENKEY, WLOGIN_PAYTOKEN, WLOGIN_PF, WLOGIN_PSKEY, WLOGIN_PT4Token,
WLOGIN_QRPUSH, WLOGIN_SID, WLOGIN_SIG64, WLOGIN_SKEY, WLOGIN_TOKEN, WLOGIN_VKEY
)
}
fun getUin(): String {

View File

@ -670,17 +670,22 @@ internal object MessageMaker {
at.atNtUid = "0"
}
else -> {
val info = GroupSvc.getTroopMemberInfoByUin(peerId, qq, true).onFailure {
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
}.getOrNull()
if (info != null) {
at.content = "@${
info.troopnick
.ifNullOrEmpty(info.friendnick)
.ifNullOrEmpty(qq)
}"
val name = data["name"].asStringOrNull
if (name == null) {
val info = GroupSvc.getTroopMemberInfoByUinV2(peerId, qq, true).onFailure {
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
}.getOrNull()
if (info != null) {
at.content = "@${
info.troopnick
.ifNullOrEmpty(info.friendnick)
.ifNullOrEmpty(qq)
}"
} else {
at.content = "@$qq"
}
} else {
at.content = "@${data["name"].asStringOrNull.ifNullOrEmpty(qq)}"
at.content = "@$name"
}
at.atType = MsgConstant.ATTYPEONE
at.atNtUid = ContactHelper.getUidByUinAsync(qq.toLong())

View File

@ -1,5 +1,6 @@
package moe.fuqiuluo.qqinterface.servlet.msg.convert
import com.tencent.mobileqq.qmmkv.QMMKV
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
@ -13,6 +14,8 @@ import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.json
import mqq.app.MobileQQ
import kotlin.jvm.internal.Intrinsics
internal sealed class MessageElemConverter: IMessageConvert {
/**
@ -135,14 +138,19 @@ internal sealed class MessageElemConverter: IMessageConvert {
ImageMapping(md5.uppercase(), chatType, image.fileSize)
)
//LogCenter.log(image.toString())
val originalUrl = image.originImageUrl ?: ""
//LogCenter.log({ "receive image: $image" }, Level.DEBUG)
return MessageSegment(
type = "image",
data = hashMapOf(
"file" to md5,
"url" to when(chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(md5)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(md5)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(md5)
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(originalUrl, md5)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(originalUrl, md5)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(originalUrl, md5)
else -> unknownChatType(chatType)
},
"subType" to image.picSubType,

View File

@ -1,9 +1,13 @@
@file:OptIn(ExperimentalSerializationApi::class)
package moe.fuqiuluo.qqinterface.servlet.transfile
import android.os.Bundle
import com.tencent.mobileqq.pb.ByteStringMicro
import com.tencent.mobileqq.perf.block.BinderMethodProxy
import com.tencent.mobileqq.qipc.QIPCClientHelper
import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.api.IProtoReqManager
import com.tencent.mobileqq.transfile.dns.IpData
import com.tencent.mobileqq.transfile.protohandler.RichProto
import com.tencent.mobileqq.transfile.protohandler.RichProtoProc
import kotlinx.coroutines.suspendCancellableCoroutine
@ -19,18 +23,35 @@ import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import mqq.app.MobileQQ
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo
import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
import protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody
import mqq.app.MobileQQ
import tencent.im.cs.cmd0x346.cmd0x346
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
import tencent.im.oidb.cmd0xe37.cmd0xe37
import tencent.im.oidb.oidb_sso
import java.util.ArrayList
import kotlin.coroutines.resume
private const val GPRO_PIC = "gchat.qpic.cn"
private const val GPRO_PIC_NT = "multimedia.nt.qq.com.cn"
private const val C2C_PIC = "c2cpicdw.qpic.cn"
internal object RichProtoSvc: BaseSvc() {
/*@Deprecated("Use RichProtoSvc.getQQDns instead", ReplaceWith("getQQDns(domain)"))
fun getQQDns(domain: String) {
val bundle = Bundle()
bundle.putString("domain", "xxx")
bundle.putInt("businessType", 1)
val result = BinderMethodProxy
.callServer(QIPCClientHelper.getInstance().client, "InnerDnsModule", "reqDomain2IpList", bundle)
if (result.isSuccess) {
val ipList: ArrayList<IpData> = result.data.getParcelableArrayList("ip")!!
}
}*/
suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String {
val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, ProtoBuf.encodeToByteArray(
Oidb0xfc2ReqBody(
@ -142,19 +163,40 @@ internal object RichProtoSvc: BaseSvc() {
}
fun getGroupPicDownUrl(
md5: String
originalUrl: String,
md5: String,
): String {
return "http://gchat.qpic.cn/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2"
val domain = if (originalUrl.startsWith("/download")) GPRO_PIC_NT else GPRO_PIC
if (originalUrl.isNotEmpty()) {
if (!originalUrl.contains("rkey=")) {
return "https://$domain$originalUrl&rkey=CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
}
return "https://$domain$originalUrl"
}
return "https://$domain/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2"
}
fun getC2CPicDownUrl(
originalUrl: String,
md5: String
): String {
return "https://c2cpicdw.qpic.cn/offpic_new/0/123-0-${md5.uppercase()}/0?term=2"
if (originalUrl.isNotEmpty()) {
return "https://$C2C_PIC$originalUrl"
}
return "https://$C2C_PIC/offpic_new/0/123-0-${md5.uppercase()}/0?term=2"
}
fun getGuildPicDownUrl(md5: String): String {
return "https://gchat.qpic.cn/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2"
fun getGuildPicDownUrl(
originalUrl: String,
md5: String
): String {
val domain = if (originalUrl.startsWith("/download") ||
originalUrl.contains("rkey=")) GPRO_PIC_NT
else GPRO_PIC
if (originalUrl.isNotEmpty()) {
return "https://$domain$originalUrl"
}
return "https://$domain/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2"
}
suspend fun getC2CVideoDownUrl(
@ -321,12 +363,4 @@ internal object RichProtoSvc: BaseSvc() {
RichProtoProc.procRichProtoReq(richProtoReq)
}
}
suspend fun getGuildPttDownUrl(
peerId: String,
md5Hex: String,
fileUUId: String
): String {
return "unsupported"
}
}

View File

@ -6,6 +6,7 @@ import com.tencent.mobileqq.transfile.TransferRequest
import moe.fuqiuluo.shamrock.utils.MD5
import java.io.File
import moe.fuqiuluo.qqinterface.servlet.transfile.ResourceType.*
import moe.fuqiuluo.shamrock.helper.TransfileHelper
internal object Transfer: FileTransfer() {
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
@ -84,11 +85,14 @@ internal object Transfer: FileTransfer() {
file: File,
wait: Boolean = true
): Boolean {
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_SHARE, wait) {
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
picUpExtraInfo.mIsRaw = true
picUpExtraInfo.mIsRaw = false
picUpExtraInfo.mUinType = FileMsg.UIN_BUDDY
it.mPicSendSource = 8
it.mExtraObj = picUpExtraInfo
it.mIsPresend = true
it.delayShowProgressTimeInMs = 2000
}
}
@ -97,10 +101,13 @@ internal object Transfer: FileTransfer() {
file: File,
wait: Boolean = true
): Boolean {
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_SHARE, wait) {
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
picUpExtraInfo.mIsRaw = true
//picUpExtraInfo.mIsRaw = !TransfileHelper.isGifFile(file)
picUpExtraInfo.mIsRaw = false
picUpExtraInfo.mUinType = FileMsg.UIN_TROOP
it.mPicSendSource = 8
it.delayShowProgressTimeInMs = 2000
it.mExtraObj = picUpExtraInfo
}
}

View File

@ -353,6 +353,21 @@ internal object MessageHelper {
database.messageMappingDao().insert(mapping)
}
fun saveMsgMappingNotExist(
hash: Int,
qqMsgId: Long,
time: Long,
chatType: Int,
peerId: String,
subPeerId: String,
msgSeq: Int,
subChatType: Int = chatType
) {
val database = MessageDB.getInstance()
val mapping = MessageMapping(hash, qqMsgId, chatType, subChatType, peerId, time, msgSeq, subPeerId)
database.messageMappingDao().insertNotExist(mapping)
}
external fun createMessageUniseq(chatType: Int, time: Long): Long
fun decodeCQCode(code: String): JsonArray {

View File

@ -1,6 +1,8 @@
package moe.fuqiuluo.shamrock.helper
import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
internal object TransfileHelper {
private val extensionMap = mapOf(
@ -94,4 +96,15 @@ internal object TransfileHelper {
val extension = name.substring(index)
return extensionMap[extension] ?: -1
}
fun isGifFile(picFile: File): Boolean {
if (picFile.exists() && picFile.length() > 3) {
return RandomAccessFile(picFile, "r").use {
val bArr = ByteArray(3)
it.read(bArr)
if (bArr[0].toInt() == 71 && bArr[1].toInt() == 73 && bArr[2].toInt() == 70) { return true } else false
}
}
return false
}
}

View File

@ -30,6 +30,9 @@ interface MessageMappingDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(mapping: MessageMapping)
@Insert(onConflict = OnConflictStrategy.ABORT)
fun insertNotExist(mapping: MessageMapping)
@Query("UPDATE message_mapping_v2 SET msgSeq = :msgSeq WHERE msgHashId = :hash")
fun updateMsgSeqByMsgHash(hash: Int, msgSeq: Int)

View File

@ -1,9 +1,12 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.guild.api.transfile.IGuildTransFileApi
import com.tencent.mobileqq.qroute.QRoute
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.data.BigDataTicket
import moe.fuqiuluo.shamrock.remote.service.data.Credentials
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.symbols.OneBotHandler
@ -17,10 +20,20 @@ internal object GetCookies: IActionHandler() {
}
operator fun invoke(echo: JsonElement = EmptyJsonString): String {
return ok(Credentials(cookie = TicketSvc.getCookie()), echo)
return ok(Credentials(
cookie = TicketSvc.getCookie(),
bigDataTicket = QRoute.api(IGuildTransFileApi::class.java).bigDataTicket?.let {
BigDataTicket(it.sessionKey, it.sessionSig)
}
), echo)
}
suspend operator fun invoke(domain: String, echo: JsonElement = EmptyJsonString): String {
return ok(Credentials(cookie = TicketSvc.getCookie(domain)), echo)
return ok(Credentials(
cookie = TicketSvc.getCookie(domain),
bigDataTicket = QRoute.api(IGuildTransFileApi::class.java).bigDataTicket?.let {
BigDataTicket(it.sessionKey, it.sessionSig)
}
), echo)
}
}

View File

@ -1,5 +1,6 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
@ -18,8 +19,10 @@ internal object GetGProChannelList: IActionHandler() {
return invoke(guildId.toULong(), refresh, echo = session.echo)
}
operator fun invoke(guildId: ULong, refresh: Boolean, echo: JsonElement = EmptyJsonString): String {
val result = GProSvc.getChannelList(guildId, refresh)
suspend operator fun invoke(guildId: ULong, refresh: Boolean, echo: JsonElement = EmptyJsonString): String {
val result = withTimeoutOrNull(5000) {
GProSvc.getChannelList(guildId, refresh)
} ?: return error("timeout", echo)
result.onFailure {
return error(it.message ?: "unable to fetch channel list", echo)
}

View File

@ -59,6 +59,16 @@ internal object GetHistoryMsg: IActionHandler() {
val msgList = ArrayList<MessageDetail>().apply {
addAll(result.data!!.map { msg ->
val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId)
MessageHelper.saveMsgMappingNotExist(
hash = msgHash,
qqMsgId = msg.msgId,
chatType = msg.chatType,
subChatType = msg.chatType,
peerId = peerId,
msgSeq = msg.msgSeq.toInt(),
time = msg.msgTime,
subPeerId = msg.channelId ?: peerId
)
MessageDetail(
time = msg.msgTime.toInt(),
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),

View File

@ -35,8 +35,8 @@ internal object GetImage: IActionHandler() {
image.size,
image.fileName,
when(image.chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(fileMd5)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(fileMd5)
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl("", fileMd5)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl("", fileMd5)
else -> error("Not supported chat type: ${image.chatType}, convertMsgElementsToMsgSegment::Pic")
}
), echo = echo)

View File

@ -52,11 +52,7 @@ internal object GetTroopMemberInfo : IActionHandler() {
area = info.alias ?: "",
lastSentTime = info.last_active_time,
level = info.level,
role = when {
GroupSvc.getOwner(groupId).toString() == uin -> MemberRole.Owner
uin.toLong() in GroupSvc.getAdminList(groupId) -> MemberRole.Admin
else -> MemberRole.Member
},
role = GroupSvc.getMemberRole(groupId.toLong(), uin.toLong()),
unfriendly = false,
title = info.mUniqueTitle ?: "",
titleExpireTime = info.mUniqueTitleExpire,

View File

@ -54,12 +54,13 @@ internal object GetTroopMemberList : IActionHandler() {
area = info.alias ?: "",
lastSentTime = info.last_active_time,
level = info.level,
role = when {
role = GroupSvc.getMemberRole(groupId.toLong(), info.memberuin.toLong())
/*when {
GroupSvc.getOwner(groupId)
.toString() == info.memberuin -> MemberRole.Owner
info.memberuin.toLong() in GroupSvc.getAdminList(groupId) -> MemberRole.Admin
else -> MemberRole.Member
},
}*/,
unfriendly = false,
title = info.mUniqueTitle ?: "",
titleExpireTime = info.mUniqueTitleExpire,

View File

@ -2,6 +2,7 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
@ -17,7 +18,7 @@ internal object ModifyTroopMemberName: IActionHandler() {
}
operator fun invoke(groupId: String, userId: String, card: String, echo: JsonElement = EmptyJsonString): String {
if (!GroupSvc.isAdmin(groupId)) {
if (!GroupSvc.isAdmin(groupId) && userId != TicketSvc.getUin()) {
return logic("you are not admin", echo)
}
return if(GroupSvc.modifyGroupMemberCard(groupId.toLong(), userId.toLong(), card))

View File

@ -103,7 +103,7 @@ internal object UploadGroupFile : IActionHandler() {
// 根据文件大小调整超时时间
val msgIdPair = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEGROUP)
val info = (withTimeoutOrNull((srcFile.length() / (300 * 1024)) * 1000 + 5000) {
val info = (withTimeoutOrNull((srcFile.length() / (125 * 1024)) * 1000 + 5000) {
val msgService = QRoute.api(IMsgService::class.java)
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId)
suspendCancellableCoroutine<FileTransNotifyInfo?> {

View File

@ -5,6 +5,7 @@ import com.tencent.mobileqq.transfile.TransferRequest
import com.tencent.mobileqq.transfile.api.ITransFileController
import io.ktor.server.routing.Routing
import io.ktor.server.routing.post
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.remote.structures.Status
import moe.fuqiuluo.shamrock.tools.fetchPost
import moe.fuqiuluo.shamrock.tools.respond
@ -15,7 +16,7 @@ import kotlin.random.Random
import kotlin.random.nextLong
fun Routing.registerBDH() {
post("/upload_group_image") {
if(ShamrockConfig.isDev()) post("/upload_group_image") {
val troop = fetchPost("troop")
val picBytes = Base64.decode(fetchPost("pic"), Base64.DEFAULT)
val md5Str = MD5.getMd5Hex(picBytes)
@ -46,5 +47,4 @@ fun Routing.registerBDH() {
.transferAsync(transferRequest)
respond(isOk = true, Status.Ok, "$md5Str.jpg")
}
}

View File

@ -5,7 +5,9 @@ import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import io.ktor.server.application.call
import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.shamrock.remote.action.handlers.*
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.remote.structures.Status
import moe.fuqiuluo.shamrock.tools.*
@ -44,20 +46,36 @@ fun Routing.ticketActions() {
}
}
fun getTicket(uin: String, id: Int, debug: Boolean = false) = TicketSvc.getTicket(uin, id)?.let {
mutableMapOf(
"sig" to (it._sig?.toHexString() ?: "null"),
"key" to (it._sig_key?.toHexString() ?: "null")
).also { map ->
if (debug)
map["content"] = ((it._sig?.decodeToString() ?: "") + ":" + (it._sig_key?.decodeToString() ?: "null"))
}.json.asJsonObject
} ?: EmptyJsonObject
getOrPost("/get_ticket") {
val uin = fetchOrThrow("uin")
val ticket = when(val id = fetchOrThrow("id").toInt()) {
32 -> TicketSvc.getStWeb(uin)
else -> {
respond(true, Status.Ok, data = TicketSvc.getTicket(uin, id)?.let {
mapOf(
"sig" to (it._sig?.toHexString() ?: "null"),
"key" to (it._sig_key?.toHexString() ?: "null")
).json.asJsonObject
} ?: EmptyJsonObject)
respond(true, Status.Ok, data = getTicket(uin, id))
return@getOrPost
}
}
respond(true, Status.Ok, data = ticket)
}
if (ShamrockConfig.isDev()) getOrPost("/get_all_ticket") {
val uin = fetchOrThrow("uin")
val ticketMap = mutableMapOf<Int, JsonElement>()
TicketSvc.SigType.ALL_TICKET.forEach {
ticketMap[it] = getTicket(uin, it, true)
}
respond(true, Status.Ok, data = ticketMap)
}
}

View File

@ -52,7 +52,6 @@ internal object HttpService: HttpTransmitServlet() {
GlobalEventTransmitter.onNoticeEvent { event ->
pushTo(event)
}
})
submitFlowJob(GlobalScope.launch {
GlobalEventTransmitter.onRequestEvent {

View File

@ -1,9 +1,15 @@
@file:OptIn(DelicateCoroutinesApi::class)
package moe.fuqiuluo.shamrock.remote.service.api
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.qqinterface.servlet.CardSvc
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
@ -48,7 +54,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message)
/**
* 消息 手淫器
* 消息
*/
object MessageTransmitter {
/**
@ -86,11 +92,11 @@ internal object GlobalEventTransmitter: BaseSvc() {
.ifEmpty { record.sendMemberName }
.ifEmpty { record.peerName },
card = record.sendMemberName,
role = when (record.senderUin) {
role = GroupSvc.getMemberRole(record.peerUin, record.senderUin)/*when (record.senderUin) {
GroupSvc.getOwner(record.peerUin.toString()) -> MemberRole.Owner
in GroupSvc.getAdminList(record.peerUin.toString()) -> MemberRole.Admin
else -> MemberRole.Member
},
}*/,
title = "",
level = "",
)
@ -108,7 +114,9 @@ internal object GlobalEventTransmitter: BaseSvc() {
rawMsg: String,
msgHash: Int,
postType: PostType,
tempSource: MessageTempSource = MessageTempSource.Unknown
tempSource: MessageTempSource = MessageTempSource.Unknown,
groupId: Long = Long.MIN_VALUE,
fromNick: String? = null
): Boolean {
val botUin = app.longAccountUin
var nickName = record.sendNickName
@ -142,7 +150,9 @@ internal object GlobalEventTransmitter: BaseSvc() {
title = "",
level = "",
),
tmpSource = tempSource.id
tmpSource = tempSource.id,
groupId = groupId,
fromNickName = fromNick
)
)
return true
@ -172,6 +182,8 @@ internal object GlobalEventTransmitter: BaseSvc() {
postType = postType,
messageType = MsgType.Guild,
subType = MsgSubType.Channel,
guildId = record.guildId,
channelId = record.channelId,
messageId = msgHash,
targetId = record.peerUin,
peerId = botUin,
@ -186,7 +198,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
userId = record.senderUid.toLong(),
nickname = nickName,
card = record.sendMemberName,
role = MemberRole.Member,
role = MemberRole.Member, // TODO(GUILD ROLE)
title = record.sendNickName,
level = record.roleId.toString(),
tinyId = record.senderUid
@ -554,19 +566,29 @@ internal object GlobalEventTransmitter: BaseSvc() {
@ShamrockDsl
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, MessageEvent>>) {
messageEventFlow.collect(collector)
messageEventFlow.collect {
GlobalScope.launch {
collector.emit(it)
}
}
}
@ShamrockDsl
suspend inline fun onNoticeEvent(collector: FlowCollector<NoticeEvent>) {
noticeEventFlow
.collect(collector)
noticeEventFlow.collect {
GlobalScope.launch {
collector.emit(it)
}
}
}
@ShamrockDsl
suspend inline fun onRequestEvent(collector: FlowCollector<RequestEvent>) {
requestEventFlow
.collect(collector)
requestEventFlow.collect {
GlobalScope.launch {
collector.emit(it)
}
}
}
}

View File

@ -41,6 +41,10 @@ internal abstract class WebSocketTransmitServlet(
private val sendLock = Mutex()
protected val eventReceivers: MutableList<WebSocket> = Collections.synchronizedList(mutableListOf<WebSocket>())
init {
connectionLostTimeout = 0
}
override val address: String
get() = "-"

View File

@ -76,6 +76,7 @@ internal object ShamrockConfig {
putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息
putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口
putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息
putBoolean("forbid_useless_process", intent.getBooleanExtra("forbid_useless_process", false)) // 禁用QQ生成无用进程
}
Config.defaultToken = intent.getStringExtra("token")
Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
@ -126,6 +127,10 @@ internal object ShamrockConfig {
return mmkv.getBoolean("enable_self_msg", false)
}
fun forbidUselessProcess(): Boolean {
return mmkv.getBoolean("forbid_useless_process", false)
}
fun openWebSocketClient(): Boolean {
return mmkv.getBoolean("ws_client", false)
}

View File

@ -6,5 +6,12 @@ import kotlinx.serialization.Serializable
@Serializable
internal data class Credentials(
@SerialName("token") val bkn: String = "",
@SerialName("cookies") val cookie: String = ""
@SerialName("cookies") val cookie: String = "",
@SerialName("bigdata_ticket") val bigDataTicket: BigDataTicket? = null
)
@Serializable
data class BigDataTicket(
var key: String? = null,
var sig: String? = null
)

View File

@ -52,8 +52,10 @@ internal data class MessageEvent (
@SerialName("message_type") val messageType: MsgType,
@SerialName("sub_type") val subType: MsgSubType,
@SerialName("message_id") val messageId: Int,
@SerialName("group_id") val groupId: Long = 0,
@SerialName("target_id") val targetId: Long = 0,
@SerialName("group_id") val groupId: Long = Long.MIN_VALUE,
@SerialName("guild_id") val guildId: String? = null,
@SerialName("channel_id") val channelId: String? = null,
@SerialName("target_id") val targetId: Long = Long.MIN_VALUE,
@SerialName("peer_id") val peerId: Long,
@SerialName("user_id") val userId: Long,
@SerialName("anonymous") val anonymous: Anonymous? = null,
@ -61,7 +63,8 @@ internal data class MessageEvent (
@SerialName("raw_message") val rawMessage: String,
@SerialName("font") val font: Int,
@SerialName("sender") val sender: Sender,
@SerialName("temp_source") val tmpSource: Int = -1
@SerialName("temp_source") val tmpSource: Int = Int.MIN_VALUE,
@SerialName("from_nick") val fromNickName: String? = null
)
enum class MessageTempSource(val id: Int) {
@ -86,7 +89,9 @@ internal data class Anonymous(
internal enum class MemberRole {
@SerialName("owner") Owner,
@SerialName("admin") Admin,
@SerialName("member") Member
@SerialName("member") Member,
@SerialName("stranger") Stranger,
@SerialName("unknown") Unknown
}
@Serializable

View File

@ -7,6 +7,7 @@ import com.tencent.qqnt.kernel.nativeinterface.*
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toCQCode
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
@ -44,11 +45,11 @@ internal object AioListener : IKernelMsgListener {
it.value(record)
messageLessListenerMap.remove(it.key)
}
if (record.msgSeq < 0) return
if (record.msgSeq < 0) return
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val peerId = when(record.chatType) {
val peerId = when (record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
@ -87,12 +88,14 @@ internal object AioListener : IKernelMsgListener {
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
}
if(!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(
if (!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(
record, record.elements, rawMsg, msgHash, postType
)) {
)
) {
LogCenter.log("群消息推送失败 -> 推送目标可能不存在", Level.WARN)
}
}
MsgConstant.KCHATTYPEC2C -> {
LogCenter.log("私聊消息(private = ${record.senderUin}, id = [$msgHash | ${record.msgId} | ${record.msgSeq}], msg = $rawMsg)")
ShamrockConfig.getPrivateRule()?.let { rule ->
@ -100,9 +103,10 @@ internal object AioListener : IKernelMsgListener {
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
}
if(!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
record, record.elements, rawMsg, msgHash, postType
)) {
)
) {
LogCenter.log("私聊消息推送失败 -> MessageTransmitter", Level.WARN)
}
}
@ -110,24 +114,40 @@ internal object AioListener : IKernelMsgListener {
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
if (!ShamrockConfig.allowTempSession()) return
LogCenter.log("私聊临时消息(private = ${record.senderUin}, id = $msgHash, msg = $rawMsg)")
ShamrockConfig.getPrivateRule()?.let { rule ->
if (!rule.black.isNullOrEmpty() && rule.black.contains(record.senderUin)) return
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
}
if(!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
record, record.elements, rawMsg, msgHash, tempSource = MessageTempSource.Group, postType = postType
)) {
var groupCode = 0L
var fromNick = ""
MsgSvc.getTempChatInfo(record.chatType, record.senderUid).onSuccess {
groupCode = it.groupCode.toLong()
fromNick = it.fromNick
}
LogCenter.log("私聊临时消息(private = ${record.senderUin}, groupId=$groupCode, id = $msgHash, msg = $rawMsg)")
if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
record,
record.elements,
rawMsg,
msgHash,
tempSource = MessageTempSource.Group,
postType = postType,
groupId = groupCode,
fromNick = fromNick
)
) {
LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN)
}
}
MsgConstant.KCHATTYPEGUILD -> {
LogCenter.log("频道消息(guildId = ${record.guildId}, sender=${record.senderUid}, id = [$msgHash | ${record.msgId}], msg = $rawMsg)")
if(!GlobalEventTransmitter.MessageTransmitter
.transGuildMessage(record, record.elements, rawMsg, msgHash, postType = postType)
) {
if (!GlobalEventTransmitter.MessageTransmitter
.transGuildMessage(record, record.elements, rawMsg, msgHash, postType = postType)
) {
LogCenter.log("频道消息推送失败 -> MessageTransmitter", Level.WARN)
}
}
@ -150,7 +170,7 @@ internal object AioListener : IKernelMsgListener {
try {
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val peerId = when(record.chatType) {
val peerId = when (record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
@ -174,8 +194,6 @@ internal object AioListener : IKernelMsgListener {
override fun onMsgInfoListUpdate(msgList: ArrayList<MsgRecord>?) {
msgList?.forEach { record ->
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return@forEach// TODO: 频道消息暂不处理
if (record.sendStatus == MsgConstant.KSENDSTATUSFAILED
|| record.sendStatus == MsgConstant.KSENDSTATUSSENDING
) {
@ -184,7 +202,7 @@ internal object AioListener : IKernelMsgListener {
GlobalScope.launch {
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val peerId = when(record.chatType) {
val peerId = when (record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString()
}
@ -449,6 +467,7 @@ internal object AioListener : IKernelMsgListener {
override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) {
}
override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) {
}

View File

@ -534,12 +534,11 @@ internal object PrimitiveListener {
val groupCode = event.groupCode
val applierUid = event.applierUid
val reason = event.applyMsg ?: ""
val applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
var applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
if (applier == getLongUin()) {
return
}
val msgSeq = contentHead.msgSeq
LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq")
val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(5, 2)
@ -548,10 +547,14 @@ internal object PrimitiveListener {
it.msg_time.get() == time && it.msg?.group_code?.get() == groupCode
}
val seq = req?.msg_seq?.get() ?: time
if (applier == 0L) {
applier = req?.req_uin?.get() ?: 0L
}
"$seq;$groupCode;$applier"
} catch (err: Throwable) {
"$time;$groupCode;$applier"
}
LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq")
if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, applierUid, reason, groupCode, flag, RequestSubType.Add)
) {
@ -562,7 +565,7 @@ internal object PrimitiveListener {
val event = ProtoBuf.decodeFromByteArray<GroupInvitedApplyEvent>(richMsg.rawBuffer!!)
val groupCode = event.applyInfo?.groupCode ?: return
val applierUid = event.applyInfo?.applierUid ?: return
val applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
var applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
if (applier == getLongUin()) {
return
}
@ -570,7 +573,6 @@ internal object PrimitiveListener {
// todo
return
}
LogCenter.log("邀请入群申请($groupCode): $applier")
val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(5, 2)
@ -579,10 +581,14 @@ internal object PrimitiveListener {
it.msg_time.get() == time
}
val seq = req?.msg_seq?.get() ?: time
if (applier == 0L) {
applier = req?.req_uin?.get() ?: 0L
}
"$seq;$groupCode;$applier"
} catch (err: Throwable) {
"$time;$groupCode;$applier"
}
LogCenter.log("邀请入群申请($groupCode): $applier")
if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, applierUid, "", groupCode, flag, RequestSubType.Add)
) {

View File

@ -1,10 +1,14 @@
package moe.fuqiuluo.shamrock.xposed
import android.content.Context
import android.os.Process
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage
import de.robv.android.xposed.XposedBridge.log
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
import moe.fuqiuluo.shamrock.xposed.loader.KeepAlive
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
@ -15,12 +19,14 @@ import moe.fuqiuluo.shamrock.xposed.hooks.runFirstActions
import mqq.app.MobileQQ
import java.lang.reflect.Field
import java.lang.reflect.Modifier
import kotlin.system.exitProcess
internal const val PACKAGE_NAME_QQ = "com.tencent.mobileqq"
internal const val PACKAGE_NAME_QQ_INTERNATIONAL = "com.tencent.mobileqqi"
internal const val PACKAGE_NAME_QQ_LITE = "com.tencent.qqlite"
internal const val PACKAGE_NAME_TIM = "com.tencent.tim"
private const val PACKAGE_NAME_QQ = "com.tencent.mobileqq"
private const val PACKAGE_NAME_QQ_INTERNATIONAL = "com.tencent.mobileqqi"
private const val PACKAGE_NAME_QQ_LITE = "com.tencent.qqlite"
private const val PACKAGE_NAME_TIM = "com.tencent.tim"
private val uselessProcess = listOf("peak", "tool", "mini", "qzone")
internal class XposedEntry: IXposedHookLoadPackage {
companion object {
@ -121,9 +127,7 @@ internal class XposedEntry: IXposedHookLoadPackage {
System.setProperty("qxbot_flag", "1")
} else return
log("Process Name = " + MobileQQ.getMobileQQ().qqProcessName)
PlatformUtils.isTim()
val processName = MobileQQ.getMobileQQ().qqProcessName
// MSG LISTENER 进程运行在主进程
// API 也应该开放在主进程
@ -134,6 +138,22 @@ internal class XposedEntry: IXposedHookLoadPackage {
MMKVFetcher.initMMKV(ctx)
}
runCatching {
if (ShamrockConfig.forbidUselessProcess()) {
if(uselessProcess.any {
processName.contains(it, ignoreCase = true)
}) {
log("[Shamrock] Useless process detected: $processName, exit.")
Process.killProcess(Process.myPid())
exitProcess(0)
}
} else {
log("[Shamrock] Useless process detection is disabled.")
}
}
log("Process Name = $processName")
runFirstActions(ctx)
}
}

View File

@ -2,24 +2,17 @@ package moe.fuqiuluo.shamrock.xposed.helper
import com.tencent.qqnt.kernel.api.IKernelService
import com.tencent.qqnt.kernel.api.impl.MsgService
import com.tencent.qqnt.kernel.nativeinterface.IKernelGroupService
import com.tencent.qqnt.kernel.nativeinterface.IKernelGuildService
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback
import com.tencent.qqnt.kernel.nativeinterface.IQQNTWrapperSession
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XposedBridge
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.service.PacketReceiver
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
import moe.fuqiuluo.shamrock.remote.service.listener.GroupEventListener
import moe.fuqiuluo.shamrock.remote.service.listener.KernelGuildListener
import moe.fuqiuluo.shamrock.remote.service.listener.PrimitiveListener
import moe.fuqiuluo.shamrock.tools.hookMethod
import moe.fuqiuluo.shamrock.utils.PlatformUtils
import kotlin.reflect.jvm.javaMethod
internal object NTServiceFetcher {
private lateinit var iKernelService: IKernelService
@ -30,7 +23,7 @@ internal object NTServiceFetcher {
lock.withLock {
val msgService = service.msgService ?: return
val sessionService = service.wrapperSession ?: return
val groupService = sessionService.groupService ?: return
//val groupService = sessionService.groupService ?: return
val curHash = service.hashCode() + msgService.hashCode()
if (isInitForNt(curHash)) return
@ -43,7 +36,7 @@ internal object NTServiceFetcher {
this.iKernelService = service
initNTKernelListener(msgService, groupService)
initNTKernelListener(msgService)
antiBackgroundMode(sessionService)
//hookGuildListener(sessionService)
}
@ -66,7 +59,7 @@ internal object NTServiceFetcher {
return hash == curKernelHash
}
private fun initNTKernelListener(msgService: MsgService, groupService: IKernelGroupService) {
private fun initNTKernelListener(msgService: MsgService) {
if (!PlatformUtils.isMainProcess()) return
try {

View File

@ -1,12 +1,50 @@
@file:Suppress("UNUSED_VARIABLE", "LocalVariableName")
package moe.fuqiuluo.shamrock.xposed.hooks
import android.content.Context
import com.tencent.mobileqq.perf.block.BinderMethodProxy
import com.tencent.mobileqq.qmmkv.MMKVOptionEntity
import com.tencent.mobileqq.qroute.QRoute
import com.tencent.qqnt.aio.api.IAIOPicDownloaderProvider
import de.robv.android.xposed.XposedBridge
import epic.EIPCClient
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.beforeHook
import moe.fuqiuluo.shamrock.tools.hookMethod
import moe.fuqiuluo.shamrock.tools.toInnerValuesString
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
import moe.fuqiuluo.symbols.Process
import moe.fuqiuluo.symbols.XposedHook
import java.lang.reflect.Modifier
@XposedHook(priority = -1)
@XposedHook(priority = -1, process = Process.ALL)
internal class HookForDebug: IAction {
override fun invoke(ctx: Context) {
/*
//val LibraDownloader = QRoute.api(IAIOPicDownloaderProvider::class.java).provideDownloader().javaClass
//LibraDownloader.hookMethod("downLoad").before {
// val option = it.args[0]
// LogCenter.log("LibraDownloader.downLoad(${option.toInnerValuesString()})")
//}
}
}
/*val NtDnsManager = LuoClassloader.load("com.tencent.qqnt.dns.NtDnsManager")!!
val NtDnsInternal = NtDnsManager.declaredMethods.first {
!Modifier.isStatic(it.modifiers) && it.parameterCount == 0
}.returnType
XposedBridge.hookMethod(NtDnsInternal.declaredMethods.first {
it.parameterCount == 2
&& it.parameterTypes[0] == String::class.java
&& it.parameterTypes[1] == Int::class.java
&& it.returnType == ArrayList::class.java
}, beforeHook {
val domain = it.args[0] as String
val type = it.args[1] as Int
LogCenter.log("NtDnsManager: reqDomain2IpList($domain, $type)")
LogCenter.log(Exception().stackTraceToString())
})*/
/*
val httpEngineService = AppRuntimeFetcher.appRuntime
.getRuntimeService(IHttpEngineService::class.java, "all")
httpEngineService.javaClass.hookMethod("sendReq").before {
@ -19,7 +57,17 @@ internal class HookForDebug: IAction {
LogCenter.log("请求地址: ${req.mReqUrl}")
LogCenter.log("请求: ${req.toInnerValuesString(NetReq::class.java)}")
}
}
BinderMethodProxy::class.java.hookMethod("callServer").before {
val action = it.args[2] as String
if (action == "reqDomain2IpList") {
LogCenter.log(Exception().stackTraceToString())
}
}
EIPCClient::class.java.hookMethod("callServer").before {
val module = it.args[0] as String
val action = it.args[1] as String
if (action == "reqDomain2IpList" || module.contains("dns", ignoreCase = true)) {
LogCenter.log(Exception().stackTraceToString())
}
}*/
}
}