diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b33864a..8b9920f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,6 +18,8 @@ android:supportsRtl="true" android:theme="@style/Theme.Shamrock" android:zygotePreloadName="@string/app_name" + android:multiArch="true" + android:extractNativeLibs="true" tools:targetApi="31"> +#include +#include +#include +#include +#include +#include +#include +#include +#include "lsposed.h" +#include "jnihelper.h" + +static HookFunType hook_function = nullptr; + +static std::vector qemu_detect_props = { + "init.svc.qemu-props", "qemu.hw.mainkeys", "qemu.sf.fake_camera", "ro.kernel.android.qemud", + "qemu.sf.lcd_density", "init.svc.qemud", "ro.kernel.qemu", + "libc.debug.malloc" +}; + +int (*backup_system_property_get)(const char *name, char *value); + +int fake_system_property_get(const char *name, char *value) { + for (auto &prop: qemu_detect_props) { + if (strstr(name, prop.c_str())) { + LOGI("[Shamrock] bypass qemu detection"); + value[0] = 0; + return 0; + } + } + + if (strstr(name, "ro.debuggable") + || strstr(name, "ro.kernel.qemu.gles") + || strstr(name, "debug.atrace.tags.enableflags")) { + strcpy(value, "0"); + return 1; + } + + if (strstr(name, "ro.product.cpu.abilist")) { + int len = backup_system_property_get(name, value); + if (len > 0) { + if (strstr(value, "x86")) { + strcpy(value, "arm64-v8a,armeabi-v7a,armeabi"); + return 29; + } + } + return len; + } + + if (strstr(name, "ro.hardware")) { + int len = backup_system_property_get(name, value); + if (len > 0) { + if (strstr(value, "generic") + || strstr(value, "unknown") + || strstr(value, "emulator") + || strstr(value, "vbox") + || strstr(value, "genymotion") + || strstr(value, "goldfish")) { + strcpy(value, "qcom"); + return 4; + } + } + return len; + } + + //LOGI("[Shamrock] fake_system_property_get(%s)", name); + return backup_system_property_get(name, value); +} + +void on_library_loaded(const char *name, void *handle) { + auto libraryName = std::string(name); + if (libraryName.ends_with("libc.so") || libraryName.ends_with("libfekit.so")) { + void *target = dlsym(handle, "__system_property_get"); + if (target != nullptr) { + hook_function(target, (void *)fake_system_property_get, (void **) &backup_system_property_get); + } else { + LOGE("[Shamrock] failed to hook __system_property_get"); + } + } +} + +extern "C" [[gnu::visibility("default")]] [[gnu::used]] +jint JNI_OnLoad(JavaVM *jvm, void*) { + JNIHelper::initJavaVM(jvm); + int attach = 0; + JNIEnv *env = JNIHelper::getJNIEnv(&attach); + + // do something + LOGI("[Shamrock] JNI_OnLoad NativeModule Init: %p", env); + + if (attach == 1) { + JNIHelper::delJNIEnv(); + } + + //hook_function((void *)env->functions->FindClass, (void *)fake_FindClass, (void **)&backup_FindClass); + return JNI_VERSION_1_6; +} + +extern "C" [[gnu::visibility("default")]] [[gnu::used]] +NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) { + hook_function = entries->hook_func; + LOGI("[Shamrock] LSPosed NativeModule Init: %p", hook_function); + return on_library_loaded; +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_moe_fuqiuluo_shamrock_xposed_XposedEntry_00024Companion_injected(JNIEnv *env, jobject thiz) { + return hook_function != nullptr; +} + +extern "C" +JNIEXPORT jboolean JNICALL +Java_moe_fuqiuluo_shamrock_xposed_XposedEntry_00024Companion_hasEnv(JNIEnv *env, jobject thiz) { + return JNIHelper::global_jvm != nullptr; +} \ No newline at end of file diff --git a/xposed/src/main/cpp/jnihelper.h b/xposed/src/main/cpp/jnihelper.h new file mode 100644 index 0000000..a45fef6 --- /dev/null +++ b/xposed/src/main/cpp/jnihelper.h @@ -0,0 +1,40 @@ +#ifndef SHAMROCK_JNIHELPER_H +#define SHAMROCK_JNIHELPER_H + +#include "android/log.h" + +namespace JNIHelper { + static JavaVM *global_jvm = nullptr; + + void initJavaVM(JavaVM *jvm) { + global_jvm = jvm; + } + + JNIEnv *getJNIEnv(int *attach) { + if (global_jvm == NULL) return NULL; + + *attach = 0; + JNIEnv *jni_env = NULL; + + int status = global_jvm->GetEnv((void **)&jni_env, JNI_VERSION_1_6); + + if (status == JNI_EDETACHED || jni_env == NULL) { + status = global_jvm->AttachCurrentThread(&jni_env, NULL); + if (status < 0) { + jni_env = NULL; + } else { + *attach = 1; + } + } + return jni_env; + } + + jint delJNIEnv() { + if (global_jvm == nullptr) return 0; + return global_jvm->DetachCurrentThread(); + } +} + + + +#endif //SHAMROCK_JNIHELPER_H diff --git a/xposed/src/main/cpp/lsposed.h b/xposed/src/main/cpp/lsposed.h new file mode 100644 index 0000000..2be13b7 --- /dev/null +++ b/xposed/src/main/cpp/lsposed.h @@ -0,0 +1,27 @@ +#ifndef SHAMROCK_LSPOSED_H +#define SHAMROCK_LSPOSED_H + +#include "stdint.h" + +#define TAG "LSPosed-Bridge" +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG ,__VA_ARGS__) +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG ,__VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,TAG ,__VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG ,__VA_ARGS__) +#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,TAG ,__VA_ARGS__) + +typedef int (*HookFunType)(void *func, void *replace, void **backup); + +typedef int (*UnhookFunType)(void *func); + +typedef void (*NativeOnModuleLoaded)(const char *name, void *handle); + +typedef struct { + uint32_t version; + HookFunType hook_func; + UnhookFunType unhook_func; +} NativeAPIEntries; + +typedef NativeOnModuleLoaded (*NativeInit)(const NativeAPIEntries *entries); + +#endif //SHAMROCK_LSPOSED_H diff --git a/xposed/src/main/cpp/xposed.cpp b/xposed/src/main/cpp/xposed.cpp deleted file mode 100644 index e99001e..0000000 --- a/xposed/src/main/cpp/xposed.cpp +++ /dev/null @@ -1,5 +0,0 @@ -#include -#include -#include -#include - diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt index 43b1c96..944893a 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt @@ -318,7 +318,7 @@ internal object GroupSvc: BaseSvc() { fun getOwner(groupId: String): Long { val groupInfo = getGroupInfo(groupId) - return groupInfo.troopowneruin.toLong() + return groupInfo.troopowneruin?.toLong() ?: 0 } fun isOwner(groupId: String): Boolean { diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageMaker.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageMaker.kt index a86814d..189788e 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageMaker.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/MessageMaker.kt @@ -96,9 +96,21 @@ internal object MessageMaker { "touch" to MessageMaker::createTouchElem, "weather" to MessageMaker::createWeatherElem, "json" to MessageMaker::createJsonElem, + //"node" to MessageMaker::createNodeElem, //"multi_msg" to MessageMaker::createLongMsgStruct, ) +// private suspend fun createNodeElem( +// chatType: Int, +// msgId: Long, +// peerId: String, +// data: JsonObject +// ): Result { +// data.checkAndThrow("data") +// SendForwardMessage(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin(), data["content"].asJsonArray) +// +// } + private suspend fun createJsonElem( chatType: Int, msgId: Long, diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LogCenter.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LogCenter.kt index 55e8dbc..d82b26a 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LogCenter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/LogCenter.kt @@ -51,8 +51,29 @@ internal object LogCenter { private val format = SimpleDateFormat("[HH:mm:ss] ") - fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) = - log({ string }, level, toast) + fun log(string: String, level: Level = Level.INFO, toast: Boolean = false) { + if (!ShamrockConfig.isDebug() && level == Level.DEBUG) { + return + } + + if (toast) { + MobileQQ.getContext().toast(string) + } + // 把日志广播到主进程 + GlobalScope.launch(Dispatchers.Default) { + DataRequester.request("send_message", bodyBuilder = { + put("string", string) + put("level", level.id) + }) + } + + if (!LogFile.exists()) { + LogFile.createNewFile() + } + val format = "%s%s %s\n".format(format.format(Date()), level.name, string) + + LogFile.appendText(format) + } fun log( string: () -> String, diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt index 62ed859..9b12d6f 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt @@ -74,7 +74,7 @@ internal object MessageHelper { val nonMsg: Boolean = message.isEmpty() return if (!nonMsg) { val service = QRoute.api(IMsgService::class.java) - if(callback is MsgSvc.MessageCallback) { + if (callback is MsgSvc.MessageCallback) { callback.msgHash = uniseq.first } @@ -107,7 +107,7 @@ internal object MessageHelper { val nonMsg: Boolean = message.isEmpty() return if (!nonMsg) { val service = QRoute.api(IMsgService::class.java) - if(callback is MsgSvc.MessageCallback) { + if (callback is MsgSvc.MessageCallback) { callback.msgHash = uniseq.first } @@ -132,7 +132,7 @@ internal object MessageHelper { val nonMsg: Boolean = message.isEmpty() return if (!nonMsg) { val service = QRoute.api(IMsgService::class.java) - if(callback is MsgSvc.MessageCallback) { + if (callback is MsgSvc.MessageCallback) { callback.msgHash = uniseq.first } @@ -156,7 +156,7 @@ internal object MessageHelper { } fun obtainMessageTypeByDetailType(detailType: String): Int { - return when(detailType) { + return when (detailType) { "troop", "group" -> MsgConstant.KCHATTYPEGROUP "private" -> MsgConstant.KCHATTYPEC2C "less" -> MsgConstant.KCHATTYPETEMPC2CFROMUNKNOWN @@ -166,7 +166,7 @@ internal object MessageHelper { } fun obtainDetailTypeByMsgType(msgType: Int): String { - return when(msgType) { + return when (msgType) { MsgConstant.KCHATTYPEGROUP -> "group" MsgConstant.KCHATTYPEC2C -> "private" MsgConstant.KCHATTYPEGUILD -> "guild" @@ -180,9 +180,9 @@ internal object MessageHelper { var hasActionMsg = false messageList.forEach { val msg = it.jsonObject - try { - val maker = MessageMaker[msg["type"].asString] - if (maker != null) { + val maker = MessageMaker[msg["type"].asString] + if (maker != null) { + try { val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject maker(chatType, msgId, targetUin, data).onSuccess { msgElem -> msgList.add(msgElem) @@ -193,18 +193,19 @@ internal object MessageHelper { hasActionMsg = true } } - } else { - LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR) + } catch (e: Throwable) { + LogCenter.log(e.stackTraceToString(), Level.ERROR) } - } catch (e: Throwable) { - LogCenter.log(e.stackTraceToString(), Level.ERROR) + } else { + LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR) + return false to arrayListOf() } } return hasActionMsg to msgList } fun generateMsgIdHash(chatType: Int, msgId: Long): Int { - val key = when (chatType) { + val key = when (chatType) { MsgConstant.KCHATTYPEGROUP -> "grp$msgId" MsgConstant.KCHATTYPEC2C -> "c2c$msgId" MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> "tmpgrp$msgId" diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/ActionManager.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/ActionManager.kt index 075a860..bdbe629 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/ActionManager.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/ActionManager.kt @@ -34,8 +34,8 @@ internal object ActionManager { GetGroupSystemMsg, GetProhibitedMemberList, GetEssenceMessageList, GetGroupNotice, SendGroupNotice, // MSG ACTIONS - SendMessage, DeleteMessage, GetMsg, GetForwardMsg, SendGroupForwardMsg, SendGroupMessage, SendPrivateMessage, - ClearMsgs, GetHistoryMsg, GetGroupMsgHistory, SendPrivateForwardMsg, + SendMessage, DeleteMessage, GetMsg, GetForwardMsg, SendPrivateForwardMessage, SendGroupMessage, SendPrivateMessage, + ClearMsgs, GetHistoryMsg, GetGroupMsgHistory, SendGroupForwardMessage, // RESOURCE ACTION GetRecord, GetImage, UploadGroupFile, CreateGroupFileFolder, DeleteGroupFolder, diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt new file mode 100644 index 0000000..8401921 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMessage.kt @@ -0,0 +1,169 @@ +package moe.fuqiuluo.shamrock.remote.action.handlers + +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import moe.fuqiuluo.qqinterface.servlet.MsgSvc +import moe.fuqiuluo.qqinterface.servlet.TicketSvc +import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.helper.MessageHelper +import moe.fuqiuluo.shamrock.helper.ParamsException +import moe.fuqiuluo.shamrock.remote.action.ActionSession +import moe.fuqiuluo.shamrock.remote.action.IActionHandler +import moe.fuqiuluo.shamrock.remote.service.data.ForwardMessageResult +import moe.fuqiuluo.shamrock.tools.* +import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +sealed interface ForwardMsgNode { + class MessageIdNode( + val id: Int + ) : ForwardMsgNode + + open class MessageNode( + val name: String, + val content: JsonElement? + ) : ForwardMsgNode + + object EmptyNode : MessageNode("", null) +} + +internal object SendForwardMessage : IActionHandler() { + override suspend fun internalHandle(session: ActionSession): String { + val detailType = session.getStringOrNull("detail_type") ?: session.getStringOrNull("message_type") + try { + val chatType = detailType?.let { + MessageHelper.obtainMessageTypeByDetailType(it) + } ?: run { + if (session.has("user_id")) { + MsgConstant.KCHATTYPEC2C + } else if (session.has("group_id")) { + MsgConstant.KCHATTYPEGROUP + } else { + return noParam("detail_type/message_type", session.echo) + } + } + val peerId = when (chatType) { + MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam( + "group_id", + session.echo + ) + + MsgConstant.KCHATTYPEC2C -> session.getStringOrNull("user_id") ?: return noParam( + "user_id", + session.echo + ) + + else -> error("unknown chat type: $chatType") + } + if (session.isArray("messages")) { + val messages = session.getArray("messages") + invoke(chatType, peerId, messages, echo = session.echo) + } + return logic("未知格式合并转发消息", session.echo) + } catch (e: ParamsException) { + return noParam(e.message!!, session.echo) + } catch (e: Throwable) { + return logic(e.message ?: e.toString(), session.echo) + } + } + + suspend operator fun invoke( + chatType: Int, + peerId: String, + message: JsonArray, + echo: JsonElement = EmptyJsonString + ): String { + kotlin.runCatching { + val kernelService = NTServiceFetcher.kernelService + val sessionService = kernelService.wrapperSession + val msgService = sessionService.msgService + val selfUin = TicketSvc.getUin() + + val msgs = message.map { + if (it.asJsonObject["type"].asStringOrNull != "node") return@map ForwardMsgNode.EmptyNode // 过滤非node类型消息段 + it.asJsonObject["data"].asJsonObject.let { data -> + if (data.containsKey("content")) { + data["content"].asJsonArray.forEach { msg -> + if (msg.asJsonObject["type"].asStringOrNull == "node") { + LogCenter.log("合并转发消息不支持嵌套", Level.ERROR) + return@map ForwardMsgNode.EmptyNode + } + } + ForwardMsgNode.MessageNode( + name = data["name"].asStringOrNull ?: "", + content = data["content"] + ) + } + else ForwardMsgNode.MessageIdNode(data["id"].asInt) + } + }.map { + if (it is ForwardMsgNode.MessageIdNode) { + val recordResult = MsgSvc.getMsg(it.id) + if (!recordResult.isFailure) { + ForwardMsgNode.EmptyNode + } else { + val record = recordResult.getOrThrow() + ForwardMsgNode.MessageNode( + name = record.sendMemberName + .ifBlank { record.sendNickName } + .ifBlank { record.sendRemarkName } + .ifBlank { record.peerName }, + content = record.toSegments().map { segment -> + segment.toJson() + }.json + ) + } + } else { + it as ForwardMsgNode.MessageNode + } + }.filter { + it.content != null + } + + val multiNodes = msgs.map { node -> + suspendCoroutine { + GlobalScope.launch { + var msgId: Long = 0 + msgId = MessageHelper.sendMessageWithMsgId(MsgConstant.KCHATTYPEC2C, selfUin, node.content!!.let { msg -> + if (msg is JsonArray) msg else MessageHelper.decodeCQCode(msg.asString) + }, + { code, why -> + if (code != 0) { + error("合并转发消息节点消息发送失败:$code($why)") + } + it.resume(node.name to msgId) + }).first + } + } + } + + val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin) + val to = MessageHelper.generateContact(chatType, peerId) + + val uniseq = MessageHelper.generateMsgId(chatType) + msgService.multiForwardMsg(ArrayList().apply { + multiNodes.forEach { add(MultiMsgInfo(it.second, it.first)) } + }.also { it.reverse() }, from, to, MsgSvc.MessageCallback(peerId, uniseq.first)) + + return ok( + ForwardMessageResult( + msgId = uniseq.first, + forwardId = "" + ), echo = echo) + }.onFailure { + return error("error: $it", echo) + } + return logic("合并转发消息失败(unknown error)", echo) + } + + override val requiredParams: Array = arrayOf("message") + + override fun path(): String = "send_forward_msg" +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMsg.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMsg.kt deleted file mode 100644 index 33439ad..0000000 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendForwardMsg.kt +++ /dev/null @@ -1,233 +0,0 @@ -@file:OptIn(DelicateCoroutinesApi::class) - -package moe.fuqiuluo.shamrock.remote.action.handlers - -import com.tencent.qqnt.kernel.nativeinterface.MsgConstant -import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import kotlinx.serialization.json.JsonArray -import kotlinx.serialization.json.JsonElement -import moe.fuqiuluo.qqinterface.servlet.MsgSvc -import moe.fuqiuluo.shamrock.remote.action.ActionSession -import moe.fuqiuluo.shamrock.remote.action.IActionHandler -import moe.fuqiuluo.qqinterface.servlet.TicketSvc -import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments -import moe.fuqiuluo.shamrock.helper.Level -import moe.fuqiuluo.shamrock.helper.LogCenter -import moe.fuqiuluo.shamrock.helper.MessageHelper -import moe.fuqiuluo.shamrock.tools.EmptyJsonObject -import moe.fuqiuluo.shamrock.tools.EmptyJsonString -import moe.fuqiuluo.shamrock.tools.asInt -import moe.fuqiuluo.shamrock.tools.asJsonObject -import moe.fuqiuluo.shamrock.tools.asString -import moe.fuqiuluo.shamrock.tools.asStringOrNull -import moe.fuqiuluo.shamrock.tools.json -import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -/** - * 合并转发消息节点数据类 - */ -sealed interface ForwardMsgNode { - class MessageIdNode( - val id: Int - ): ForwardMsgNode - - open class MessageNode( - val name: String, - val content: JsonElement? - ): ForwardMsgNode - - object EmptyNode: MessageNode("", null) -} - -/** - * 私聊合并转发 - */ -internal object SendPrivateForwardMsg: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getString("user_id") - if (session.isArray("messages")) { - val messages = session.getArray("messages") - return invoke(messages, groupId, session.echo) - } - return logic("未知格式合并转发消息", session.echo) - } - - suspend operator fun invoke( - message: JsonArray, - userId: String, - echo: JsonElement = EmptyJsonString - ): String { - kotlin.runCatching { - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - val msgService = sessionService.msgService - val selfUin = TicketSvc.getUin() - - val msgs = message.map { - if (it.asJsonObject["type"].asStringOrNull != "node") return@map ForwardMsgNode.EmptyNode // 过滤非node类型消息段 - it.asJsonObject["data"].asJsonObject.let { data -> - if (data.containsKey("content")) - ForwardMsgNode.MessageNode( - name = data["name"].asStringOrNull ?: "", - content = data["content"] - ) - else ForwardMsgNode.MessageIdNode(data["id"].asInt) - } - }.map { - if (it is ForwardMsgNode.MessageIdNode) { - val recordResult = MsgSvc.getMsg(it.id) - if (recordResult.isFailure) { - ForwardMsgNode.EmptyNode - } else { - val record = recordResult.getOrThrow() - ForwardMsgNode.MessageNode( - name = record.sendMemberName - .ifBlank { record.sendNickName } - .ifBlank { record.sendRemarkName } - .ifBlank { record.peerName }, - content = record.toSegments().map { segment -> - segment.toJson() - }.json - ) - } - } else { - it as ForwardMsgNode.MessageNode - } - }.filter { - it.content != null - } - - val multiNodes = msgs.map { node -> - suspendCoroutine { - GlobalScope.launch { - var msgId: Long = 0 - msgId = MessageHelper.sendMessageWithMsgId(MsgConstant.KCHATTYPEC2C, selfUin, node.content!!.let { msg -> - if (msg is JsonArray) msg else MessageHelper.decodeCQCode(msg.asString) - }, { code, why -> - if (code != 0) { - LogCenter.log("合并转发消息节点消息发送失败:$code($why)", Level.WARN) - } - it.resume(node.name to msgId) - }).first - } - } - } - - val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin) - val to = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, userId) - msgService.multiForwardMsg(ArrayList().apply { - multiNodes.forEach { add(MultiMsgInfo(it.second, it.first)) } - }.also { it.reverse() }, from, to) { code, why -> - if (code != 0) - LogCenter.log("合并转发消息:$code($why)", Level.WARN) - } - return ok(data = EmptyJsonObject, echo = echo) - }.onFailure { - return error("error: $it", echo) - } - return logic("合并转发消息失败(unknown error)", echo) - } - - override val requiredParams: Array = arrayOf("user_id") - - override fun path(): String = "send_private_forward_msg" -} - -/** - * 群聊合并转发 - */ -internal object SendGroupForwardMsg: IActionHandler() { - override suspend fun internalHandle(session: ActionSession): String { - val groupId = session.getString("group_id") - if (session.isArray("messages")) { - val messages = session.getArray("messages") - return invoke(messages, groupId, session.echo) - } - return logic("未知格式合并转发消息", session.echo) - } - - suspend operator fun invoke( - message: JsonArray, - groupId: String, - echo: JsonElement = EmptyJsonString - ): String { - kotlin.runCatching { - val kernelService = NTServiceFetcher.kernelService - val sessionService = kernelService.wrapperSession - val msgService = sessionService.msgService - val selfUin = TicketSvc.getUin() - - val msgs = message.map { - if (it.asJsonObject["type"].asStringOrNull != "node") return@map ForwardMsgNode.EmptyNode // 过滤非node类型消息段 - it.asJsonObject["data"].asJsonObject.let { data -> - if (data.containsKey("content")) - ForwardMsgNode.MessageNode( - name = data["name"].asStringOrNull ?: "", - content = data["content"] - ) - else ForwardMsgNode.MessageIdNode(data["id"].asInt) - } - }.map { - if (it is ForwardMsgNode.MessageIdNode) { - val recordResult = MsgSvc.getMsg(it.id) - if (recordResult.isFailure) { - ForwardMsgNode.EmptyNode - } else { - val record = recordResult.getOrThrow() - ForwardMsgNode.MessageNode( - name = record.sendMemberName - .ifBlank { record.sendNickName } - .ifBlank { record.sendRemarkName } - .ifBlank { record.peerName }, - content = record.toSegments().map { segment -> - segment.toJson() - }.json - ) - } - } else { - it as ForwardMsgNode.MessageNode - } - }.filter { - it.content != null - } - - val multiNodes = msgs.map { node -> - suspendCoroutine { - GlobalScope.launch { - var msgId: Long = 0 - msgId = MessageHelper.sendMessageWithMsgId(MsgConstant.KCHATTYPEC2C, selfUin, node.content!!.let { msg -> - if (msg is JsonArray) msg else MessageHelper.decodeCQCode(msg.asString) - }, { code, why -> - if (code != 0) { - LogCenter.log("合并转发消息节点消息发送失败:$code($why)", Level.WARN) - } - it.resume(node.name to msgId) - }).first - } - } - } - - val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin) - val to = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId) - msgService.multiForwardMsg(ArrayList().apply { - multiNodes.forEach { add(MultiMsgInfo(it.second, it.first)) } - }.also { it.reverse() }, from, to) { code, why -> - if (code != 0) - LogCenter.log("合并转发消息:$code($why)", Level.WARN) - } - return ok(data = EmptyJsonObject, echo = echo) - }.onFailure { - return error("error: $it", echo) - } - return logic("合并转发消息失败(unknown error)", echo) - } - - override val requiredParams: Array = arrayOf("group_id") - - override fun path(): String = "send_group_forward_msg" -} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt new file mode 100644 index 0000000..87452dc --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendGroupForwardMessage.kt @@ -0,0 +1,21 @@ +package moe.fuqiuluo.shamrock.remote.action.handlers; + +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import moe.fuqiuluo.shamrock.remote.action.ActionSession +import moe.fuqiuluo.shamrock.remote.action.IActionHandler + +internal object SendGroupForwardMessage: IActionHandler() { + override suspend fun internalHandle(session: ActionSession): String { + val groupId = session.getString("group_id") + return if (session.isArray("messages")) { + val messages = session.getArray("messages") + SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId, messages, session.echo) + } else { + logic("未知格式合并转发消息", session.echo) + } + } + + override val requiredParams: Array = arrayOf("messages", "group_id") + + override fun path(): String = "send_group_forward_msg" +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt new file mode 100644 index 0000000..4468ba2 --- /dev/null +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateForwardMessage.kt @@ -0,0 +1,21 @@ +package moe.fuqiuluo.shamrock.remote.action.handlers; + +import com.tencent.qqnt.kernel.nativeinterface.MsgConstant +import moe.fuqiuluo.shamrock.remote.action.ActionSession +import moe.fuqiuluo.shamrock.remote.action.IActionHandler + +internal object SendPrivateForwardMessage : IActionHandler() { + override suspend fun internalHandle(session: ActionSession): String { + val userId = session.getString("user_id") + return if (session.isArray("messages")) { + val messages = session.getArray("messages") + SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId, messages, session.echo) + } else { + logic("未知格式合并转发消息", session.echo) + } + } + + override val requiredParams: Array = arrayOf("messages", "user_id") + + override fun path(): String = "send_private_forward_msg" +} \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateMessage.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateMessage.kt index 823bfb9..4f20d72 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateMessage.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/action/handlers/SendPrivateMessage.kt @@ -9,12 +9,12 @@ internal object SendPrivateMessage: IActionHandler() { override suspend fun internalHandle(session: ActionSession): String { val userId = session.getString("user_id") val groupId = session.getStringOrNull("group_id") - val chatTYpe = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP + val chatType = if (groupId == null) MsgConstant.KCHATTYPEC2C else MsgConstant.KCHATTYPETEMPC2CFROMGROUP return if (session.isString("message")) { val autoEscape = session.getBooleanOrDefault("auto_escape", false) val message = session.getString("message") SendMessage.invoke( - chatType = chatTYpe, + chatType = chatType, peerId = userId, message = message, autoEscape = autoEscape, @@ -24,7 +24,7 @@ internal object SendPrivateMessage: IActionHandler() { } else if (session.isArray("message")) { val message = session.getArray("message") SendMessage( - chatType = chatTYpe, + chatType = chatType, peerId = userId, message = message, echo = session.echo, @@ -33,7 +33,7 @@ internal object SendPrivateMessage: IActionHandler() { } else { val message = session.getObject("message") SendMessage( - chatType = chatTYpe, + chatType = chatType, peerId = userId, message = listOf( message ).jsonArray, echo = session.echo, diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt index c9ed7eb..0e32a18 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/api/MessageAction.kt @@ -33,20 +33,22 @@ fun Routing.messageAction() { post { val groupId = fetchPostOrNull("group_id") val messages = fetchPostJsonArray("messages") - call.respondText(SendGroupForwardMsg(messages, groupId ?: ""), ContentType.Application.Json) + call.respondText(SendForwardMessage(MsgConstant.KCHATTYPEGROUP, groupId ?: "", messages), ContentType.Application.Json) } get { respond(false, Status.InternalHandlerError, "Not support GET method") } } - post("/send_group_forward_msg") { - } - - post("/send_private_forward_msg") { - val userId = fetchPostOrNull("user_id") - val messages = fetchPostJsonArray("messages") - call.respondText(SendPrivateForwardMsg(messages, userId ?: ""), ContentType.Application.Json) + route("/send_private_forward_msg") { + post { + val userId = fetchPostOrNull("user_id") + val messages = fetchPostJsonArray("messages") + call.respondText(SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId ?: "", messages), ContentType.Application.Json) + } + get { + respond(false, Status.InternalHandlerError, "Not support GET method") + } } getOrPost("/get_forward_msg") { diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketClientService.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketClientService.kt index 75239fe..5412e27 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketClientService.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/WebSocketClientService.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.launch import moe.fuqiuluo.shamrock.remote.service.api.WebSocketClientServlet -import moe.fuqiuluo.shamrock.remote.service.data.push.* import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter @@ -35,7 +34,7 @@ internal class WebSocketClientService( } }) submitFlowJob(GlobalScope.launch { - GlobalEventTransmitter.onRequestEvent() { event -> + GlobalEventTransmitter.onRequestEvent { event -> pushTo(event) } }) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt index 78dd0bd..264ce60 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/config/ShamrockConfig.kt @@ -33,16 +33,17 @@ internal object ShamrockConfig { val mmkv = MMKVFetcher.mmkvWithId("shamrock_config") mmkv.apply { putBoolean( "tablet", intent.getBooleanExtra("tablet", false)) // 强制平板模式 - putInt( "port", intent.getIntExtra("port", 5700)) // 主动HTTP端口 + putInt( "port", intent.getIntExtra("port", 5700)) // 主动HTTP端口 putBoolean( "ws", intent.getBooleanExtra("ws", false)) // 主动WS开关 putBoolean( "http", intent.getBooleanExtra("http", false)) // HTTP回调开关 - putString( "http_addr", intent.getStringExtra("http_addr")) // WebHook回调地址 + putString( "http_addr", intent.getStringExtra("http_addr")) // WebHook回调地址 putBoolean( "ws_client", intent.getBooleanExtra("ws_client", false)) // 被动WS开关 putBoolean( "use_cqcode", intent.getBooleanExtra("use_cqcode", false)) // 使用CQ码 putBoolean( "inject_packet", intent.getBooleanExtra("inject_packet", false)) // 拦截无用包 - putBoolean( "debug", intent.getBooleanExtra("debug", false)) // 调试模式 + putBoolean( "debug", intent.getBooleanExtra("debug", false)) // 调试模式 Config.defaultToken = intent.getStringExtra("token") + Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true) val wsPort = intent.getIntExtra("ws_port", 5800) Config.activeWebSocket = if (Config.activeWebSocket == null) ConnectionConfig( @@ -58,16 +59,16 @@ internal object ShamrockConfig { ConnectionConfig(address = it) }?.toMutableList() - putString( "key_store", intent.getStringExtra("key_store")) // 证书路径 - putString( "ssl_pwd", intent.getStringExtra("ssl_pwd")) // 证书密码 - putString( "ssl_private_pwd", intent.getStringExtra("ssl_private_pwd")) // 证书私钥密码 - putString( "ssl_alias", intent.getStringExtra("ssl_alias")) // 证书别名 - putInt( "ssl_port", intent.getIntExtra("ssl_port", 5701)) // 主动HTTP端口 + putString( "key_store", intent.getStringExtra("key_store")) // 证书路径 + putString( "ssl_pwd", intent.getStringExtra("ssl_pwd")) // 证书密码 + putString( "ssl_private_pwd", intent.getStringExtra("ssl_private_pwd")) // 证书私钥密码 + putString( "ssl_alias", intent.getStringExtra("ssl_alias")) // 证书别名 + putInt( "ssl_port", intent.getIntExtra("ssl_port", 5701)) // 主动HTTP端口 - putBoolean("auto_clear", intent.getBooleanExtra("auto_clear", false)) // 自动清理 + putBoolean("auto_clear", intent.getBooleanExtra("auto_clear", false)) // 自动清理 - putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息 - putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口 + putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息 + putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口 putBoolean("isInit", true) } diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt index 8880583..42c8068 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/Message.kt @@ -9,6 +9,11 @@ internal data class MessageResult( @SerialName("message_id") val msgId: Int, @SerialName("time") val time: Long ) +@Serializable +internal data class ForwardMessageResult( + @SerialName("message_id") val msgId: Int, + @SerialName("forward_id") val forwardId: String +) @Serializable internal data class MessageDetail( diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/XposedEntry.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/XposedEntry.kt index 987e9f2..6753acb 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/XposedEntry.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/XposedEntry.kt @@ -5,6 +5,8 @@ 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.utils.MMKVFetcher import moe.fuqiuluo.shamrock.xposed.loader.ActionLoader import moe.fuqiuluo.shamrock.xposed.loader.FuckAMS @@ -12,6 +14,7 @@ import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader import moe.fuqiuluo.shamrock.tools.FuzzySearchClass import moe.fuqiuluo.shamrock.tools.afterHook import moe.fuqiuluo.shamrock.utils.PlatformUtils +import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader import mqq.app.MobileQQ import java.lang.reflect.Field import java.lang.reflect.Modifier @@ -26,6 +29,12 @@ internal class XposedEntry: IXposedHookLoadPackage { companion object { @JvmStatic var sec_static_stage_inited = false + @JvmStatic + var sec_static_nativehook_inited = false + + external fun injected(): Boolean + + external fun hasEnv(): Boolean } private var firstStageInit = false @@ -108,7 +117,6 @@ internal class XposedEntry: IXposedHookLoadPackage { if (sec_static_stage_inited) return val classLoader = ctx.classLoader.also { requireNotNull(it) } - LuoClassloader.hostClassLoader = classLoader if(injectClassloader(XposedEntry::class.java.classLoader)) { @@ -116,12 +124,7 @@ internal class XposedEntry: IXposedHookLoadPackage { System.setProperty("qxbot_flag", "1") } else return - log("Process Name = " + MobileQQ.getMobileQQ().qqProcessName.apply { - // if (!contains("msf", ignoreCase = true)) return // 非MSF进程 退出 - //if (contains("peak")) { - // PlatformUtils.killProcess(ctx, this) - //} - }) + log("Process Name = " + MobileQQ.getMobileQQ().qqProcessName) PlatformUtils.isTim() diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt index f0bbc8e..6507dc8 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/actions/AntiDetection.kt @@ -5,7 +5,6 @@ import android.content.Context import android.content.pm.PackageManager import android.content.pm.VersionedPackage import android.os.Build -import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers @@ -13,8 +12,9 @@ import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig import moe.fuqiuluo.shamrock.tools.hookMethod +import moe.fuqiuluo.shamrock.xposed.XposedEntry import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader -import mqq.app.MobileQQ +import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader /** * 反检测 @@ -22,6 +22,7 @@ import mqq.app.MobileQQ class AntiDetection: IAction { override fun invoke(ctx: Context) { antiFindPackage(ctx) + antiNativeDetection() if (ShamrockConfig.isAntiTrace()) antiTrace() antiMemoryWalking() @@ -38,6 +39,23 @@ class AntiDetection: IAction { return false } + private fun antiNativeDetection() { + try { + //System.loadLibrary("clover") + NativeLoader.load("clover") + val env = XposedEntry.hasEnv() + val injected = XposedEntry.injected() + if (!env || !injected) { + LogCenter.log("[Shamrock] Shamrock反检测启动失败(env=$env, injected=$injected)", Level.ERROR) + } else { + XposedEntry.sec_static_nativehook_inited = true + LogCenter.log("[Shamrock] Shamrock反检测启动成功", Level.INFO) + } + } catch (e: Throwable) { + LogCenter.log("[Shamrock] Shamrock反检测启动失败,请检查LSPosed版本使用大于100: ${e.message}", Level.ERROR) + } + } + private fun antiFindPackage(context: Context) { val packageManager = context.packageManager val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock", 0) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/NativeLoader.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/NativeLoader.kt index ba0d006..babb63f 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/NativeLoader.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/NativeLoader.kt @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import de.robv.android.xposed.XposedBridge import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.LogCenter +import moe.fuqiuluo.shamrock.xposed.XposedEntry import mqq.app.MobileQQ import java.io.File @@ -16,15 +17,13 @@ internal object NativeLoader { return externalLibPath.resolve("libffmpegkit.so").exists() } - private var isInitShamrock = false - /** * 使目标进程可以使用来自模块的库 */ @SuppressLint("UnsafeDynamicallyLoadedCode") fun load(name: String) { try { - if (name == "shamrock") { + if (name == "shamrock" || name == "clover") { val context = MobileQQ.getContext() val packageManager = context.packageManager val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock.hided", 0)