Merge branch 'master' of github.com:whitechi73/OpenShamrock

This commit is contained in:
ikechan8370 2023-12-06 15:53:11 +08:00
commit 48c9048a00
18 changed files with 387 additions and 202 deletions

View File

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

View File

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

View File

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

View File

@ -109,7 +109,7 @@ internal object CardSvc: BaseSvc() {
val dataService = app
.getRuntimeService(IProfileDataService::class.java, "all")
val card = refreshCardLock.withLock {
suspendCancellableCoroutine<Card?> {
suspendCancellableCoroutine {
app.addObserver(object: ProfileCardObserver() {
override fun onGetProfileCard(success: Boolean, obj: Any) {
app.removeObserver(this)

View File

@ -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
// 特殊处理&#10;目的是替换为换行符否则会被fromHtml忽略并移除
.fromHtml(htmlString.replace("&#10;", "[shamrockplaceholder]"), HtmlCompat.FROM_HTML_MODE_LEGACY)
.toString()
.replace("[shamrockplaceholder]", "\n")
}
@OptIn(ExperimentalSerializationApi::class)
suspend fun uploadImageTroopNotice(image: String): Result<GroupAnnouncementMessageImage> {
val file = FileUtils.parseAndSave(image)

View File

@ -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())
}

View File

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

View File

@ -184,6 +184,32 @@ internal object MessageHelper {
}
}
suspend fun sendMessageNoCb(
chatType: Int,
peerId: String,
message: JsonArray,
fromId: String = peerId
): Pair<Int, Long> {
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<MsgElement>
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())

View File

@ -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<MultiMsgInfo>().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<String> = arrayOf("message")
override val requiredParams: Array<String> = arrayOf("messages")
override fun path(): String = "send_forward_msg"
}

View File

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

View File

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

View File

@ -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<ServiceConfig>(ConfigDir.resolve("config.json").also {
if (!it.exists()) it.writeText("{}")
}.readText())
}.onFailure {
LogCenter.log("您的配置文件出现错误: ${it.stackTraceToString()}", Level.ERROR)
}.getOrElse {
ServiceConfig()
}
fun isInit(): Boolean {

View File

@ -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,
)
/**
@ -132,3 +137,10 @@ internal data class PokeDetail (
@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? = "",
)

View File

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

View File

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

View File

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

View File

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

View File

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