mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 13:12:17 +08:00
Merge branch 'master' of github.com:whitechi73/OpenShamrock
This commit is contained in:
commit
48c9048a00
35
.github/workflows/build-apk.yml
vendored
35
.github/workflows/build-apk.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-java@v3
|
- uses: actions/setup-java@v4.0.0
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
distribution: "temurin"
|
||||||
java-version: 17
|
java-version: 17
|
||||||
@ -30,7 +30,7 @@ jobs:
|
|||||||
echo "sdk.dir=${ANDROID_HOME}" > local.properties
|
echo "sdk.dir=${ANDROID_HOME}" > local.properties
|
||||||
|
|
||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/gradle-build-action@v2.9.0
|
uses: gradle/gradle-build-action@v2.10.0
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: |
|
run: |
|
||||||
@ -46,42 +46,41 @@ jobs:
|
|||||||
KEY_ALIAS: ${{ secrets.SIGN_ALIAS }}
|
KEY_ALIAS: ${{ secrets.SIGN_ALIAS }}
|
||||||
KEY_PASSWORD: ${{ secrets.SIGN_KEY_PASSWORD }}
|
KEY_PASSWORD: ${{ secrets.SIGN_KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: Install aapt
|
|
||||||
run: sudo apt-get update && sudo apt-get install -y aapt
|
|
||||||
|
|
||||||
- name: Set Shamrock Version
|
- name: Set Shamrock Version
|
||||||
run: |
|
run: |
|
||||||
apk_file=${{ env.APK_FILE_ALL }}
|
version_name_all=$(basename -s .apk "${{ env.APK_FILE_ALL }}")
|
||||||
apk_dump=$(aapt dump badging "$apk_file")
|
version_name_arm64=$(basename -s .apk "${{ env.APK_FILE_ARM64 }}")
|
||||||
version_name=$(sed -n "s/.*versionName='\([^']*\)'.*/\1/p" <<< "$apk_dump")
|
version_name_x86_64=$(basename -s .apk "${{ env.APK_FILE_X86_64 }}")
|
||||||
echo "SHAMROCK_VERSION=$version_name" >> $GITHUB_ENV
|
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
|
- name: Show Artifacts SHA256
|
||||||
run: |
|
run: |
|
||||||
echo "### Build Success :rocket:" >> $GITHUB_STEP_SUMMARY
|
echo "### Build Success :rocket:" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "|ABI|SHA256|" >> $GITHUB_STEP_SUMMARY
|
echo "|ABI|SHA256|" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "|:--------:|:----------|" >> $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
|
echo "|all|$all" >> $GITHUB_STEP_SUMMARY
|
||||||
arm64=($(sha256sum ${{ env.APK_FILE_ARM64 }}))
|
arm64=($(sha256sum "${{ env.APK_FILE_ARM64 }}"))
|
||||||
echo "|arm64|$arm64" >> $GITHUB_STEP_SUMMARY
|
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
|
echo "|x86_64|$x86_64" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
- name: Upload ALL APK RELEASE
|
- name: Upload ALL APK RELEASE
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Shamrock-v${{ env.SHAMROCK_VERSION }}-all
|
name: "${{ env.SHAMROCK_VERSION_ALL }}"
|
||||||
path: ${{ env.APK_FILE_ALL }}
|
path: "${{ env.APK_FILE_ALL }}"
|
||||||
|
|
||||||
- name: Upload ARM64 APK RELEASE
|
- name: Upload ARM64 APK RELEASE
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Shamrock-v${{ env.SHAMROCK_VERSION }}-arm64
|
name: "${{ env.SHAMROCK_VERSION_ARM64 }}"
|
||||||
path: ${{ env.APK_FILE_ARM64 }}
|
path: "${{ env.APK_FILE_ARM64 }}"
|
||||||
|
|
||||||
- name: Upload X86_64 APK RELEASE
|
- name: Upload X86_64 APK RELEASE
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: Shamrock-v${{ env.SHAMROCK_VERSION }}-x86_64
|
name: "${{ env.SHAMROCK_VERSION_x86_64 }}"
|
||||||
path: ${{ env.APK_FILE_X86_64 }}
|
path: "${{ env.APK_FILE_X86_64 }}"
|
||||||
|
@ -24,7 +24,7 @@ android {
|
|||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 33
|
targetSdk = 33
|
||||||
versionCode = (System.currentTimeMillis() / 1000).toInt()
|
versionCode = (System.currentTimeMillis() / 1000).toInt()
|
||||||
versionName = "1.0.6-dev" + gitCommitHash()
|
versionName = "1.0.7-dev" + gitCommitHash()
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
@ -122,16 +122,6 @@ fun LabFragment() {
|
|||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
Function(
|
|
||||||
title = "自动唤醒QQ",
|
|
||||||
desc = "QQ进程死亡时重新打开QQ进程,前提本进程存活。",
|
|
||||||
descColor = color,
|
|
||||||
isSwitch = ShamrockConfig.enableAutoStart(ctx)
|
|
||||||
) {
|
|
||||||
ShamrockConfig.setAutoStart(ctx, it)
|
|
||||||
return@Function true
|
|
||||||
}
|
|
||||||
|
|
||||||
Function(
|
Function(
|
||||||
title = "开启Shell接口",
|
title = "开启Shell接口",
|
||||||
desc = "可能导致设备被入侵,请勿随意开启。",
|
desc = "可能导致设备被入侵,请勿随意开启。",
|
||||||
@ -142,6 +132,16 @@ fun LabFragment() {
|
|||||||
return@Function true
|
return@Function true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Function(
|
||||||
|
title = "自动唤醒QQ",
|
||||||
|
desc = "QQ进程死亡时重新打开QQ进程,前提本进程存活。",
|
||||||
|
descColor = color,
|
||||||
|
isSwitch = ShamrockConfig.enableAutoStart(ctx)
|
||||||
|
) {
|
||||||
|
ShamrockConfig.setAutoStart(ctx, it)
|
||||||
|
return@Function true
|
||||||
|
}
|
||||||
|
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
ctx.getSharedPreferences("shared_config", Context.MODE_WORLD_READABLE)
|
ctx.getSharedPreferences("shared_config", Context.MODE_WORLD_READABLE)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
@ -155,6 +155,17 @@ fun LabFragment() {
|
|||||||
scope.toast(ctx, LocalString.restartSysToast)
|
scope.toast(ctx, LocalString.restartSysToast)
|
||||||
return@Function true
|
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 {
|
}.onFailure {
|
||||||
AppRuntime.log("无法启用附加选项,LSPosed模块未激活或者不支持XSharedPreferences", Level.WARN)
|
AppRuntime.log("无法启用附加选项,LSPosed模块未激活或者不支持XSharedPreferences", Level.WARN)
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ internal object CardSvc: BaseSvc() {
|
|||||||
val dataService = app
|
val dataService = app
|
||||||
.getRuntimeService(IProfileDataService::class.java, "all")
|
.getRuntimeService(IProfileDataService::class.java, "all")
|
||||||
val card = refreshCardLock.withLock {
|
val card = refreshCardLock.withLock {
|
||||||
suspendCancellableCoroutine<Card?> {
|
suspendCancellableCoroutine {
|
||||||
app.addObserver(object: ProfileCardObserver() {
|
app.addObserver(object: ProfileCardObserver() {
|
||||||
override fun onGetProfileCard(success: Boolean, obj: Any) {
|
override fun onGetProfileCard(success: Boolean, obj: Any) {
|
||||||
app.removeObserver(this)
|
app.removeObserver(this)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package moe.fuqiuluo.qqinterface.servlet
|
package moe.fuqiuluo.qqinterface.servlet
|
||||||
|
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
import com.tencent.common.app.AppInterface
|
import com.tencent.common.app.AppInterface
|
||||||
import com.tencent.mobileqq.app.BusinessHandlerFactory
|
import com.tencent.mobileqq.app.BusinessHandlerFactory
|
||||||
import com.tencent.mobileqq.app.QQAppInterface
|
import com.tencent.mobileqq.app.QQAppInterface
|
||||||
@ -838,12 +839,13 @@ internal object GroupSvc: BaseSvc() {
|
|||||||
senderId = obj["u"].asLong,
|
senderId = obj["u"].asLong,
|
||||||
publishTime = obj["pubt"].asLong,
|
publishTime = obj["pubt"].asLong,
|
||||||
message = GroupAnnouncementMessage(
|
message = GroupAnnouncementMessage(
|
||||||
text = obj["msg"].asJsonObject["text"].asString,
|
// text = obj["msg"].asJsonObject["text"].asString,
|
||||||
images = obj["msg"].asJsonObject["pics"].asJsonArrayOrNull?.map {
|
text = fromHtml(obj["msg"].asJsonObject["text"].asString),
|
||||||
|
images = obj["msg"].asJsonObject["pics"].asJsonArrayOrNull?.map { pic ->
|
||||||
GroupAnnouncementMessageImage(
|
GroupAnnouncementMessageImage(
|
||||||
id = it.jsonObject["id"].asString,
|
id = pic.jsonObject["id"].asString,
|
||||||
width = it.jsonObject["w"].asString,
|
width = pic.jsonObject["w"].asString,
|
||||||
height = it.jsonObject["h"].asString,
|
height = pic.jsonObject["h"].asString,
|
||||||
)
|
)
|
||||||
} ?: ArrayList()
|
} ?: 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)
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
suspend fun uploadImageTroopNotice(image: String): Result<GroupAnnouncementMessageImage> {
|
suspend fun uploadImageTroopNotice(image: String): Result<GroupAnnouncementMessageImage> {
|
||||||
val file = FileUtils.parseAndSave(image)
|
val file = FileUtils.parseAndSave(image)
|
||||||
|
@ -274,7 +274,7 @@ internal object MessageMaker {
|
|||||||
element.elementType = MsgConstant.KELEMTYPEREPLY
|
element.elementType = MsgConstant.KELEMTYPEREPLY
|
||||||
val reply = ReplyElement()
|
val reply = ReplyElement()
|
||||||
|
|
||||||
val msgHash = data["id"].asString.toInt()
|
val msgHash = data["id"].asInt
|
||||||
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
|
val mapping = MessageHelper.getMsgMappingByHash(msgHash)
|
||||||
?: return Result.failure(Exception("不存在该消息映射,无法回复消息"))
|
?: return Result.failure(Exception("不存在该消息映射,无法回复消息"))
|
||||||
|
|
||||||
@ -627,10 +627,16 @@ internal object MessageMaker {
|
|||||||
else -> {
|
else -> {
|
||||||
val info = GroupSvc.getTroopMemberInfoByUin(peerId, qq, true).onFailure {
|
val info = GroupSvc.getTroopMemberInfoByUin(peerId, qq, true).onFailure {
|
||||||
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
|
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
|
||||||
}.getOrThrow()
|
}.getOrNull()
|
||||||
at.content = "@${info.troopnick
|
if (info != null) {
|
||||||
.ifNullOrEmpty(info.friendnick)
|
at.content = "@${
|
||||||
.ifNullOrEmpty(qq)}"
|
info.troopnick
|
||||||
|
.ifNullOrEmpty(info.friendnick)
|
||||||
|
.ifNullOrEmpty(qq)
|
||||||
|
}"
|
||||||
|
} else {
|
||||||
|
at.content = "@${data["name"].asStringOrNull.ifNullOrEmpty(qq)}"
|
||||||
|
}
|
||||||
at.atType = MsgConstant.ATTYPEONE
|
at.atType = MsgConstant.ATTYPEONE
|
||||||
at.atNtUid = ContactHelper.getUidByUinAsync(qq.toLong())
|
at.atNtUid = ContactHelper.getUidByUinAsync(qq.toLong())
|
||||||
}
|
}
|
||||||
|
@ -331,7 +331,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
|
|||||||
MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
|
MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
|
||||||
val notify = tip.xmlElement
|
val notify = tip.xmlElement
|
||||||
when(notify.busiId) {
|
when(notify.busiId) {
|
||||||
/* 群戳一戳 */1061L -> {}
|
/* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
|
||||||
else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
|
else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact {
|
||||||
val peerId = if (MsgConstant.KCHATTYPEC2C == chatType || MsgConstant.KCHATTYPETEMPC2CFROMGROUP == chatType) {
|
val peerId = if (MsgConstant.KCHATTYPEC2C == chatType || MsgConstant.KCHATTYPETEMPC2CFROMGROUP == chatType) {
|
||||||
ContactHelper.getUidByUinAsync(id.toLong())
|
ContactHelper.getUidByUinAsync(id.toLong())
|
||||||
|
@ -2,11 +2,7 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
|
|||||||
|
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
|
||||||
import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo
|
import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.serialization.json.*
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.serialization.json.JsonArray
|
|
||||||
import kotlinx.serialization.json.JsonElement
|
|
||||||
import kotlinx.serialization.json.JsonObject
|
|
||||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||||
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments
|
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.remote.service.data.ForwardMessageResult
|
||||||
import moe.fuqiuluo.shamrock.tools.*
|
import moe.fuqiuluo.shamrock.tools.*
|
||||||
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
|
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() {
|
internal object SendForwardMessage : IActionHandler() {
|
||||||
override suspend fun internalHandle(session: ActionSession): String {
|
override suspend fun internalHandle(session: ActionSession): String {
|
||||||
@ -78,7 +59,7 @@ internal object SendForwardMessage : IActionHandler() {
|
|||||||
suspend operator fun invoke(
|
suspend operator fun invoke(
|
||||||
chatType: Int,
|
chatType: Int,
|
||||||
peerId: String,
|
peerId: String,
|
||||||
message: JsonArray,
|
messages: JsonArray,
|
||||||
echo: JsonElement = EmptyJsonString
|
echo: JsonElement = EmptyJsonString
|
||||||
): String {
|
): String {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
@ -87,79 +68,91 @@ internal object SendForwardMessage : IActionHandler() {
|
|||||||
val msgService = sessionService.msgService
|
val msgService = sessionService.msgService
|
||||||
val selfUin = TicketSvc.getUin()
|
val selfUin = TicketSvc.getUin()
|
||||||
|
|
||||||
val msgs = message.map {
|
val multiNodes = messages.map {
|
||||||
if (it.asJsonObject["type"].asStringOrNull != "node") return@map ForwardMsgNode.EmptyNode // 过滤非node类型消息段
|
if (it.asJsonObject["type"].asStringOrNull != "node") {
|
||||||
it.asJsonObject["data"].asJsonObject.let { data ->
|
LogCenter.log("包含非node类型节点", Level.WARN)
|
||||||
if (data.containsKey("content")) {
|
return@map null
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}.map {
|
if (it.asJsonObject["data"] !is JsonObject) {
|
||||||
if (it is ForwardMsgNode.MessageIdNode) {
|
LogCenter.log("data字段错误", Level.WARN)
|
||||||
val recordResult = MsgSvc.getMsg(it.id)
|
return@map null
|
||||||
if (!recordResult.isFailure) {
|
}
|
||||||
ForwardMsgNode.EmptyNode
|
it.asJsonObject["data"].asJsonObject.let { data ->
|
||||||
} else {
|
if (data.containsKey("id")) {
|
||||||
val record = recordResult.getOrThrow()
|
val record = MsgSvc.getMsg(data["id"].asInt).getOrNull()
|
||||||
ForwardMsgNode.MessageNode(
|
if (record == null) {
|
||||||
name = record.sendMemberName
|
LogCenter.log("合并转发消息节点消息获取失败:${data["id"]}", Level.WARN)
|
||||||
.ifBlank { record.sendNickName }
|
return@map null
|
||||||
.ifBlank { record.sendRemarkName }
|
} else {
|
||||||
.ifBlank { record.peerName },
|
record.peerName to record.toSegments().map { segment ->
|
||||||
content = record.toSegments().map { segment ->
|
|
||||||
segment.toJson()
|
segment.toJson()
|
||||||
}.json
|
}.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 from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin)
|
||||||
val to = MessageHelper.generateContact(chatType, peerId)
|
val to = MessageHelper.generateContact(chatType, peerId)
|
||||||
|
|
||||||
val uniseq = MessageHelper.generateMsgId(chatType)
|
val uniseq = MessageHelper.generateMsgId(chatType)
|
||||||
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>().apply {
|
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))
|
}.also { it.reverse() }, from, to, MsgSvc.MessageCallback(peerId, uniseq.first))
|
||||||
|
|
||||||
return ok(
|
return ok(
|
||||||
@ -174,7 +167,7 @@ internal object SendForwardMessage : IActionHandler() {
|
|||||||
return logic("合并转发消息失败(unknown error)", echo)
|
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"
|
override fun path(): String = "send_forward_msg"
|
||||||
}
|
}
|
@ -29,7 +29,7 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
|
|||||||
import moe.fuqiuluo.shamrock.tools.respond
|
import moe.fuqiuluo.shamrock.tools.respond
|
||||||
|
|
||||||
fun Routing.messageAction() {
|
fun Routing.messageAction() {
|
||||||
route("/send_group_forward_msg") {
|
route("/send_group_forward_(msg|message)".toRegex()) {
|
||||||
post {
|
post {
|
||||||
val groupId = fetchPostOrNull("group_id")
|
val groupId = fetchPostOrNull("group_id")
|
||||||
val messages = fetchPostJsonArray("messages")
|
val messages = fetchPostJsonArray("messages")
|
||||||
@ -40,7 +40,7 @@ fun Routing.messageAction() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
route("/send_private_forward_msg") {
|
route("/send_private_forward_(msg|message)".toRegex()) {
|
||||||
post {
|
post {
|
||||||
val userId = fetchPostOrNull("user_id")
|
val userId = fetchPostOrNull("user_id")
|
||||||
val messages = fetchPostJsonArray("messages")
|
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") {
|
getOrPost("/get_forward_msg") {
|
||||||
val id = fetchOrThrow("id")
|
val id = fetchOrThrow("id")
|
||||||
call.respondText(GetForwardMsg(id), ContentType.Application.Json)
|
call.respondText(GetForwardMsg(id), ContentType.Application.Json)
|
||||||
|
@ -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.RequestSubType
|
||||||
import moe.fuqiuluo.shamrock.remote.service.data.push.RequestType
|
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.Sender
|
||||||
|
import moe.fuqiuluo.shamrock.remote.service.data.push.SignDetail
|
||||||
import moe.fuqiuluo.shamrock.tools.ShamrockDsl
|
import moe.fuqiuluo.shamrock.tools.ShamrockDsl
|
||||||
import moe.fuqiuluo.shamrock.tools.json
|
import moe.fuqiuluo.shamrock.tools.json
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
@ -81,8 +82,8 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
|||||||
sender = Sender(
|
sender = Sender(
|
||||||
userId = record.senderUin,
|
userId = record.senderUin,
|
||||||
nickname = record.sendNickName
|
nickname = record.sendNickName
|
||||||
.ifBlank { record.sendMemberName }
|
|
||||||
.ifBlank { record.sendRemarkName }
|
.ifBlank { record.sendRemarkName }
|
||||||
|
.ifBlank { record.sendMemberName }
|
||||||
.ifBlank { record.peerName },
|
.ifBlank { record.peerName },
|
||||||
card = record.sendMemberName,
|
card = record.sendMemberName,
|
||||||
role = when (record.senderUin) {
|
role = when (record.senderUin) {
|
||||||
@ -222,6 +223,24 @@ internal object GlobalEventTransmitter: BaseSvc() {
|
|||||||
* 群聊通知 通知器
|
* 群聊通知 通知器
|
||||||
*/
|
*/
|
||||||
object GroupNoticeTransmitter {
|
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 {
|
suspend fun transGroupPoke(time: Long, operation: Long, target: Long, action: String?, suffix: String?, actionImg: String?, groupCode: Long): Boolean {
|
||||||
pushNotice(NoticeEvent(
|
pushNotice(NoticeEvent(
|
||||||
time = time,
|
time = time,
|
||||||
|
@ -4,6 +4,8 @@ import android.content.Intent
|
|||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import kotlinx.serialization.decodeFromString
|
import kotlinx.serialization.decodeFromString
|
||||||
import kotlinx.serialization.encodeToString
|
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.tools.GlobalJson5
|
||||||
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
|
import moe.fuqiuluo.shamrock.utils.MMKVFetcher
|
||||||
import mqq.app.MobileQQ
|
import mqq.app.MobileQQ
|
||||||
@ -15,10 +17,14 @@ internal object ShamrockConfig {
|
|||||||
if (it.exists()) it.delete()
|
if (it.exists()) it.delete()
|
||||||
it.mkdirs()
|
it.mkdirs()
|
||||||
}
|
}
|
||||||
private val Config: ServiceConfig by lazy {
|
private val Config = kotlin.runCatching {
|
||||||
GlobalJson5.decodeFromString(ConfigDir.resolve("config.json").also {
|
GlobalJson5.decodeFromString<ServiceConfig>(ConfigDir.resolve("config.json").also {
|
||||||
if (!it.exists()) it.writeText("{}")
|
if (!it.exists()) it.writeText("{}")
|
||||||
}.readText())
|
}.readText())
|
||||||
|
}.onFailure {
|
||||||
|
LogCenter.log("您的配置文件出现错误: ${it.stackTraceToString()}", Level.ERROR)
|
||||||
|
}.getOrElse {
|
||||||
|
ServiceConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isInit(): Boolean {
|
fun isInit(): Boolean {
|
||||||
|
@ -42,6 +42,8 @@ internal enum class NoticeSubType {
|
|||||||
@SerialName("kick_me") KickMe,
|
@SerialName("kick_me") KickMe,
|
||||||
|
|
||||||
@SerialName("poke") Poke,
|
@SerialName("poke") Poke,
|
||||||
|
@SerialName("sign") Sign,
|
||||||
|
|
||||||
|
|
||||||
@SerialName("title") Title,
|
@SerialName("title") Title,
|
||||||
@SerialName("delete") Delete,
|
@SerialName("delete") Delete,
|
||||||
@ -87,6 +89,9 @@ internal data class NoticeEvent(
|
|||||||
// 戳一戳
|
// 戳一戳
|
||||||
@SerialName("poke_detail") val pokeDetail: PokeDetail? = null,
|
@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? = "",
|
val suffix: String? = "",
|
||||||
@SerialName("action_img_url")
|
@SerialName("action_img_url")
|
||||||
val actionImg: String? = "https://tianquan.gtimg.cn/nudgeaction/item/0/expression.jpg",
|
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? = "",
|
||||||
)
|
)
|
@ -182,7 +182,9 @@ internal object AioListener: IKernelMsgListener {
|
|||||||
.updateMsgSeqByMsgHash(msgHash, record.msgSeq.toInt())
|
.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
|
return@launch
|
||||||
|
|
||||||
val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString())
|
val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString())
|
||||||
|
@ -63,28 +63,32 @@ internal object PrimitiveListener {
|
|||||||
subType = pb[1, 2, 2].asInt
|
subType = pb[1, 2, 2].asInt
|
||||||
}
|
}
|
||||||
val msgTime = pb[1, 2, 6].asLong
|
val msgTime = pb[1, 2, 6].asLong
|
||||||
when (msgType) {
|
try {
|
||||||
33 -> onGroupMemIncreased(msgTime, pb)
|
when (msgType) {
|
||||||
34 -> onGroupMemberDecreased(msgTime, pb)
|
33 -> onGroupMemIncreased(msgTime, pb)
|
||||||
44 -> onGroupAdminChange(msgTime, pb)
|
34 -> onGroupMemberDecreased(msgTime, pb)
|
||||||
84 -> onGroupApply(msgTime, pb)
|
44 -> onGroupAdminChange(msgTime, pb)
|
||||||
87 -> onInviteGroup(msgTime, pb)
|
84 -> onGroupApply(msgTime, pb)
|
||||||
528 -> when (subType) {
|
87 -> onInviteGroup(msgTime, pb)
|
||||||
35 -> onFriendApply(msgTime, pb)
|
528 -> when (subType) {
|
||||||
39 -> onCardChange(msgTime, pb)
|
35 -> onFriendApply(msgTime, pb)
|
||||||
// invite
|
39 -> onCardChange(msgTime, pb)
|
||||||
68 -> onGroupApply(msgTime, pb)
|
// invite
|
||||||
138 -> onC2CRecall(msgTime, pb)
|
68 -> onGroupApply(msgTime, pb)
|
||||||
290 -> onC2cPoke(msgTime, pb)
|
138 -> onC2CRecall(msgTime, pb)
|
||||||
}
|
290 -> onC2cPoke(msgTime, pb)
|
||||||
|
}
|
||||||
|
|
||||||
732 -> when (subType) {
|
732 -> when (subType) {
|
||||||
12 -> onGroupBan(msgTime, pb)
|
12 -> onGroupBan(msgTime, pb)
|
||||||
16 -> onGroupTitleChange(msgTime, pb)
|
16 -> onGroupTitleChange(msgTime, pb)
|
||||||
17 -> onGroupRecall(msgTime, pb)
|
17 -> onGroupRecall(msgTime, pb)
|
||||||
20 -> onGroupPoke(msgTime, pb)
|
20 -> onGroupPokeAndGroupSign(msgTime, pb)
|
||||||
21 -> onEssenceMessage(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) {
|
private suspend fun onCardChange(msgTime: Long, pb: ProtoMap) {
|
||||||
val targetId = pb[1, 3, 2, 1, 13, 2].asUtf8String
|
var detail = pb[1, 3, 2]
|
||||||
val newCardList = pb[1, 3, 2, 1, 13, 3].asList
|
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 = ""
|
var newCard = ""
|
||||||
newCardList
|
newCardList
|
||||||
.value
|
.value
|
||||||
@ -163,7 +180,7 @@ internal object PrimitiveListener {
|
|||||||
newCard = it[2].asUtf8String
|
newCard = it[2].asUtf8String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val groupId = pb[1, 3, 2, 1, 13, 4].asLong
|
val groupId = pb[1, 13, 4].asLong
|
||||||
var oldCard = ""
|
var oldCard = ""
|
||||||
val targetQQ = ContactHelper.getUinByUidAsync(targetId).toLong()
|
val targetQQ = ContactHelper.getUinByUidAsync(targetId).toLong()
|
||||||
LogCenter.log("群组[$groupId]成员$targetQQ 群名片变动 -> $newCard")
|
LogCenter.log("群组[$groupId]成员$targetQQ 群名片变动 -> $newCard")
|
||||||
@ -181,13 +198,18 @@ internal object PrimitiveListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onGroupTitleChange(msgTime: Long, pb: ProtoMap) {
|
private suspend fun onGroupTitleChange(msgTime: Long, pb: ProtoMap) {
|
||||||
val groupCode = pb[1, 1, 1].asULong
|
var detail = pb[1, 3, 2]
|
||||||
|
if (detail !is ProtoMap) {
|
||||||
val readPacket = ByteReadPacket(pb[1, 3, 2].asByteArray)
|
try {
|
||||||
val detail = if (readPacket.readBuf32Long() == groupCode) {
|
val readPacket = ByteReadPacket(detail.asByteArray)
|
||||||
readPacket.discardExact(1)
|
readPacket.readBuf32Long()
|
||||||
ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
|
readPacket.discardExact(1)
|
||||||
} else pb[1, 3, 2]
|
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
|
val targetUin = detail[5, 5].asLong
|
||||||
|
|
||||||
@ -212,13 +234,18 @@ internal object PrimitiveListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun onEssenceMessage(msgTime: Long, pb: ProtoMap) {
|
private suspend fun onEssenceMessage(msgTime: Long, pb: ProtoMap) {
|
||||||
val groupCode = pb[1, 1, 1].asULong
|
var detail = pb[1, 3, 2]
|
||||||
|
if (detail !is ProtoMap) {
|
||||||
val readPacket = ByteReadPacket(pb[1, 3, 2].asByteArray)
|
try {
|
||||||
val detail = if (readPacket.readBuf32Long() == groupCode) {
|
val readPacket = ByteReadPacket(detail.asByteArray)
|
||||||
readPacket.discardExact(1)
|
readPacket.readBuf32Long()
|
||||||
ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
|
readPacket.discardExact(1)
|
||||||
} else pb[1, 3, 2]
|
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 groupId = detail[4].asLong
|
||||||
val mesSeq = detail[37].asInt
|
val mesSeq = detail[37].asInt
|
||||||
@ -254,31 +281,26 @@ internal object PrimitiveListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private suspend fun onGroupPoke(time: Long, pb: ProtoMap) {
|
private suspend fun onGroupPokeAndGroupSign(time: Long, pb: ProtoMap) {
|
||||||
val groupCode1 = pb[1, 1, 1].asULong
|
var detail = pb[1, 3, 2]
|
||||||
|
|
||||||
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]
|
|
||||||
if (detail !is ProtoMap) {
|
if (detail !is ProtoMap) {
|
||||||
groupCode = groupCode2
|
try {
|
||||||
readPacket.discardExact(1)
|
val readPacket = ByteReadPacket(detail.asByteArray)
|
||||||
detail = ProtoUtils.decodeFromByteArray(readPacket.readBytes(readPacket.readShort().toInt()))
|
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 target: String
|
||||||
lateinit var operation: String
|
lateinit var operation: String
|
||||||
var action: String? = null
|
var action: String? = null
|
||||||
var suffix: String? = null
|
var suffix: String? = null
|
||||||
var actionImg: String? = null
|
var actionImg: String? = null
|
||||||
|
var rankImg: String? = null
|
||||||
|
val groupCode = detail[4].asULong
|
||||||
detail[26][7]
|
detail[26][7]
|
||||||
.asList
|
.asList
|
||||||
.value
|
.value
|
||||||
@ -287,18 +309,42 @@ internal object PrimitiveListener {
|
|||||||
when (it[1].asUtf8String) {
|
when (it[1].asUtf8String) {
|
||||||
"uin_str1" -> operation = value
|
"uin_str1" -> operation = value
|
||||||
"uin_str2" -> target = value
|
"uin_str2" -> target = value
|
||||||
|
// "nick_str1" -> operation_nick = value
|
||||||
|
// "nick_str2" -> operation_nick = value
|
||||||
"action_str" -> action = value
|
"action_str" -> action = value
|
||||||
"alt_str1" -> action = value
|
"alt_str1" -> action = value
|
||||||
"suffix_str" -> suffix = value
|
"suffix_str" -> suffix = value
|
||||||
"action_img_url" -> actionImg = 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
|
1068 -> {
|
||||||
.transGroupPoke(time, operation.toLong(), target.toLong(), action, suffix, actionImg, groupCode)
|
LogCenter.log("群打卡($groupCode): $action $target")
|
||||||
) {
|
if (!GlobalEventTransmitter.GroupNoticeTransmitter
|
||||||
LogCenter.log("群戳一戳推送失败!", Level.WARN)
|
.transGroupSign(time, target.toLong(), action, rankImg, groupCode)
|
||||||
|
) {
|
||||||
|
LogCenter.log("群打卡推送失败!", Level.WARN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
LogCenter.log("onGroupPokeAndGroupSign unknown type ${detail[2].asInt}", Level.WARN)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,16 +5,13 @@ import de.robv.android.xposed.IXposedHookLoadPackage
|
|||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||||
import de.robv.android.xposed.XposedBridge.log
|
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.utils.MMKVFetcher
|
||||||
import moe.fuqiuluo.shamrock.xposed.loader.ActionLoader
|
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.xposed.loader.LuoClassloader
|
||||||
import moe.fuqiuluo.shamrock.tools.FuzzySearchClass
|
import moe.fuqiuluo.shamrock.tools.FuzzySearchClass
|
||||||
import moe.fuqiuluo.shamrock.tools.afterHook
|
import moe.fuqiuluo.shamrock.tools.afterHook
|
||||||
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
import moe.fuqiuluo.shamrock.utils.PlatformUtils
|
||||||
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
|
|
||||||
import mqq.app.MobileQQ
|
import mqq.app.MobileQQ
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.lang.reflect.Modifier
|
import java.lang.reflect.Modifier
|
||||||
@ -42,7 +39,7 @@ internal class XposedEntry: IXposedHookLoadPackage {
|
|||||||
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) {
|
override fun handleLoadPackage(param: XC_LoadPackage.LoadPackageParam) {
|
||||||
when (param.packageName) {
|
when (param.packageName) {
|
||||||
PACKAGE_NAME_QQ -> entryMQQ(param.classLoader)
|
PACKAGE_NAME_QQ -> entryMQQ(param.classLoader)
|
||||||
"android" -> FuckAMS.injectAMS(param.classLoader)
|
"android" -> KeepAlive(param.classLoader)
|
||||||
PACKAGE_NAME_TIM -> entryTim(param.classLoader)
|
PACKAGE_NAME_TIM -> entryTim(param.classLoader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,14 +7,12 @@ import android.content.pm.VersionedPackage
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import de.robv.android.xposed.XC_MethodReplacement
|
import de.robv.android.xposed.XC_MethodReplacement
|
||||||
import de.robv.android.xposed.XSharedPreferences
|
import de.robv.android.xposed.XSharedPreferences
|
||||||
import de.robv.android.xposed.XposedBridge
|
|
||||||
import de.robv.android.xposed.XposedHelpers
|
import de.robv.android.xposed.XposedHelpers
|
||||||
import moe.fuqiuluo.shamrock.helper.Level
|
import moe.fuqiuluo.shamrock.helper.Level
|
||||||
import moe.fuqiuluo.shamrock.helper.LogCenter
|
import moe.fuqiuluo.shamrock.helper.LogCenter
|
||||||
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
||||||
import moe.fuqiuluo.shamrock.tools.hookMethod
|
import moe.fuqiuluo.shamrock.tools.hookMethod
|
||||||
import moe.fuqiuluo.shamrock.xposed.XposedEntry
|
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.LuoClassloader
|
||||||
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
|
import moe.fuqiuluo.shamrock.xposed.loader.NativeLoader
|
||||||
|
|
||||||
@ -68,6 +66,8 @@ class AntiDetection: IAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun antiFindPackage(context: Context) {
|
private fun antiFindPackage(context: Context) {
|
||||||
|
if (isAntiFindPackage) return
|
||||||
|
|
||||||
val packageManager = context.packageManager
|
val packageManager = context.packageManager
|
||||||
val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock", 0)
|
val applicationInfo = packageManager.getApplicationInfo("moe.fuqiuluo.shamrock", 0)
|
||||||
val packageInfo = packageManager.getPackageInfo("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
|
val packageName = it.args[0] as String
|
||||||
if(packageName == "moe.fuqiuluo.shamrock") {
|
if(packageName == "moe.fuqiuluo.shamrock") {
|
||||||
LogCenter.log("AntiDetection: 检测到对Shamrock的检测,欺骗PackageManager(GA)", Level.WARN)
|
LogCenter.log("AntiDetection: 检测到对Shamrock的检测,欺骗PackageManager(GA)", Level.WARN)
|
||||||
it.throwable = PackageManager.NameNotFoundException()
|
it.throwable = PackageManager.NameNotFoundException("Hided")
|
||||||
} else if (packageName == "moe.fuqiuluo.shamrock.hided") {
|
} else if (packageName == "moe.fuqiuluo.shamrock.hided") {
|
||||||
it.result = applicationInfo
|
it.result = applicationInfo
|
||||||
}
|
}
|
||||||
@ -102,6 +102,8 @@ class AntiDetection: IAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAntiFindPackage = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun antiMemoryWalking() {
|
private fun antiMemoryWalking() {
|
||||||
@ -207,4 +209,9 @@ class AntiDetection: IAction {
|
|||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
var isAntiFindPackage = false
|
||||||
|
}
|
||||||
}
|
}
|
@ -3,14 +3,15 @@ package moe.fuqiuluo.shamrock.xposed.loader
|
|||||||
|
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.arthenica.ffmpegkit.BuildConfig
|
import android.os.Process
|
||||||
import de.robv.android.xposed.XSharedPreferences
|
import de.robv.android.xposed.XSharedPreferences
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.XposedHelpers
|
import de.robv.android.xposed.XposedHelpers
|
||||||
import moe.fuqiuluo.shamrock.tools.hookMethod
|
import moe.fuqiuluo.shamrock.tools.hookMethod
|
||||||
import java.lang.reflect.Method
|
import java.lang.reflect.Method
|
||||||
|
import kotlin.concurrent.timer
|
||||||
|
|
||||||
internal object FuckAMS {
|
internal object KeepAlive {
|
||||||
private val KeepPackage = arrayOf(
|
private val KeepPackage = arrayOf(
|
||||||
"com.tencent.mobileqq", "moe.fuqiuluo.shamrock"
|
"com.tencent.mobileqq", "moe.fuqiuluo.shamrock"
|
||||||
)
|
)
|
||||||
@ -20,7 +21,46 @@ internal object FuckAMS {
|
|||||||
private lateinit var METHOD_IS_KILLED: Method
|
private lateinit var METHOD_IS_KILLED: Method
|
||||||
private var allowPersistent: Boolean = false
|
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 {
|
kotlin.runCatching {
|
||||||
val ActivityManagerService = XposedHelpers.findClass("com.android.server.am.ActivityManagerService", loader)
|
val ActivityManagerService = XposedHelpers.findClass("com.android.server.am.ActivityManagerService", loader)
|
||||||
ActivityManagerService.hookMethod("newProcessRecordLocked").after {
|
ActivityManagerService.hookMethod("newProcessRecordLocked").after {
|
||||||
@ -30,7 +70,6 @@ internal object FuckAMS {
|
|||||||
XposedBridge.log("[Shamrock] Plan A failed: ${it.message}")
|
XposedBridge.log("[Shamrock] Plan A failed: ${it.message}")
|
||||||
}
|
}
|
||||||
|
|
||||||
val pref = XSharedPreferences("moe.fuqiuluo.shamrock", "shared_config")
|
|
||||||
if (pref.file.canRead()) {
|
if (pref.file.canRead()) {
|
||||||
allowPersistent = pref.getBoolean("persistent", false)
|
allowPersistent = pref.getBoolean("persistent", false)
|
||||||
XposedBridge.log("[Shamrock] allowPersistent = $allowPersistent")
|
XposedBridge.log("[Shamrock] allowPersistent = $allowPersistent")
|
Loading…
x
Reference in New Issue
Block a user