diff --git a/src/main/kotlin/app/revanced/patches/tumblr/ads/DisableDashboardAds.kt b/src/main/kotlin/app/revanced/patches/tumblr/ads/DisableDashboardAds.kt index 8c8c6de8..09972604 100644 --- a/src/main/kotlin/app/revanced/patches/tumblr/ads/DisableDashboardAds.kt +++ b/src/main/kotlin/app/revanced/patches/tumblr/ads/DisableDashboardAds.kt @@ -1,33 +1,37 @@ package app.revanced.patches.tumblr.ads -import app.revanced.extensions.exception import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.patch.BytecodePatch import app.revanced.patcher.patch.annotation.CompatiblePackage import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patches.tumblr.ads.fingerprints.AdWaterfallFingerprint -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import app.revanced.patches.tumblr.timelinefilter.TimelineFilterPatch @Patch( name = "Disable dashboard ads", description = "Disables ads in the dashboard.", - compatiblePackages = [CompatiblePackage("com.tumblr")] + compatiblePackages = [CompatiblePackage("com.tumblr")], + dependencies = [TimelineFilterPatch::class] ) @Suppress("unused") -object DisableDashboardAds : BytecodePatch( - setOf(AdWaterfallFingerprint) -) { - override fun execute(context: BytecodeContext) = AdWaterfallFingerprint.result?.let { - it.scanResult.stringsScanResult!!.matches.forEach { match -> - // We just replace all occurrences of "client_side_ad_waterfall" with anything else - // so the app fails to handle ads in the timeline elements array and just skips them. - // See AdWaterfallFingerprint for more info. - val stringRegister = it.mutableMethod.getInstruction(match.index).registerA - it.mutableMethod.replaceInstruction( - match.index, "const-string v$stringRegister, \"dummy\"" - ) +object DisableDashboardAds : BytecodePatch() { + override fun execute(context: BytecodeContext) { + // The timeline object types are filtered by their name in the TimelineObjectType enum. + // This is often different from the "object_type" returned in the api (noted in comments here) + arrayOf( + "CLIENT_SIDE_MEDIATION", // "client_side_ad_waterfall" + "GEMINI_AD", // "backfill_ad" + + // The object types below weren't actually spotted in the wild in testing, but they are valid Object types + // and their names clearly indicate that they are ads, so we just block them anyway, + // just in case they will be used in the future. + "NIMBUS_AD", // "nimbus_ad" + "CLIENT_SIDE_AD", // "client_side_ad" + "DISPLAY_IO_INTERSCROLLER_AD", // "display_io_interscroller" + "DISPLAY_IO_HEADLINE_VIDEO_AD", // "display_io_headline_video" + "FACEBOOK_BIDDAABLE", // "facebook_biddable_sdk_ad" + "GOOGLE_NATIVE" // "google_native_ad" + ).forEach { + TimelineFilterPatch.addObjectTypeFilter(it) } - } ?: throw AdWaterfallFingerprint.exception + } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/tumblr/ads/fingerprints/AdWaterfallFingerprint.kt b/src/main/kotlin/app/revanced/patches/tumblr/ads/fingerprints/AdWaterfallFingerprint.kt deleted file mode 100644 index b7737a6c..00000000 --- a/src/main/kotlin/app/revanced/patches/tumblr/ads/fingerprints/AdWaterfallFingerprint.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.patches.tumblr.ads.fingerprints - -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint - -// The Tumblr app sends a request to /v2/timeline/dashboard which replies with an array of elements -// to show in the user dashboard. These element have different type ids (post, title, carousel, etc.) -// The standard dashboard Ad has the id client_side_ad_waterfall, and this string has to be in the code -// to handle ads and provide their appearance. -// If we just replace this string in the tumblr code with anything else, it will fail to recognize the -// dashboard object type and just skip it. This is a bit weird, but it shouldn't break -// unless they change the api (unlikely) or explicitly Change the tumblr code to prevent this. -object AdWaterfallFingerprint : MethodFingerprint(strings = listOf("client_side_ad_waterfall")) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/tumblr/live/DisableTumblrLivePatch.kt b/src/main/kotlin/app/revanced/patches/tumblr/live/DisableTumblrLivePatch.kt index f588d02a..1c9ec2a8 100644 --- a/src/main/kotlin/app/revanced/patches/tumblr/live/DisableTumblrLivePatch.kt +++ b/src/main/kotlin/app/revanced/patches/tumblr/live/DisableTumblrLivePatch.kt @@ -1,37 +1,26 @@ package app.revanced.patches.tumblr.live -import app.revanced.extensions.exception import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.patch.BytecodePatch import app.revanced.patcher.patch.annotation.CompatiblePackage import app.revanced.patcher.patch.annotation.Patch import app.revanced.patches.tumblr.featureflags.OverrideFeatureFlagsPatch -import app.revanced.patches.tumblr.live.fingerprints.LiveMarqueeFingerprint -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import app.revanced.patches.tumblr.timelinefilter.TimelineFilterPatch @Patch( name = "Disable Tumblr Live", description = "Disable the Tumblr Live tab button and dashboard carousel.", - dependencies = [OverrideFeatureFlagsPatch::class], + dependencies = [OverrideFeatureFlagsPatch::class, TimelineFilterPatch::class], compatiblePackages = [CompatiblePackage("com.tumblr")] ) @Suppress("unused") -object DisableTumblrLivePatch : BytecodePatch( - setOf(LiveMarqueeFingerprint) -) { - override fun execute(context: BytecodeContext) = LiveMarqueeFingerprint.result?.let { - it.scanResult.stringsScanResult!!.matches.forEach { match -> - // Replace the string constant "live_marquee" - // with a dummy so the app doesn't recognize this type of element in the Dashboard and skips it - it.mutableMethod.apply { - val stringRegister = getInstruction(match.index).registerA - replaceInstruction(match.index, "const-string v$stringRegister, \"dummy2\"") - } - } +object DisableTumblrLivePatch : BytecodePatch() { + override fun execute(context: BytecodeContext) { + // Hide the LIVE_MARQUEE timeline element that appears in the feed + // Called "live_marquee" in api response + TimelineFilterPatch.addObjectTypeFilter("LIVE_MARQUEE") - // We hide the Tab button for Tumblr Live by forcing the feature flag to false + // Hide the Tab button for Tumblr Live by forcing the feature flag to false OverrideFeatureFlagsPatch.addOverride("liveStreaming", "false") - } ?: throw LiveMarqueeFingerprint.exception + } } \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/tumblr/live/fingerprints/LiveMarqueeFingerprint.kt b/src/main/kotlin/app/revanced/patches/tumblr/live/fingerprints/LiveMarqueeFingerprint.kt deleted file mode 100644 index 3f4da4b0..00000000 --- a/src/main/kotlin/app/revanced/patches/tumblr/live/fingerprints/LiveMarqueeFingerprint.kt +++ /dev/null @@ -1,6 +0,0 @@ -package app.revanced.patches.tumblr.live.fingerprints - -import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint - -// This works identically to the Tumblr AdWaterfallFingerprint, see comments there -object LiveMarqueeFingerprint : MethodFingerprint(strings = listOf("live_marquee")) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/TimelineFilterPatch.kt b/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/TimelineFilterPatch.kt new file mode 100644 index 00000000..a919124c --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/TimelineFilterPatch.kt @@ -0,0 +1,68 @@ +package app.revanced.patches.tumblr.timelinefilter + +import app.revanced.extensions.exception +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patches.tumblr.timelinefilter.fingerprints.PostsResponseConstructorFingerprint +import app.revanced.patches.tumblr.timelinefilter.fingerprints.TimelineConstructorFingerprint +import app.revanced.patches.tumblr.timelinefilter.fingerprints.TimelineFilterIntegrationFingerprint +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c + +@Patch(description = "Filter timeline objects.", requiresIntegrations = true) +object TimelineFilterPatch : BytecodePatch( + setOf(TimelineConstructorFingerprint, TimelineFilterIntegrationFingerprint, PostsResponseConstructorFingerprint) +) { + /** + * Add a filter to hide the given timeline object type. + * The list of all Timeline object types is found in the TimelineObjectType class, + * where they are mapped from their api name (returned by tumblr via the HTTP API) to the enum value name. + * + * @param typeName The enum name of the timeline object type to hide. + */ + @Suppress("KDocUnresolvedReference") + internal lateinit var addObjectTypeFilter: (typeName: String) -> Unit private set + + override fun execute(context: BytecodeContext) { + TimelineFilterIntegrationFingerprint.result?.let { integration -> + val filterInsertIndex = integration.scanResult.patternScanResult!!.startIndex + + integration.mutableMethod.apply { + val addInstruction = getInstruction(filterInsertIndex + 1) + if (addInstruction.registerCount != 2) throw TimelineFilterIntegrationFingerprint.exception + + val filterListRegister = addInstruction.registerC + val stringRegister = addInstruction.registerD + + // Remove "BLOCKED_OBJECT_DUMMY" + removeInstructions(filterInsertIndex, 2) + + addObjectTypeFilter = { typeName -> + // blockedObjectTypes.add({typeName}) + addInstructionsWithLabels( + filterInsertIndex, """ + const-string v$stringRegister, "$typeName" + invoke-interface { v$filterListRegister, v$stringRegister }, Ljava/util/HashSet;->add(Ljava/lang/Object;)Z + """ + ) + } + } + } ?: throw TimelineFilterIntegrationFingerprint.exception + + mapOf( + TimelineConstructorFingerprint to 1, + PostsResponseConstructorFingerprint to 2 + ).forEach { (fingerprint, timelineObjectsRegister) -> + fingerprint.result?.mutableMethod?.addInstructions( + 0, + "invoke-static {p$timelineObjectsRegister}, " + + "Lapp/revanced/tumblr/patches/TimelineFilterPatch;->" + + "filterTimeline(Ljava/util/List;)V" + ) ?: throw fingerprint.exception + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/fingerprints/PostsResponseConstructorFingerprint.kt b/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/fingerprints/PostsResponseConstructorFingerprint.kt new file mode 100644 index 00000000..cab0a3c3 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/fingerprints/PostsResponseConstructorFingerprint.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.tumblr.timelinefilter.fingerprints + +import app.revanced.patcher.extensions.or +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +// This is the constructor of the PostsResponse class. +// The same applies here as with the TimelineConstructorFingerprint. +object PostsResponseConstructorFingerprint : MethodFingerprint( + accessFlags = AccessFlags.CONSTRUCTOR or AccessFlags.PUBLIC, + customFingerprint = { methodDef, _ -> methodDef.definingClass.endsWith("/PostsResponse;") && methodDef.parameters.size == 4 }, +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/fingerprints/TimelineConstructorFingerprint.kt b/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/fingerprints/TimelineConstructorFingerprint.kt new file mode 100644 index 00000000..a8f59c41 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/fingerprints/TimelineConstructorFingerprint.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.tumblr.timelinefilter.fingerprints + +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint + +// This is the constructor of the Timeline class. +// It receives the List as an argument with a @Json annotation, so this should be the first time +// that the List is exposed in non-library code. +object TimelineConstructorFingerprint : MethodFingerprint( + customFingerprint = { methodDef, _ -> + methodDef.definingClass.endsWith("/Timeline;") && methodDef.parameters[0].type == "Ljava/util/List;" + }, strings = listOf("timelineObjectsList") +) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/fingerprints/TimelineFilterIntegrationFingerprint.kt b/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/fingerprints/TimelineFilterIntegrationFingerprint.kt new file mode 100644 index 00000000..2b08f503 --- /dev/null +++ b/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/fingerprints/TimelineFilterIntegrationFingerprint.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.tumblr.timelinefilter.fingerprints + +import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint +import com.android.tools.smali.dexlib2.Opcode + +// This fingerprints the Integration TimelineFilterPatch.filterTimeline method. +// The opcode fingerprint is searching for +// if ("BLOCKED_OBJECT_DUMMY".equals(elementType)) iterator.remove(); +object TimelineFilterIntegrationFingerprint : MethodFingerprint( + customFingerprint = { methodDef, _ -> methodDef.definingClass.endsWith("/TimelineFilterPatch;") }, + strings = listOf("BLOCKED_OBJECT_DUMMY"), + opcodes = listOf( + Opcode.CONST_STRING, // "BLOCKED_OBJECT_DUMMY" + Opcode.INVOKE_INTERFACE // List.add(^) + ) +) \ No newline at end of file