diff --git a/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt b/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt index 3c72939098..b41bc26726 100644 --- a/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt +++ b/app/src/main/java/me/iacn/biliroaming/BiliBiliPackage.kt @@ -176,6 +176,9 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex val httpUrlClass by Weak { mHookInfo.okHttp.httpUrl.class_ from mClassLoader } val preBuiltConfigClass by Weak { mHookInfo.preBuiltConfig.class_ from mClassLoader } val dataSPClass by Weak { mHookInfo.dataSP.class_ from mClassLoader } + val fastjsonFieldAnnotation by Weak { "com.alibaba.fastjson.annotation.JSONField" from mClassLoader } + val gsonFieldAnnotation by Weak { "com.google.gson.annotations.SerializedName" from mClassLoader } + val pegasusParserClass by Weak { mHookInfo.pegasusParser from mClassLoader } // for v8.17.0+ val useNewMossFunc = instance.viewMossClass?.declaredMethods?.any { @@ -2310,6 +2313,22 @@ class BiliBiliPackage constructor(private val mClassLoader: ClassLoader, mContex class_ = class_ { name = getDataSP.declaringClass.name } get = method { name = getDataSP.name } } + pegasusParser = class_ { + val getPegasusParser = dexHelper.findMethodUsingString( + "[Pegasus]PegasusParser", + false, + -1, + -1, + null, + -1, + null, + null, + null, + true + ).asSequence().mapNotNull { dexHelper.decodeMethodIndex(it) }.firstOrNull() + ?: return@class_ + name = getPegasusParser.declaringClass.name + } dexHelper.close() } diff --git a/app/src/main/java/me/iacn/biliroaming/hook/PegasusHook.kt b/app/src/main/java/me/iacn/biliroaming/hook/PegasusHook.kt index 994a3be33e..d1d9c2681f 100644 --- a/app/src/main/java/me/iacn/biliroaming/hook/PegasusHook.kt +++ b/app/src/main/java/me/iacn/biliroaming/hook/PegasusHook.kt @@ -1,7 +1,14 @@ package me.iacn.biliroaming.hook +import de.robv.android.xposed.XC_MethodHook import me.iacn.biliroaming.BiliBiliPackage.Companion.instance import me.iacn.biliroaming.utils.* +import me.iacn.biliroaming.utils.json.FastjsonHelper +import me.iacn.biliroaming.utils.json.GsonHelper +import me.iacn.biliroaming.utils.json.JsonHelper +import me.iacn.biliroaming.utils.json.getObjectAs +import me.iacn.biliroaming.utils.json.getObjectAsHelperOrNull +import me.iacn.biliroaming.utils.json.toJsonHelper class PegasusHook(classLoader: ClassLoader) : BaseHook(classLoader) { private val hidden = sPrefs.getBoolean("hidden", false) @@ -106,10 +113,10 @@ class PegasusHook(classLoader: ClassLoader) : BaseHook(classLoader) { } ?: -1L // 屏蔽过低的播放数 - private fun isLowCountVideo(obj: Any): Boolean { + private fun isLowCountVideo(obj: JsonHelper): Boolean { if (hideLowPlayCountLimit == 0L) return false val text = obj.runCatchingOrNull { - getObjectFieldAs("coverLeftText1") + getObjectAs("cover_left_text_1") }.orEmpty() return text.toPlayCount().let { if (it == -1L) false @@ -122,11 +129,11 @@ class PegasusHook(classLoader: ClassLoader) : BaseHook(classLoader) { } // 屏蔽指定播放时长 - private fun durationVideo(obj: Any): Boolean { + private fun durationVideo(obj: JsonHelper): Boolean { if (hideLongDurationLimit == 0 && hideShortDurationLimit == 0) return false - val duration = obj.getObjectField("playerArgs") - ?.getObjectFieldAs("fakeDuration") ?: 0 + val duration = obj.getObjectAsHelperOrNull("player_args") + ?.getObjectAs("duration") ?: 0 if (hideLongDurationLimit != 0 && duration > hideLongDurationLimit) return true return hideShortDurationLimit != 0 && duration < hideShortDurationLimit @@ -139,10 +146,10 @@ class PegasusHook(classLoader: ClassLoader) : BaseHook(classLoader) { return hideShortDurationLimit != 0 && duration < hideShortDurationLimit } - private fun isContainsBlockKwd(obj: Any): Boolean { + private fun isContainsBlockKwd(obj: JsonHelper): Boolean { // 屏蔽标题关键词 if (kwdFilterTitleList.isNotEmpty()) { - val title = obj.getObjectFieldAs("title").orEmpty() + val title = obj.getObjectAs("title").orEmpty() if (kwdFilterTitleRegexMode && title.isNotEmpty()) { if (kwdFilterTitleRegexes.any { title.contains(it) }) return true @@ -154,17 +161,17 @@ class PegasusHook(classLoader: ClassLoader) : BaseHook(classLoader) { // 屏蔽UID if (kwdFilterUidList.isNotEmpty()) { - val uid = obj.getObjectField("args")?.getLongField("upId") ?: 0L + val uid = obj.getObjectAsHelperOrNull("args")?.getObjectAs("up_id") ?: 0L if (uid != 0L && kwdFilterUidList.any { it == uid }) return true } // 屏蔽UP主 if (kwdFilterUpnameList.isNotEmpty()) { - val upname = if (obj.getObjectField("goTo") == "picture") { - obj.runCatchingOrNull { getObjectFieldAs("desc") }.orEmpty() + val upname = if (obj.getObjectAs("goto") == "picture") { + obj.runCatchingOrNull { getObjectAs("desc") }.orEmpty() } else { - obj.getObjectField("args")?.getObjectFieldAs("upName").orEmpty() + obj.getObjectAsHelperOrNull("args")?.getObjectAs("up_name").orEmpty() } if (kwdFilterUpnameRegexMode && upname.isNotEmpty()) { if (kwdFilterUpnameRegexes.any { upname.contains(it) }) @@ -177,16 +184,16 @@ class PegasusHook(classLoader: ClassLoader) : BaseHook(classLoader) { // 屏蔽分区 if (kwdFilterRnameList.isNotEmpty()) { - val rname = obj.getObjectField("args") - ?.getObjectFieldAs("rname").orEmpty() + val rname = obj.getObjectAsHelperOrNull("args") + ?.getObjectAs("rname").orEmpty() if (rname.isNotEmpty() && kwdFilterRnameList.any { rname.contains(it) }) return true } // 屏蔽频道 if (kwdFilterTnameList.isNotEmpty()) { - val tname = obj.getObjectField("args") - ?.getObjectFieldAs("tname").orEmpty() + val tname = obj.getObjectAsHelperOrNull("args") + ?.getObjectAs("tname").orEmpty() if (tname.isNotEmpty() && kwdFilterTnameList.any { tname.contains(it) }) return true } @@ -194,7 +201,7 @@ class PegasusHook(classLoader: ClassLoader) : BaseHook(classLoader) { // 屏蔽推荐关键词(可能不存在,必须放最后) if (kwdFilterReasonList.isNotEmpty()) { val reason = obj.runCatchingOrNull { - getObjectField("rcmdReason")?.getObjectFieldAs("text").orEmpty() + getObjectAsHelperOrNull("rcmd_reason")?.getObjectAs("text").orEmpty() }.orEmpty() if (kwdFilterReasonRegexMode && reason.isNotEmpty()) { if (kwdFilterReasonRegexes.any { reason.contains(it) }) @@ -248,25 +255,26 @@ class PegasusHook(classLoader: ClassLoader) : BaseHook(classLoader) { return false } - private fun ArrayList.appendReasons() = forEach { item -> - val title = item.getObjectFieldAs("title").orEmpty() + private fun ArrayList.appendReasons(jsonHelper: Class) = this.forEach { + val item = it.toJsonHelper(jsonHelper) + val title = item.getObjectAs("title").orEmpty() val rcmdReason = item.runCatchingOrNull { - getObjectField("rcmdReason")?.getObjectFieldAs("text") + getObjectAsHelperOrNull("rcmd_reason")?.getObjectAs("text") }.orEmpty() - val args = item.getObjectField("args") - val upId = args?.getLongField("upId") ?: 0L - val upName = if (item.getObjectField("goTo") == "picture") { - item.runCatchingOrNull { getObjectFieldAs("desc") }.orEmpty() + val args = item.getObjectAsHelperOrNull("args") + val upId = args?.getObjectAs("up_id") ?: 0L + val upName = if (item.getObject("goto") == "picture") { + item.runCatchingOrNull { getObjectAs("desc") }.orEmpty() } else { - args?.getObjectFieldAs("upName").orEmpty() + args?.getObjectAs("up_name").orEmpty() } - val categoryName = args?.getObjectFieldAs("rname").orEmpty() - val channelName = args?.getObjectFieldAs("tname").orEmpty() - val treePoint = item.getObjectFieldAs?>("threePoint") + val categoryName = args?.getObjectAs("rname").orEmpty() + val channelName = args?.getObjectAs("tname").orEmpty() + val treePoint = item.getObjectAs?>("three_point_v2") val reasons = mutableListOf() instance.treePointItemClass?.new()?.apply { setObjectField("title", "漫游屏蔽") - setObjectField("subtitle", "(本地屏蔽,重启生效,可前往首页推送过滤器查看)") + setObjectField("subtitle", "(本地屏蔽,重启生效,可前往首页推送过滤器查看)") setObjectField("type", "dislike") if (title.isNotEmpty()) { instance.dislikeReasonClass?.new()?.apply { @@ -507,29 +515,45 @@ class PegasusHook(classLoader: ClassLoader) : BaseHook(classLoader) { override fun startHook() { Log.d("startHook: Pegasus") - instance.pegasusFeedClass?.hookAfterMethod( - instance.pegasusFeed(), - instance.responseBodyClass - ) { param -> - param.result ?: return@hookAfterMethod - val data = param.result.getObjectField("data") - data?.getObjectField("config")?.apply { + + fun hookPegasusFeedConvert(json: JsonHelper) { + json.getObject("config")!!.apply { disableAutoRefresh() customSmallCoverWhRatio() } - if (!hidden) return@hookAfterMethod - data?.getObjectFieldAs>("items")?.run { + if (!hidden) return + json.getObjectAs>("items").run { removeAll { - val cardGoto = it.getObjectFieldAs("cardGoto").orEmpty() - val cardType = it.getObjectFieldAs("cardType").orEmpty() - val goto = it.getObjectFieldAs("goTo").orEmpty() - filter.any { item -> - item in cardGoto || item in cardType || item in goto - } || isLowCountVideo(it) || isContainsBlockKwd(it) || durationVideo(it) + val item = it.toJsonHelper(json.javaClass) + val cardGoto = item.getObjectAs("card_goto").orEmpty() + val cardType = item.getObjectAs("card_type").orEmpty() + val goto = item.getObjectAs("goto").orEmpty() + filter.any { + it in cardGoto || it in cardType || it in goto + } || isLowCountVideo(item) || isContainsBlockKwd(item) || durationVideo(item) } - appendReasons() + appendReasons(json.javaClass) } } + + instance.pegasusFeedClass?.hookAfterMethod( + "convert", + Object::class.java + ) { param -> + val data = param.result?.getObjectField("data") ?: return@hookAfterMethod + val json = data.toJsonHelper() + hookPegasusFeedConvert(json) + } + + instance.pegasusParserClass?.hookAfterMethod( + "convert", + Object::class.java + ) { param -> + val data = param.result?.getObjectField("data") ?: return@hookAfterMethod + val json = data.toJsonHelper() + hookPegasusFeedConvert(json) + } + if (!hidden) return fun MutableList.filter() = removeAll { isPromoteRelate(it) || isNotAvRelate(it) || (applyToRelate && (isLowCountRelate(it) @@ -650,50 +674,61 @@ class PegasusHook(classLoader: ClassLoader) : BaseHook(classLoader) { } } - instance.cardClickProcessorClass?.declaredMethods - ?.find { it.name == instance.onFeedClicked() }?.hookBeforeMethod { param -> - val reason = param.args[2] - if (reason == null || reason.getLongField("id") !in blockReasonIds) - return@hookBeforeMethod - val id = reason.getLongField("id") - val name = reason.getObjectFieldAs("name").orEmpty() - val value = name.substringAfter(":") - when (id) { - REASON_ID_TITLE -> { - val validValue = - if (kwdFilterTitleRegexMode) Regex.escape(value) else value - sPrefs.appendStringForSet("home_filter_keywords_title", validValue) - } + fun hookDislikeReason(reason: Any?): Boolean { + if (reason == null || reason.getLongField("id") !in blockReasonIds) + return false + val id = reason.getLongField("id") + val name = reason.getObjectFieldAs("name").orEmpty() + val value = name.substringAfter(":") + when (id) { + REASON_ID_TITLE -> { + val validValue = + if (kwdFilterTitleRegexMode) Regex.escape(value) else value + sPrefs.appendStringForSet("home_filter_keywords_title", validValue) + } - REASON_ID_RCMD_REASON -> { - val validValue = - if (kwdFilterReasonRegexMode) Regex.escape(value) else value - sPrefs.appendStringForSet("home_filter_keywords_reason", validValue) - } + REASON_ID_RCMD_REASON -> { + val validValue = + if (kwdFilterReasonRegexMode) Regex.escape(value) else value + sPrefs.appendStringForSet("home_filter_keywords_reason", validValue) + } - REASON_ID_UP_ID -> { - sPrefs.appendStringForSet("home_filter_keywords_uid", value) - } + REASON_ID_UP_ID -> { + sPrefs.appendStringForSet("home_filter_keywords_uid", value) + } - REASON_ID_UP_NAME -> { - val validValue = - if (kwdFilterUpnameRegexMode) Regex.escape(value) else value - sPrefs.appendStringForSet("home_filter_keywords_up", validValue) - } + REASON_ID_UP_NAME -> { + val validValue = + if (kwdFilterUpnameRegexMode) Regex.escape(value) else value + sPrefs.appendStringForSet("home_filter_keywords_up", validValue) + } - REASON_ID_CATEGORY_NAME -> { - sPrefs.appendStringForSet("home_filter_keywords_category", value) - } + REASON_ID_CATEGORY_NAME -> { + sPrefs.appendStringForSet("home_filter_keywords_category", value) + } - REASON_ID_CHANNEL_NAME -> { - sPrefs.appendStringForSet("home_filter_keywords_channel", value) - } + REASON_ID_CHANNEL_NAME -> { + sPrefs.appendStringForSet("home_filter_keywords_channel", value) } - Log.toast("添加成功", force = true) - param.result = null } + Log.toast("添加成功", force = true) + return true + } + instance.cardClickProcessorClass?.declaredMethods + ?.find { it.name == instance.onFeedClicked() }?.hookBeforeMethod { param -> + if (hookDislikeReason(param.args[2])) { + param.result = null + } + } + "com.bilibili.pegasus.ext.threepoint.ThreePointKt".findClass(mClassLoader) + .declaredMethods.find { it.parameterTypes.size == 8 } + ?.hookBeforeMethod { + if (hookDislikeReason(it.args[3])) { + it.result = null + } + } fun MutableList.filterPopular() = removeIf { when (it.callMethod("getItemCase")?.toString()) { diff --git a/app/src/main/java/me/iacn/biliroaming/utils/Utils.kt b/app/src/main/java/me/iacn/biliroaming/utils/Utils.kt index f44a774170..00e8921a62 100644 --- a/app/src/main/java/me/iacn/biliroaming/utils/Utils.kt +++ b/app/src/main/java/me/iacn/biliroaming/utils/Utils.kt @@ -24,6 +24,7 @@ import java.io.File import java.io.IOException import java.io.InputStream import java.lang.ref.WeakReference +import java.lang.reflect.Field import java.lang.reflect.Proxy import java.math.BigInteger import java.net.URL @@ -444,7 +445,7 @@ fun Any.dumpToString(): String { val sb = StringBuilder() sb.append("---- ${this.javaClass.name}@${this.hashCode().toHexString()} dump begin ----\n") fun dumpClassFields(clazz: Class<*>) { - clazz.fields.forEach { field -> + clazz.declaredFields.forEach { field -> if (field.isStatic) return@forEach field.isAccessible = true val v = field.get(this) @@ -458,3 +459,10 @@ fun Any.dumpToString(): String { sb.append("---- ${this.javaClass.name}@${this.hashCode().toHexString()} dump end ----") return sb.toString() } + +fun Class<*>.findField(filter: (Field) -> Boolean): Field? { + return declaredFields.find { field -> + field.isAccessible = true + filter(field) + } ?: superclass?.findField(filter) +} diff --git a/app/src/main/java/me/iacn/biliroaming/utils/json/FastjsonHelper.kt b/app/src/main/java/me/iacn/biliroaming/utils/json/FastjsonHelper.kt new file mode 100644 index 0000000000..814d6be520 --- /dev/null +++ b/app/src/main/java/me/iacn/biliroaming/utils/json/FastjsonHelper.kt @@ -0,0 +1,24 @@ +package me.iacn.biliroaming.utils.json + +import me.iacn.biliroaming.BiliBiliPackage.Companion.instance +import me.iacn.biliroaming.utils.callMethod +import me.iacn.biliroaming.utils.findField +import java.lang.reflect.Field + +class FastjsonHelper : JsonHelper { + private lateinit var _data: Any + override var data: Any + get() = _data + set(value) { + _data = value + } + + override fun getField(key: String): Field { + val field = data.javaClass.findField{ field -> + val annotation = field.getAnnotation(instance.fastjsonFieldAnnotation as Class) ?: return@findField false + annotation.callMethod("name") == key + } ?: throw NoSuchFieldException("No field found for key: $key in ${data.javaClass.name} or its superclasses") + return field + } + +} \ No newline at end of file diff --git a/app/src/main/java/me/iacn/biliroaming/utils/json/GsonHelper.kt b/app/src/main/java/me/iacn/biliroaming/utils/json/GsonHelper.kt new file mode 100644 index 0000000000..cf6ec25447 --- /dev/null +++ b/app/src/main/java/me/iacn/biliroaming/utils/json/GsonHelper.kt @@ -0,0 +1,27 @@ +package me.iacn.biliroaming.utils.json + +import me.iacn.biliroaming.BiliBiliPackage.Companion.instance +import me.iacn.biliroaming.utils.Log +import me.iacn.biliroaming.utils.callMethod +import me.iacn.biliroaming.utils.findField +import java.lang.reflect.Field + +class GsonHelper : JsonHelper { + lateinit var _data: Any + + override var data: Any + get() = _data + set(value) { + _data = value + } + + override fun getField(key: String): Field { + val field = data.javaClass.findField { field -> + val annotation = field.annotations.find { + it.toString().startsWith("@com.google.gson.annotations.SerializedName(") + } + annotation?.callMethod("value") == key + } ?: throw NoSuchFieldException("No field found for key: $key in ${data.javaClass.name} or its superclasses") + return field + } +} \ No newline at end of file diff --git a/app/src/main/java/me/iacn/biliroaming/utils/json/JsonHelper.kt b/app/src/main/java/me/iacn/biliroaming/utils/json/JsonHelper.kt new file mode 100644 index 0000000000..d485872128 --- /dev/null +++ b/app/src/main/java/me/iacn/biliroaming/utils/json/JsonHelper.kt @@ -0,0 +1,51 @@ +package me.iacn.biliroaming.utils.json + +import java.lang.reflect.Field + +interface JsonHelper { + var data: Any + fun getField(key: String): Field + fun getObject(key: String): Any? { + return getField(key).get(data) + } + + fun getObjectAsHelper(key: String): JsonHelper { + return getObject(key)!!.toJsonHelper(this.javaClass) + } + + fun setObject(key: String, value: Any) { + getField(key).set(data, value) + } +} + +fun JsonHelper.getObjectAs(key: String): T { + return getObject(key) as T +} + +fun JsonHelper.getObjectOrNull(key: String): Any? { + return try { + getObject(key) + } catch (e: NoSuchFieldException) { + null + } +} + +fun JsonHelper.getObjectAsHelperOrNull(key: String): JsonHelper? { + return try { + getObjectAsHelper(key) + } catch (e: NoSuchFieldException) { + null + } +} + +inline fun Any.toJsonHelper(): T { + val jsonHelper = T::class.java.newInstance() as T + jsonHelper.data = this + return jsonHelper +} + +fun Any.toJsonHelper(jsonHelper: Class): JsonHelper { + return jsonHelper.newInstance().apply { + data = this@toJsonHelper + } +} diff --git a/app/src/main/proto/me/iacn/biliroaming/configs.proto b/app/src/main/proto/me/iacn/biliroaming/configs.proto index ffdbcb1274..d4617ede42 100644 --- a/app/src/main/proto/me/iacn/biliroaming/configs.proto +++ b/app/src/main/proto/me/iacn/biliroaming/configs.proto @@ -380,4 +380,5 @@ message HookInfo { optional LiveQuality liveQuality = 98; optional PreBuiltConfig preBuiltConfig = 99; optional DataSP dataSP = 100; + optional Class pegasus_parser = 101; }