From 3dbc5ff2722559211232999ae29e7fabafe3b857 Mon Sep 17 00:00:00 2001 From: oSumAtrIX Date: Wed, 22 Feb 2023 05:37:52 +0100 Subject: [PATCH] fix(twitter): make `hide-promoted-ads` patch compatible with any version Signed-off-by: oSumAtrIX --- .../TimelineTweetJsonParserFingerprint.kt | 18 --- .../ad/timeline/patch/TimelineAdsPatch.kt | 77 ------------ .../fingerprints/JsonHookPatchFingerprint.kt | 9 ++ .../JsonInputStreamFingerprint.kt | 10 ++ .../fingerprints/LoganSquareFingerprint.kt | 7 ++ .../misc/hook/json/patch/JsonHookPatch.kt | 114 ++++++++++++++++++ .../misc/hook/patch/BaseHookPatchPatch.kt | 17 +++ .../ads/annotations/HideAdsCompatibility.kt} | 6 +- .../misc/hook/patch/ads/patch/HideAdsPatch.kt | 22 ++++ 9 files changed, 182 insertions(+), 98 deletions(-) delete mode 100644 src/main/kotlin/app/revanced/patches/twitter/ad/timeline/fingerprints/TimelineTweetJsonParserFingerprint.kt delete mode 100644 src/main/kotlin/app/revanced/patches/twitter/ad/timeline/patch/TimelineAdsPatch.kt create mode 100644 src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/JsonHookPatchFingerprint.kt create mode 100644 src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/JsonInputStreamFingerprint.kt create mode 100644 src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/LoganSquareFingerprint.kt create mode 100644 src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/patch/JsonHookPatch.kt create mode 100644 src/main/kotlin/app/revanced/patches/twitter/misc/hook/patch/BaseHookPatchPatch.kt rename src/main/kotlin/app/revanced/patches/twitter/{ad/timeline/annotations/TimelineAdsCompatibility.kt => misc/hook/patch/ads/annotations/HideAdsCompatibility.kt} (55%) create mode 100644 src/main/kotlin/app/revanced/patches/twitter/misc/hook/patch/ads/patch/HideAdsPatch.kt diff --git a/src/main/kotlin/app/revanced/patches/twitter/ad/timeline/fingerprints/TimelineTweetJsonParserFingerprint.kt b/src/main/kotlin/app/revanced/patches/twitter/ad/timeline/fingerprints/TimelineTweetJsonParserFingerprint.kt deleted file mode 100644 index 5d4b98732..000000000 --- a/src/main/kotlin/app/revanced/patches/twitter/ad/timeline/fingerprints/TimelineTweetJsonParserFingerprint.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.revanced.patches.twitter.ad.timeline.fingerprints - -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint -import org.jf.dexlib2.Opcode - -object TimelineTweetJsonParserFingerprint : MethodFingerprint( - opcodes = listOf( - Opcode.IPUT_OBJECT, - Opcode.GOTO, - Opcode.SGET_OBJECT, - Opcode.INVOKE_VIRTUAL, - Opcode.MOVE_RESULT_OBJECT, - Opcode.CHECK_CAST, - Opcode.IPUT_OBJECT, - Opcode.RETURN_VOID, - ), strings = listOf("tweetPromotedMetadata", "promotedMetadata", "hasModeratedReplies", "conversationAnnotation"), - customFingerprint = { methodDef -> methodDef.name == "parseField" } -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/twitter/ad/timeline/patch/TimelineAdsPatch.kt b/src/main/kotlin/app/revanced/patches/twitter/ad/timeline/patch/TimelineAdsPatch.kt deleted file mode 100644 index 753a2ea05..000000000 --- a/src/main/kotlin/app/revanced/patches/twitter/ad/timeline/patch/TimelineAdsPatch.kt +++ /dev/null @@ -1,77 +0,0 @@ -package app.revanced.patches.twitter.ad.timeline.patch - -import app.revanced.patcher.annotation.Description -import app.revanced.patcher.annotation.Name -import app.revanced.patcher.annotation.Version -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.addInstructions -import app.revanced.patcher.extensions.instruction -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprintResult -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.PatchResult -import app.revanced.patcher.patch.PatchResultError -import app.revanced.patcher.patch.PatchResultSuccess -import app.revanced.patcher.patch.annotations.Patch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.twitter.ad.timeline.annotations.TimelineAdsCompatibility -import app.revanced.patches.twitter.ad.timeline.fingerprints.TimelineTweetJsonParserFingerprint -import org.jf.dexlib2.Opcode -import org.jf.dexlib2.builder.BuilderInstruction -import org.jf.dexlib2.builder.instruction.BuilderInstruction22c -import org.jf.dexlib2.iface.instruction.ReferenceInstruction -import org.jf.dexlib2.iface.reference.FieldReference -import org.jf.dexlib2.iface.reference.StringReference - -@Patch -@Name("timeline-ads") -@Description("Removes ads from the Twitter timeline. Might require clearing app data to remove already cached ads.") -@TimelineAdsCompatibility -@Version("0.0.1") -class TimelineAdsPatch : BytecodePatch( - listOf(TimelineTweetJsonParserFingerprint) -) { - override fun execute(context: BytecodeContext): PatchResult { - if (removePromotedAds()) - return PatchResultError("The instruction for the tweet id field could not be found") - - return PatchResultSuccess() - } - - private fun removePromotedAds(): Boolean { - val (parserFingerprintResult, parserMethod, instructions) = TimelineTweetJsonParserFingerprint.unwrap() - - // Anchor index - val tweetIdFieldInstructionIndex = instructions.indexOfFirst { instruction -> - if (instruction.opcode.ordinal != Opcode.CONST_STRING.ordinal) return@indexOfFirst false - if (((instruction as? ReferenceInstruction)?.reference as StringReference).string != "tweetSocialProof") return@indexOfFirst false - - // Use the above conditions as an anchor to find the index for the instruction with the field we need - return@indexOfFirst true - } - 2 // This is where the instruction with the field is located - - // Reference to the tweetId field for of the timeline tweet - val tweetIdFieldReference = - (parserMethod.instruction(tweetIdFieldInstructionIndex) as? BuilderInstruction22c)?.reference as? FieldReference - ?: return true - - // Set the tweetId field to null - // This will cause twitter to not show the promoted ads, because we set it to null, when the tweet is promoted - parserFingerprintResult.mutableMethod.addInstructions( - parserFingerprintResult.scanResult.patternScanResult!!.startIndex + 1, - """ - const/4 v2, 0x0 - iput-object v2, p0, Lcom/twitter/model/json/timeline/urt/JsonTimelineTweet;->${tweetIdFieldReference.name}:Ljava/lang/String; - """ - ) - return false - } - - private fun MethodFingerprint.unwrap(): Triple> { - val parserFingerprintResult = this.result!! - val parserMethod = parserFingerprintResult.mutableMethod - val instructions = parserMethod.implementation!!.instructions - - return Triple(parserFingerprintResult, parserMethod, instructions) - } -} diff --git a/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/JsonHookPatchFingerprint.kt b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/JsonHookPatchFingerprint.kt new file mode 100644 index 000000000..cc533162d --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/JsonHookPatchFingerprint.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.twitter.misc.hook.json.fingerprints + +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import org.jf.dexlib2.Opcode + +object JsonHookPatchFingerprint : MethodFingerprint( + customFingerprint = { methodDef -> methodDef.name == "" }, + opcodes = listOf(Opcode.IGET_OBJECT) +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/JsonInputStreamFingerprint.kt b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/JsonInputStreamFingerprint.kt new file mode 100644 index 000000000..7c80f5bde --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/JsonInputStreamFingerprint.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.twitter.misc.hook.json.fingerprints + +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint + +object JsonInputStreamFingerprint : MethodFingerprint( + customFingerprint = { methodDef -> + if (methodDef.parameterTypes.size == 0) false + else methodDef.parameterTypes.first() == "Ljava/io/InputStream;" + } +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/LoganSquareFingerprint.kt b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/LoganSquareFingerprint.kt new file mode 100644 index 000000000..98f0a82f6 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/fingerprints/LoganSquareFingerprint.kt @@ -0,0 +1,7 @@ +package app.revanced.patches.twitter.misc.hook.json.fingerprints + +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint + +object LoganSquareFingerprint : MethodFingerprint( + customFingerprint = { methodDef -> methodDef.definingClass.endsWith("LoganSquare;") } +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/patch/JsonHookPatch.kt b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/patch/JsonHookPatch.kt new file mode 100644 index 000000000..b7122fbc9 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/patch/JsonHookPatch.kt @@ -0,0 +1,114 @@ +package app.revanced.patches.twitter.misc.hook.json.patch + +import app.revanced.patcher.annotation.Description +import app.revanced.patcher.annotation.Name +import app.revanced.patcher.annotation.Version +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.addInstructions +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint.Companion.resolve +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprintResult +import app.revanced.patcher.patch.* +import app.revanced.patches.twitter.misc.hook.json.fingerprints.JsonHookPatchFingerprint +import app.revanced.patches.twitter.misc.hook.json.fingerprints.JsonInputStreamFingerprint +import app.revanced.patches.twitter.misc.hook.json.fingerprints.LoganSquareFingerprint +import java.io.InvalidClassException + +@Name("json-hook") +@Description("Hooks the stream which reads JSON responses.") +@Version("0.0.1") +class JsonHookPatch : BytecodePatch( + listOf(LoganSquareFingerprint) +) { + override fun execute(context: BytecodeContext): PatchResult { + // Make sure the integrations are present. + val jsonHookPatch = context.findClass { it.type == JSON_HOOK_PATCH_CLASS_DESCRIPTOR } + ?: return PatchResultError("Could not find integrations.") + + // Allow patch to inject hooks into the patches integrations. + jsonHookPatchFingerprintResult = JsonHookPatchFingerprint.also { + it.resolve(context, jsonHookPatch.immutableClass) + }.result ?: return PatchResultError("Unexpected integrations.") + + // Conveniently find the type to hook a method in, via a named field. + val jsonFactory = LoganSquareFingerprint.result + ?.classDef + ?.fields + ?.firstOrNull { it.name == "JSON_FACTORY" } + ?.type + .let { type -> + context.findClass { it.type == type }?.mutableClass + } ?: return PatchResultError("Could not find required class.") + + // Hook the methods first parameter. + JsonInputStreamFingerprint + .also { it.resolve(context, jsonFactory) } + .result + ?.mutableMethod + ?.addInstructions( + 0, + """ + invoke-static { p1 }, $JSON_HOOK_PATCH_CLASS_DESCRIPTOR->parseJsonHook(Ljava/io/InputStream;)Ljava/io/InputStream; + move-result-object p1 + """ + ) ?: return PatchResultError("Could not find method to hook.") + + return PatchResultSuccess() + } + + /** + * Create a hook class. + * The class has to extend on **JsonHook**. + * The class has to be a Kotlin object class, or at least have an INSTANCE field of itself. + * + * @param context The [BytecodeContext] of the current patch. + * @param descriptor The class descriptor of the hook. + */ + internal class Hook(context: BytecodeContext, private val descriptor: String) { + private var added = false + + /** + * Add the hook. + */ + internal fun add() { + if (added) return + + jsonHookPatchFingerprintResult.apply { + mutableMethod.apply { + addInstructions( + scanResult.patternScanResult!!.startIndex, + """ + sget-object v1, $descriptor->INSTANCE:$descriptor + invoke-virtual {v0, v1}, Lkotlin/collections/builders/ListBuilder;->add(Ljava/lang/Object;)Z + """ + ) + } + } + + added = true + } + + init { + context.findClass { it.type == descriptor }?.let { + it.mutableClass.also { classDef -> + if ( + classDef.superclass != JSON_HOOK_CLASS_DESCRIPTOR || + !classDef.fields.any { field -> field.name == "INSTANCE" } + ) throw InvalidClassException(classDef.type, "Not a hook class") + + } + } ?: throw ClassNotFoundException("Failed to find hook class") + } + } + + private companion object { + const val JSON_HOOK_CLASS_NAMESPACE = "app/revanced/twitter/patches/hook/json" + + const val JSON_HOOK_PATCH_CLASS_DESCRIPTOR = "L$JSON_HOOK_CLASS_NAMESPACE/JsonHookPatch;" + + const val BASE_PATCH_CLASS_NAME = "BaseJsonHook" + + const val JSON_HOOK_CLASS_DESCRIPTOR = "L$JSON_HOOK_CLASS_NAMESPACE/$BASE_PATCH_CLASS_NAME;" + + private lateinit var jsonHookPatchFingerprintResult: MethodFingerprintResult + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/twitter/misc/hook/patch/BaseHookPatchPatch.kt b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/patch/BaseHookPatchPatch.kt new file mode 100644 index 000000000..f2dec2a20 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/patch/BaseHookPatchPatch.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.twitter.misc.hook.patch + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.PatchResultError +import app.revanced.patcher.patch.PatchResultSuccess +import app.revanced.patcher.patch.annotations.DependsOn +import app.revanced.patches.twitter.misc.hook.json.patch.JsonHookPatch + +@DependsOn([JsonHookPatch::class]) +abstract class BaseHookPatchPatch(private val hookClassDescriptor: String) : BytecodePatch() { + override fun execute(context: BytecodeContext) = try { + PatchResultSuccess().also { JsonHookPatch.Hook(context, hookClassDescriptor).add() } + } catch (ex: Exception) { + PatchResultError(ex) + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/twitter/ad/timeline/annotations/TimelineAdsCompatibility.kt b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/patch/ads/annotations/HideAdsCompatibility.kt similarity index 55% rename from src/main/kotlin/app/revanced/patches/twitter/ad/timeline/annotations/TimelineAdsCompatibility.kt rename to src/main/kotlin/app/revanced/patches/twitter/misc/hook/patch/ads/annotations/HideAdsCompatibility.kt index 80f03c3e7..9d77042ba 100644 --- a/src/main/kotlin/app/revanced/patches/twitter/ad/timeline/annotations/TimelineAdsCompatibility.kt +++ b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/patch/ads/annotations/HideAdsCompatibility.kt @@ -1,13 +1,13 @@ -package app.revanced.patches.twitter.ad.timeline.annotations +package app.revanced.patches.twitter.misc.hook.patch.ads.annotations import app.revanced.patcher.annotation.Compatibility import app.revanced.patcher.annotation.Package @Compatibility( [Package( - "com.twitter.android", arrayOf("9.65.3-release.0") + "com.twitter.android" )] ) @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) -internal annotation class TimelineAdsCompatibility \ No newline at end of file +internal annotation class HideAdsCompatibility \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/twitter/misc/hook/patch/ads/patch/HideAdsPatch.kt b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/patch/ads/patch/HideAdsPatch.kt new file mode 100644 index 000000000..6273a26d8 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/twitter/misc/hook/patch/ads/patch/HideAdsPatch.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.twitter.misc.hook.patch.ads.patch + +import app.revanced.patcher.annotation.Description +import app.revanced.patcher.annotation.Name +import app.revanced.patcher.annotation.Version +import app.revanced.patcher.patch.annotations.DependsOn +import app.revanced.patcher.patch.annotations.Patch +import app.revanced.patches.twitter.misc.hook.json.patch.JsonHookPatch +import app.revanced.patches.twitter.misc.hook.patch.BaseHookPatchPatch +import app.revanced.patches.twitter.misc.hook.patch.ads.annotations.HideAdsCompatibility + +@Patch +@Name("hide-ads") +@DependsOn([JsonHookPatch::class]) +@Description("Hides ads.") +@HideAdsCompatibility +@Version("0.0.1") +class HideAdsPatch : BaseHookPatchPatch(HOOK_CLASS_DESCRIPTOR) { + private companion object { + const val HOOK_CLASS_DESCRIPTOR = "Lapp/revanced/twitter/patches/hook/patch/ads/AdsHook;" + } +} \ No newline at end of file