diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml index cb97880..da09bf0 100644 --- a/.github/workflows/build-apk.yml +++ b/.github/workflows/build-apk.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4.0.0 with: distribution: "temurin" java-version: 17 @@ -30,7 +30,7 @@ jobs: echo "sdk.dir=${ANDROID_HOME}" > local.properties - name: Setup Gradle - uses: gradle/gradle-build-action@v2.9.0 + uses: gradle/gradle-build-action@v2.10.0 - name: Build with Gradle run: | @@ -46,42 +46,41 @@ jobs: KEY_ALIAS: ${{ secrets.SIGN_ALIAS }} KEY_PASSWORD: ${{ secrets.SIGN_KEY_PASSWORD }} - - name: Install aapt - run: sudo apt-get update && sudo apt-get install -y aapt - - name: Set Shamrock Version run: | - apk_file=${{ env.APK_FILE_ALL }} - apk_dump=$(aapt dump badging "$apk_file") - version_name=$(sed -n "s/.*versionName='\([^']*\)'.*/\1/p" <<< "$apk_dump") - echo "SHAMROCK_VERSION=$version_name" >> $GITHUB_ENV + version_name_all=$(basename -s .apk "${{ env.APK_FILE_ALL }}") + version_name_arm64=$(basename -s .apk "${{ env.APK_FILE_ARM64 }}") + version_name_x86_64=$(basename -s .apk "${{ env.APK_FILE_X86_64 }}") + echo "SHAMROCK_VERSION_ALL=$version_name_all" >> $GITHUB_ENV + echo "SHAMROCK_VERSION_ARM64=$version_name_arm64" >> $GITHUB_ENV + echo "SHAMROCK_VERSION_x86_64=$version_name_x86_64" >> $GITHUB_ENV - name: Show Artifacts SHA256 run: | echo "### Build Success :rocket:" >> $GITHUB_STEP_SUMMARY echo "|ABI|SHA256|" >> $GITHUB_STEP_SUMMARY echo "|:--------:|:----------|" >> $GITHUB_STEP_SUMMARY - all=($(sha256sum ${{ env.APK_FILE_ALL }})) + all=($(sha256sum "${{ env.APK_FILE_ALL }}")) echo "|all|$all" >> $GITHUB_STEP_SUMMARY - arm64=($(sha256sum ${{ env.APK_FILE_ARM64 }})) + arm64=($(sha256sum "${{ env.APK_FILE_ARM64 }}")) echo "|arm64|$arm64" >> $GITHUB_STEP_SUMMARY - x86_64=($(sha256sum ${{ env.APK_FILE_X86_64 }})) + x86_64=($(sha256sum "${{ env.APK_FILE_X86_64 }}")) echo "|x86_64|$x86_64" >> $GITHUB_STEP_SUMMARY - name: Upload ALL APK RELEASE uses: actions/upload-artifact@v3 with: - name: Shamrock-v${{ env.SHAMROCK_VERSION }}-all - path: ${{ env.APK_FILE_ALL }} + name: "${{ env.SHAMROCK_VERSION_ALL }}" + path: "${{ env.APK_FILE_ALL }}" - name: Upload ARM64 APK RELEASE uses: actions/upload-artifact@v3 with: - name: Shamrock-v${{ env.SHAMROCK_VERSION }}-arm64 - path: ${{ env.APK_FILE_ARM64 }} + name: "${{ env.SHAMROCK_VERSION_ARM64 }}" + path: "${{ env.APK_FILE_ARM64 }}" - name: Upload X86_64 APK RELEASE uses: actions/upload-artifact@v3 with: - name: Shamrock-v${{ env.SHAMROCK_VERSION }}-x86_64 - path: ${{ env.APK_FILE_X86_64 }} + name: "${{ env.SHAMROCK_VERSION_x86_64 }}" + path: "${{ env.APK_FILE_X86_64 }}" diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d0443c5..f410d0c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,7 +24,7 @@ android { minSdk = 24 targetSdk = 33 versionCode = (System.currentTimeMillis() / 1000).toInt() - versionName = "1.0.6-dev" + gitCommitHash() + versionName = "1.0.7-dev" + gitCommitHash() testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt index 5dae5f7..b5f139a 100644 --- a/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt +++ b/app/src/main/java/moe/fuqiuluo/shamrock/ui/fragment/LabFragment.kt @@ -122,16 +122,6 @@ fun LabFragment() { return@Function true } - Function( - title = "自动唤醒QQ", - desc = "QQ进程死亡时重新打开QQ进程,前提本进程存活。", - descColor = color, - isSwitch = ShamrockConfig.enableAutoStart(ctx) - ) { - ShamrockConfig.setAutoStart(ctx, it) - return@Function true - } - Function( title = "开启Shell接口", desc = "可能导致设备被入侵,请勿随意开启。", @@ -142,6 +132,16 @@ fun LabFragment() { return@Function true } + Function( + title = "自动唤醒QQ", + desc = "QQ进程死亡时重新打开QQ进程,前提本进程存活。", + descColor = color, + isSwitch = ShamrockConfig.enableAutoStart(ctx) + ) { + ShamrockConfig.setAutoStart(ctx, it) + return@Function true + } + kotlin.runCatching { ctx.getSharedPreferences("shared_config", Context.MODE_WORLD_READABLE) }.onSuccess { @@ -155,6 +155,17 @@ fun LabFragment() { scope.toast(ctx, LocalString.restartSysToast) return@Function true } + + Function( + title = "禁用Doze模式", + desc = "禁止系统进入节能模式。", + descColor = color, + isSwitch = it.getBoolean("hook_doze", false) + ) { value -> + it.edit().putBoolean("hook_doze", value).apply() + scope.toast(ctx, LocalString.restartSysToast) + return@Function true + } }.onFailure { AppRuntime.log("无法启用附加选项,LSPosed模块未激活或者不支持XSharedPreferences", Level.WARN) } diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt index c2f9d44..7f3f91c 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/CardSvc.kt @@ -109,7 +109,7 @@ internal object CardSvc: BaseSvc() { val dataService = app .getRuntimeService(IProfileDataService::class.java, "all") val card = refreshCardLock.withLock { - suspendCancellableCoroutine { + suspendCancellableCoroutine { app.addObserver(object: ProfileCardObserver() { override fun onGetProfileCard(success: Boolean, obj: Any) { app.removeObserver(this) 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 300d8f4..2664cd0 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/GroupSvc.kt @@ -2,6 +2,7 @@ package moe.fuqiuluo.qqinterface.servlet +import androidx.core.text.HtmlCompat import com.tencent.common.app.AppInterface import com.tencent.mobileqq.app.BusinessHandlerFactory import com.tencent.mobileqq.app.QQAppInterface @@ -838,12 +839,13 @@ internal object GroupSvc: BaseSvc() { senderId = obj["u"].asLong, publishTime = obj["pubt"].asLong, message = GroupAnnouncementMessage( - text = obj["msg"].asJsonObject["text"].asString, - images = obj["msg"].asJsonObject["pics"].asJsonArrayOrNull?.map { +// text = obj["msg"].asJsonObject["text"].asString, + text = fromHtml(obj["msg"].asJsonObject["text"].asString), + images = obj["msg"].asJsonObject["pics"].asJsonArrayOrNull?.map { pic -> GroupAnnouncementMessageImage( - id = it.jsonObject["id"].asString, - width = it.jsonObject["w"].asString, - height = it.jsonObject["h"].asString, + id = pic.jsonObject["id"].asString, + width = pic.jsonObject["w"].asString, + height = pic.jsonObject["h"].asString, ) } ?: ArrayList() ) @@ -854,6 +856,14 @@ internal object GroupSvc: BaseSvc() { } } + private fun fromHtml(htmlString: String): String { + return HtmlCompat + // 特殊处理 ,目的是替换为换行符,否则会被fromHtml忽略并移除 + .fromHtml(htmlString.replace(" ", "[shamrockplaceholder]"), HtmlCompat.FROM_HTML_MODE_LEGACY) + .toString() + .replace("[shamrockplaceholder]", "\n") + } + @OptIn(ExperimentalSerializationApi::class) suspend fun uploadImageTroopNotice(image: String): Result { val file = FileUtils.parseAndSave(image) 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 c40d84e..5e3f05b 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 @@ -274,7 +274,7 @@ internal object MessageMaker { element.elementType = MsgConstant.KELEMTYPEREPLY val reply = ReplyElement() - val msgHash = data["id"].asString.toInt() + val msgHash = data["id"].asInt val mapping = MessageHelper.getMsgMappingByHash(msgHash) ?: return Result.failure(Exception("不存在该消息映射,无法回复消息")) @@ -627,10 +627,16 @@ internal object MessageMaker { else -> { val info = GroupSvc.getTroopMemberInfoByUin(peerId, qq, true).onFailure { LogCenter.log("无法获取群成员信息: $qq", Level.ERROR) - }.getOrThrow() - at.content = "@${info.troopnick - .ifNullOrEmpty(info.friendnick) - .ifNullOrEmpty(qq)}" + }.getOrNull() + if (info != null) { + at.content = "@${ + info.troopnick + .ifNullOrEmpty(info.friendnick) + .ifNullOrEmpty(qq) + }" + } else { + at.content = "@${data["name"].asStringOrNull.ifNullOrEmpty(qq)}" + } at.atType = MsgConstant.ATTYPEONE at.atNtUid = ContactHelper.getUidByUinAsync(qq.toLong()) } diff --git a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt index bac4567..602a6f4 100644 --- a/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/qqinterface/servlet/msg/convert/MessageElemConverter.kt @@ -331,7 +331,7 @@ internal sealed class MessageElemConverter: IMessageConvert { MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> { val notify = tip.xmlElement when(notify.busiId) { - /* 群戳一戳 */1061L -> {} + /* 群戳一戳 */1061L, /* 群打卡 */1068L -> {} else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN) } } 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 af6c379..456e565 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/helper/MessageHelper.kt @@ -184,6 +184,32 @@ internal object MessageHelper { } } + suspend fun sendMessageNoCb( + chatType: Int, + peerId: String, + message: JsonArray, + fromId: String = peerId + ): Pair { + val uniseq = generateMsgId(chatType) + val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, message).also { + if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") + }.second.filter { + it.elementType != -1 + } as ArrayList + val contact = generateContact(chatType, peerId, fromId) + val nonMsg: Boolean = message.isEmpty() + return if (!nonMsg) { + val service = QRoute.api(IMsgService::class.java) + return suspendCoroutine { + service.sendMsg(contact, uniseq.second, msg) { code, why -> + it.resume(code to uniseq.second) + } + } + } else { + -1 to uniseq.second + } + } + suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact { val peerId = if (MsgConstant.KCHATTYPEC2C == chatType || MsgConstant.KCHATTYPETEMPC2CFROMGROUP == chatType) { ContactHelper.getUidByUinAsync(id.toLong()) 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 index be77afe..8e8583f 100644 --- 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 @@ -2,11 +2,7 @@ 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 kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.* import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.TicketSvc import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments @@ -19,21 +15,6 @@ 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 { @@ -78,7 +59,7 @@ internal object SendForwardMessage : IActionHandler() { suspend operator fun invoke( chatType: Int, peerId: String, - message: JsonArray, + messages: JsonArray, echo: JsonElement = EmptyJsonString ): String { kotlin.runCatching { @@ -87,79 +68,91 @@ internal object SendForwardMessage : IActionHandler() { 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")) { - if (data["content"] is JsonArray) { - data["content"].asJsonArray.forEach { msg -> - if (msg.asJsonObject["type"].asStringOrNull == "node") { - LogCenter.log("合并转发消息不支持嵌套", Level.WARN) - return@map ForwardMsgNode.EmptyNode - } - } - } - ForwardMsgNode.MessageNode( - name = data["name"].asStringOrNull ?: "", - content = data["content"] - ) - } else ForwardMsgNode.MessageIdNode(data["id"].asInt) + val multiNodes = messages.map { + if (it.asJsonObject["type"].asStringOrNull != "node") { + LogCenter.log("包含非node类型节点", Level.WARN) + return@map null } - }.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 -> + if (it.asJsonObject["data"] !is JsonObject) { + LogCenter.log("data字段错误", Level.WARN) + return@map null + } + it.asJsonObject["data"].asJsonObject.let { data -> + if (data.containsKey("id")) { + val record = MsgSvc.getMsg(data["id"].asInt).getOrNull() + if (record == null) { + LogCenter.log("合并转发消息节点消息获取失败:${data["id"]}", Level.WARN) + return@map null + } else { + record.peerName to 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 if (msg is JsonObject) listOf(msg).jsonArray - else MessageHelper.decodeCQCode(msg.asString) - }, - { code, why -> - if (code != 0) { - error("合并转发消息节点消息发送失败:$code($why)") - } - it.resume(node.name to msgId) - }).first - }.invokeOnCompletion { - it?.let { - LogCenter.log("合并转发消息节点消息发送失败:${it.stackTraceToString()}", Level.ERROR) } + } else if (data.containsKey("content")) { + (data["name"].asStringOrNull ?: "Anno") to when (val raw = data["content"]) { + is JsonObject -> raw.asJsonArray + is JsonArray -> raw.asJsonArray + else -> MessageHelper.decodeCQCode(raw.asString) + } + } else { + LogCenter.log("消息节点缺少id或content字段", Level.WARN) + return@map null } + }.let { node -> + val content = node.second.map { msg -> + when (msg.asJsonObject["type"].asStringOrNull ?: "text") { + "at" -> { + buildJsonObject { + put("type", "text") + putJsonObject("data") { + put( + "text", "@${ + msg.asJsonObject["data"].asJsonObject["name"].asStringOrNull.ifNullOrEmpty( + msg.asJsonObject["data"].asJsonObject["qq"].asString + ) + }" + ) + } + } + } + + "voice" -> { + buildJsonObject { + put("type", "text") + putJsonObject("data") { + put("text", "[语音]") + } + } + } + + "node" -> { + LogCenter.log("合并转发消息暂时不支持嵌套", Level.WARN) + buildJsonObject { + put("type", "text") + putJsonObject("data") { + put("text", "[合并转发消息]") + } + } + } + + else -> msg + } + }.json + + val result = MessageHelper.sendMessageNoCb(MsgConstant.KCHATTYPEC2C, selfUin, content) + if (result.first != 0) { + LogCenter.log("合并转发消息节点消息发送失败", Level.WARN) + } + result.second to node.first } - } + }.filterNotNull() 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)) } + multiNodes.forEach { add(MultiMsgInfo(it.first, it.second)) } }.also { it.reverse() }, from, to, MsgSvc.MessageCallback(peerId, uniseq.first)) return ok( @@ -174,7 +167,7 @@ internal object SendForwardMessage : IActionHandler() { return logic("合并转发消息失败(unknown error)", echo) } - override val requiredParams: Array = arrayOf("message") + override val requiredParams: Array = arrayOf("messages") override fun path(): String = "send_forward_msg" } \ No newline at end of file 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 0e32a18..5c038c4 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 @@ -29,7 +29,7 @@ import moe.fuqiuluo.shamrock.tools.jsonArray import moe.fuqiuluo.shamrock.tools.respond fun Routing.messageAction() { - route("/send_group_forward_msg") { + route("/send_group_forward_(msg|message)".toRegex()) { post { val groupId = fetchPostOrNull("group_id") val messages = fetchPostJsonArray("messages") @@ -40,7 +40,7 @@ fun Routing.messageAction() { } } - route("/send_private_forward_msg") { + route("/send_private_forward_(msg|message)".toRegex()) { post { val userId = fetchPostOrNull("user_id") val messages = fetchPostJsonArray("messages") @@ -51,6 +51,18 @@ fun Routing.messageAction() { } } + route("/send_forward_(msg|message)".toRegex()) { + post { + val userId = fetchPostOrNull("user_id") + val groupId = fetchPostOrNull("group_id") + val messages = fetchPostJsonArray("messages") + call.respondText(SendForwardMessage(MsgConstant.KCHATTYPEC2C, userId ?: groupId?: "", messages), ContentType.Application.Json) + } + get { + respond(false, Status.InternalHandlerError, "Not support GET method") + } + } + getOrPost("/get_forward_msg") { val id = fetchOrThrow("id") call.respondText(GetForwardMsg(id), ContentType.Application.Json) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt index 3aafe8a..75d1ef4 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/api/GlobalEventTransmitter.kt @@ -25,6 +25,7 @@ import moe.fuqiuluo.shamrock.remote.service.data.push.RequestEvent import moe.fuqiuluo.shamrock.remote.service.data.push.RequestSubType import moe.fuqiuluo.shamrock.remote.service.data.push.RequestType import moe.fuqiuluo.shamrock.remote.service.data.push.Sender +import moe.fuqiuluo.shamrock.remote.service.data.push.SignDetail import moe.fuqiuluo.shamrock.tools.ShamrockDsl import moe.fuqiuluo.shamrock.tools.json import java.util.ArrayList @@ -81,8 +82,8 @@ internal object GlobalEventTransmitter: BaseSvc() { sender = Sender( userId = record.senderUin, nickname = record.sendNickName - .ifBlank { record.sendMemberName } .ifBlank { record.sendRemarkName } + .ifBlank { record.sendMemberName } .ifBlank { record.peerName }, card = record.sendMemberName, role = when (record.senderUin) { @@ -222,6 +223,24 @@ internal object GlobalEventTransmitter: BaseSvc() { * 群聊通知 通知器 */ object GroupNoticeTransmitter { + suspend fun transGroupSign(time: Long, target: Long, action: String?, rankImg: String?, groupCode: Long): Boolean { + pushNotice(NoticeEvent( + time = time, + selfId = app.longAccountUin, + postType = PostType.Notice, + type = NoticeType.Notify, + subType = NoticeSubType.Sign, + userId = target, + groupId = groupCode, + target = target, + signDetail = SignDetail( + rankImg = rankImg, + action = action + ) + )) + return true + } + suspend fun transGroupPoke(time: Long, operation: Long, target: Long, action: String?, suffix: String?, actionImg: String?, groupCode: Long): Boolean { pushNotice(NoticeEvent( time = time, 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 361c309..d2e927f 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 @@ -4,6 +4,8 @@ import android.content.Intent import com.tencent.mmkv.MMKV import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString +import moe.fuqiuluo.shamrock.helper.Level +import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.tools.GlobalJson5 import moe.fuqiuluo.shamrock.utils.MMKVFetcher import mqq.app.MobileQQ @@ -15,10 +17,14 @@ internal object ShamrockConfig { if (it.exists()) it.delete() it.mkdirs() } - private val Config: ServiceConfig by lazy { - GlobalJson5.decodeFromString(ConfigDir.resolve("config.json").also { + private val Config = kotlin.runCatching { + GlobalJson5.decodeFromString(ConfigDir.resolve("config.json").also { if (!it.exists()) it.writeText("{}") }.readText()) + }.onFailure { + LogCenter.log("您的配置文件出现错误: ${it.stackTraceToString()}", Level.ERROR) + }.getOrElse { + ServiceConfig() } fun isInit(): Boolean { diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/NoticeEvent.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/NoticeEvent.kt index 7b75a5c..f7ac137 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/NoticeEvent.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/data/push/NoticeEvent.kt @@ -42,6 +42,8 @@ internal enum class NoticeSubType { @SerialName("kick_me") KickMe, @SerialName("poke") Poke, + @SerialName("sign") Sign, + @SerialName("title") Title, @SerialName("delete") Delete, @@ -87,6 +89,9 @@ internal data class NoticeEvent( // 戳一戳 @SerialName("poke_detail") val pokeDetail: PokeDetail? = null, + // 群打卡 + @SerialName("sign_detail") val signDetail: SignDetail? = null, + ) /** @@ -131,4 +136,11 @@ internal data class PokeDetail ( val suffix: String? = "", @SerialName("action_img_url") val actionImg: String? = "https://tianquan.gtimg.cn/nudgeaction/item/0/expression.jpg", +) + +@Serializable +internal data class SignDetail ( + val action: String? = "今日第1个打卡", + @SerialName("rank_img") + val rankImg: String? = "", ) \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt index a540d82..4c4c02a 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/AioListener.kt @@ -182,7 +182,9 @@ internal object AioListener: IKernelMsgListener { .updateMsgSeqByMsgHash(msgHash, record.msgSeq.toInt()) } - if (!ShamrockConfig.enableSelfMsg() || record.senderUin != TicketSvc.getLongUin()) + if (!ShamrockConfig.enableSelfMsg() + || record.senderUin != TicketSvc.getLongUin() + || record.peerUin == TicketSvc.getLongUin()) return@launch val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString()) diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt index 7dc29ae..9c215d9 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/remote/service/listener/PrimitiveListener.kt @@ -63,28 +63,32 @@ internal object PrimitiveListener { subType = pb[1, 2, 2].asInt } val msgTime = pb[1, 2, 6].asLong - when (msgType) { - 33 -> onGroupMemIncreased(msgTime, pb) - 34 -> onGroupMemberDecreased(msgTime, pb) - 44 -> onGroupAdminChange(msgTime, pb) - 84 -> onGroupApply(msgTime, pb) - 87 -> onInviteGroup(msgTime, pb) - 528 -> when (subType) { - 35 -> onFriendApply(msgTime, pb) - 39 -> onCardChange(msgTime, pb) - // invite - 68 -> onGroupApply(msgTime, pb) - 138 -> onC2CRecall(msgTime, pb) - 290 -> onC2cPoke(msgTime, pb) - } + try { + when (msgType) { + 33 -> onGroupMemIncreased(msgTime, pb) + 34 -> onGroupMemberDecreased(msgTime, pb) + 44 -> onGroupAdminChange(msgTime, pb) + 84 -> onGroupApply(msgTime, pb) + 87 -> onInviteGroup(msgTime, pb) + 528 -> when (subType) { + 35 -> onFriendApply(msgTime, pb) + 39 -> onCardChange(msgTime, pb) + // invite + 68 -> onGroupApply(msgTime, pb) + 138 -> onC2CRecall(msgTime, pb) + 290 -> onC2cPoke(msgTime, pb) + } - 732 -> when (subType) { - 12 -> onGroupBan(msgTime, pb) - 16 -> onGroupTitleChange(msgTime, pb) - 17 -> onGroupRecall(msgTime, pb) - 20 -> onGroupPoke(msgTime, pb) - 21 -> onEssenceMessage(msgTime, pb) + 732 -> when (subType) { + 12 -> onGroupBan(msgTime, pb) + 16 -> onGroupTitleChange(msgTime, pb) + 17 -> onGroupRecall(msgTime, pb) + 20 -> onGroupPokeAndGroupSign(msgTime, pb) + 21 -> onEssenceMessage(msgTime, pb) + } } + } catch (e: Exception) { + LogCenter.log("onMsgPush(msgType: $msgType): "+e.stackTraceToString(), Level.WARN) } } @@ -153,8 +157,21 @@ internal object PrimitiveListener { private suspend fun onCardChange(msgTime: Long, pb: ProtoMap) { - val targetId = pb[1, 3, 2, 1, 13, 2].asUtf8String - val newCardList = pb[1, 3, 2, 1, 13, 3].asList + var detail = pb[1, 3, 2] + if (detail !is ProtoMap) { + try { + val readPacket = ByteReadPacket(detail.asByteArray) + readPacket.readBuf32Long() + readPacket.discardExact(1) + detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt())) + readPacket.release() + } catch (e: Exception) { + LogCenter.log("onCardChange error: ${e.stackTraceToString()}", Level.WARN) + } + } + + val targetId = detail[1, 13, 2].asUtf8String + val newCardList = detail[1, 13, 3].asList var newCard = "" newCardList .value @@ -163,7 +180,7 @@ internal object PrimitiveListener { newCard = it[2].asUtf8String } } - val groupId = pb[1, 3, 2, 1, 13, 4].asLong + val groupId = pb[1, 13, 4].asLong var oldCard = "" val targetQQ = ContactHelper.getUinByUidAsync(targetId).toLong() LogCenter.log("群组[$groupId]成员$targetQQ 群名片变动 -> $newCard") @@ -181,13 +198,18 @@ internal object PrimitiveListener { } private suspend fun onGroupTitleChange(msgTime: Long, pb: ProtoMap) { - val groupCode = pb[1, 1, 1].asULong - - val readPacket = ByteReadPacket(pb[1, 3, 2].asByteArray) - val detail = if (readPacket.readBuf32Long() == groupCode) { - readPacket.discardExact(1) - ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt())) - } else pb[1, 3, 2] + var detail = pb[1, 3, 2] + if (detail !is ProtoMap) { + try { + val readPacket = ByteReadPacket(detail.asByteArray) + readPacket.readBuf32Long() + readPacket.discardExact(1) + detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt())) + readPacket.release() + } catch (e: Exception) { + LogCenter.log("onGroupTitleChange error: ${e.stackTraceToString()}", Level.WARN) + } + } val targetUin = detail[5, 5].asLong @@ -212,13 +234,18 @@ internal object PrimitiveListener { } private suspend fun onEssenceMessage(msgTime: Long, pb: ProtoMap) { - val groupCode = pb[1, 1, 1].asULong - - val readPacket = ByteReadPacket(pb[1, 3, 2].asByteArray) - val detail = if (readPacket.readBuf32Long() == groupCode) { - readPacket.discardExact(1) - ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt())) - } else pb[1, 3, 2] + var detail = pb[1, 3, 2] + if (detail !is ProtoMap) { + try { + val readPacket = ByteReadPacket(detail.asByteArray) + readPacket.readBuf32Long() + readPacket.discardExact(1) + detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt())) + readPacket.release() + } catch (e: Exception) { + LogCenter.log("onEssenceMessage error: ${e.stackTraceToString()}", Level.WARN) + } + } val groupId = detail[4].asLong val mesSeq = detail[37].asInt @@ -254,31 +281,26 @@ internal object PrimitiveListener { } - private suspend fun onGroupPoke(time: Long, pb: ProtoMap) { - val groupCode1 = pb[1, 1, 1].asULong - - var groupCode: Long = groupCode1 - - val readPacket = ByteReadPacket(pb[1, 3, 2].asByteArray) - val groupCode2 = readPacket.readBuf32Long() - - var detail = if (groupCode2 == groupCode1) { - groupCode = groupCode2 - readPacket.discardExact(1) - ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt())) - } else pb[1, 3, 2] + private suspend fun onGroupPokeAndGroupSign(time: Long, pb: ProtoMap) { + var detail = pb[1, 3, 2] if (detail !is ProtoMap) { - groupCode = groupCode2 - readPacket.discardExact(1) - detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt())) + try { + val readPacket = ByteReadPacket(detail.asByteArray) + readPacket.discardExact(4) + readPacket.discardExact(1) + detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt())) + readPacket.release() + } catch (e: Exception) { + LogCenter.log("onGroupPokeAndGroupSign error: ${e.stackTraceToString()}", Level.WARN) + } } - readPacket.release() - lateinit var target: String lateinit var operation: String var action: String? = null var suffix: String? = null var actionImg: String? = null + var rankImg: String? = null + val groupCode = detail[4].asULong detail[26][7] .asList .value @@ -287,18 +309,42 @@ internal object PrimitiveListener { when (it[1].asUtf8String) { "uin_str1" -> operation = value "uin_str2" -> target = value + // "nick_str1" -> operation_nick = value + // "nick_str2" -> operation_nick = value "action_str" -> action = value "alt_str1" -> action = value "suffix_str" -> suffix = value "action_img_url" -> actionImg = value + + "mqq_uin" -> target = value + // "mqq_nick" -> operation_nick = value + "user_sign" -> action = value + "rank_img" -> rankImg = value + // "sign_word" -> 我也要打卡 + } + } + when (detail[26][2].asInt) { + 1061 -> { + LogCenter.log("群戳一戳($groupCode): $operation $action $target $suffix") + if (!GlobalEventTransmitter.GroupNoticeTransmitter + .transGroupPoke(time, operation.toLong(), target.toLong(), action, suffix, actionImg, groupCode) + ) { + LogCenter.log("群戳一戳推送失败!", Level.WARN) } } - LogCenter.log("群戳一戳($groupCode): $operation $action $target $suffix") - if (!GlobalEventTransmitter.GroupNoticeTransmitter - .transGroupPoke(time, operation.toLong(), target.toLong(), action, suffix, actionImg, groupCode) - ) { - LogCenter.log("群戳一戳推送失败!", Level.WARN) + 1068 -> { + LogCenter.log("群打卡($groupCode): $action $target") + if (!GlobalEventTransmitter.GroupNoticeTransmitter + .transGroupSign(time, target.toLong(), action, rankImg, groupCode) + ) { + LogCenter.log("群打卡推送失败!", Level.WARN) + } + } + + else -> { + LogCenter.log("onGroupPokeAndGroupSign unknown type ${detail[2].asInt}", Level.WARN) + } } } 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 6753acb..058faa7 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/XposedEntry.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/XposedEntry.kt @@ -5,16 +5,13 @@ 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 +import moe.fuqiuluo.shamrock.xposed.loader.KeepAlive 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 @@ -42,7 +39,7 @@ internal class XposedEntry: IXposedHookLoadPackage { override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) { when (param.packageName) { PACKAGE_NAME_QQ -> entryMQQ(param.classLoader) - "android" -> FuckAMS.injectAMS(param.classLoader) + "android" -> KeepAlive(param.classLoader) PACKAGE_NAME_TIM -> entryTim(param.classLoader) } } 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 5861098..a27598c 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 @@ -7,14 +7,12 @@ import android.content.pm.VersionedPackage import android.os.Build import de.robv.android.xposed.XC_MethodReplacement import de.robv.android.xposed.XSharedPreferences -import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers 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.FuckAMS import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader @@ -68,6 +66,8 @@ class AntiDetection: IAction { } private fun antiFindPackage(context: Context) { + if (isAntiFindPackage) return + val packageManager = context.packageManager val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock", 0) val packageInfo = packageManager.getPackageInfo("moe.fuqiuluo.shamrock", 0) @@ -76,7 +76,7 @@ class AntiDetection: IAction { val packageName = it.args[0] as String if(packageName == "moe.fuqiuluo.shamrock") { LogCenter.log("AntiDetection: 检测到对Shamrock的检测,欺骗PackageManager(GA)", Level.WARN) - it.throwable = PackageManager.NameNotFoundException() + it.throwable = PackageManager.NameNotFoundException("Hided") } else if (packageName == "moe.fuqiuluo.shamrock.hided") { it.result = applicationInfo } @@ -102,6 +102,8 @@ class AntiDetection: IAction { } } } + + isAntiFindPackage = true } private fun antiMemoryWalking() { @@ -207,4 +209,9 @@ class AntiDetection: IAction { }.toTypedArray() } } + + companion object { + @JvmStatic + var isAntiFindPackage = false + } } \ No newline at end of file diff --git a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/FuckAMS.kt b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/KeepAlive.kt similarity index 71% rename from xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/FuckAMS.kt rename to xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/KeepAlive.kt index e94002b..9f4586d 100644 --- a/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/FuckAMS.kt +++ b/xposed/src/main/java/moe/fuqiuluo/shamrock/xposed/loader/KeepAlive.kt @@ -3,14 +3,15 @@ package moe.fuqiuluo.shamrock.xposed.loader import android.content.pm.ApplicationInfo import android.os.Build -import com.arthenica.ffmpegkit.BuildConfig +import android.os.Process import de.robv.android.xposed.XSharedPreferences import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedHelpers import moe.fuqiuluo.shamrock.tools.hookMethod import java.lang.reflect.Method +import kotlin.concurrent.timer -internal object FuckAMS { +internal object KeepAlive { private val KeepPackage = arrayOf( "com.tencent.mobileqq", "moe.fuqiuluo.shamrock" ) @@ -20,7 +21,46 @@ internal object FuckAMS { private lateinit var METHOD_IS_KILLED: Method private var allowPersistent: Boolean = false - fun injectAMS(loader: ClassLoader) { + operator fun invoke(loader: ClassLoader) { + val pref = XSharedPreferences("moe.fuqiuluo.shamrock", "shared_config") + hookAMS(pref, loader) + hookDoze(pref, loader) + } + + private fun hookDoze(pref: XSharedPreferences, loader: ClassLoader) { + if (pref.file.canRead() && pref.getBoolean("hook_doze", false)) { + val result = runCatching { + val DeviceIdleController = XposedHelpers.findClass("com.android.server.DeviceIdleController", loader) + ?: return@runCatching -1 + val becomeActiveLocked = XposedHelpers.findMethodBestMatch(DeviceIdleController, "becomeActiveLocked", String::class.java, Integer.TYPE) + ?: return@runCatching -2 + if (!becomeActiveLocked.isAccessible) { + becomeActiveLocked.isAccessible = true + } + DeviceIdleController.hookMethod("onStart").after { + XposedBridge.log("[Shamrock] DeviceIdleController onStart") + timer(initialDelay = 120_000L, period = 240_000L) { + XposedBridge.log("[Shamrock] try to wakeup screen") + becomeActiveLocked.invoke(it.thisObject, "screen", Process.myUid()) + } + } + DeviceIdleController.hookMethod("becomeInactiveIfAppropriateLocked").before { + XposedBridge.log("[Shamrock] DeviceIdleController becomeInactiveIfAppropriateLocked") + it.result = Unit + } + DeviceIdleController.hookMethod("stepIdleStateLocked").before { + XposedBridge.log("[Shamrock] DeviceIdleController stepIdleStateLocked") + it.result = Unit + } + return@runCatching 0 + }.getOrElse { -5 } + if(result < 0) { + XposedBridge.log("[Shamrock] Unable to hookDoze: $result") + } + } + } + + private fun hookAMS(pref: XSharedPreferences, loader: ClassLoader) { kotlin.runCatching { val ActivityManagerService = XposedHelpers.findClass("com.android.server.am.ActivityManagerService", loader) ActivityManagerService.hookMethod("newProcessRecordLocked").after { @@ -30,7 +70,6 @@ internal object FuckAMS { XposedBridge.log("[Shamrock] Plan A failed: ${it.message}") } - val pref = XSharedPreferences("moe.fuqiuluo.shamrock", "shared_config") if (pref.file.canRead()) { allowPersistent = pref.getBoolean("persistent", false) XposedBridge.log("[Shamrock] allowPersistent = $allowPersistent")