Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Mike Jones
28d26bff9b Added patches for Continuum to change the client id, redirect uri, and
user agent to pretend to be RedReader
2026-01-06 01:41:34 -07:00
5 changed files with 236 additions and 0 deletions

View file

@ -717,6 +717,27 @@ public final class app/revanced/patches/reddit/customclients/boostforreddit/misc
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
} }
public final class app/revanced/patches/reddit/customclients/continuum/api/Constants {
public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/continuum/api/Constants;
public static final field NEW_CLIENT_ID Ljava/lang/String;
public static final field NEW_REDIRECT_URI Ljava/lang/String;
public static final field NEW_USER_AGENT Ljava/lang/String;
public static final field OLD_CLIENT_ID Ljava/lang/String;
public static final field OLD_REDIRECT_URI Ljava/lang/String;
}
public final class app/revanced/patches/reddit/customclients/continuum/api/RedditApiBytecodePatchKt {
public static final fun getRedditApiBytecodePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/reddit/customclients/continuum/api/RedditApiResourcePatchKt {
public static final fun getRedditApiResourcePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
}
public final class app/revanced/patches/reddit/customclients/continuum/misc/RemoveClientIdCheckPatchKt {
public static final fun getRemoveClientIdCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatchKt { public final class app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatchKt {
public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
} }

View file

@ -0,0 +1,9 @@
package app.revanced.patches.reddit.customclients.continuum.api
object Constants {
const val NEW_USER_AGENT = "org.quantumbadger.redreader/1.25.1"
const val OLD_REDIRECT_URI = "continuum://localhost"
const val NEW_REDIRECT_URI = "redreader://rr_oauth_redir"
const val OLD_CLIENT_ID = "Ro2J-y8bN412oS6BeaIj0A"
const val NEW_CLIENT_ID = "QnM1dlkC_2UfSlACOTXGRw"
}

View file

@ -0,0 +1,54 @@
package app.revanced.patches.reddit.customclients.continuum.api
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.transformation.transformInstructionsPatch
import app.revanced.patches.reddit.customclients.continuum.misc.removeClientIdCheckPatch
import app.revanced.patches.shared.misc.string.replaceStringPatch
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.StringReference
@Suppress("unused")
val redditApiBytecodePatch = bytecodePatch(
name = "Reddit API override",
description = "Overrides Reddit User-Agent, Redirect URI, and Client ID",
) {
compatibleWith("org.cygnusx1.continuum", "org.cygnusx1.continuum.debug")
dependsOn(
redditApiResourcePatch,
// Use regex-based replacement for user agent to work across all versions
transformInstructionsPatch(
filterMap = filterMap@{ _, _, instruction, instructionIndex ->
if (instruction.opcode != Opcode.CONST_STRING && instruction.opcode != Opcode.CONST_STRING_JUMBO) {
return@filterMap null
}
val stringReference = (instruction as? com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction)
?.reference as? StringReference ?: return@filterMap null
val pattern = "android:org\\.cygnusx1\\.continuum:\\d+\\.\\d+\\.\\d+\\.\\d+ \\(by /u/edgan\\)".toRegex()
if (!pattern.matches(stringReference.string)) return@filterMap null
Triple(instructionIndex, instruction as OneRegisterInstruction, stringReference.string)
},
transform = { mutableMethod, entry ->
val (instructionIndex, instruction, _) = entry
mutableMethod.replaceInstruction(
instructionIndex,
"${instruction.opcode.name} v${instruction.registerA}, \"${Constants.NEW_USER_AGENT}\"",
)
},
),
replaceStringPatch(Constants.OLD_REDIRECT_URI, Constants.NEW_REDIRECT_URI),
removeClientIdCheckPatch
)
execute {
// The actual replacements are handled by the dependencies:
// - redditApiResourcePatch: modifies default_client_id in res/values/strings.xml
// - transformInstructionsPatch: replaces user agent with regex pattern matching
// - replaceStringPatch: replaces hardcoded redirect URI strings
}
}

View file

@ -0,0 +1,20 @@
package app.revanced.patches.reddit.customclients.continuum.api
import app.revanced.patcher.patch.resourcePatch
import app.revanced.util.findElementByAttributeValueOrThrow
val redditApiResourcePatch = resourcePatch(
name = "Reddit API override (resource)",
description = "Overrides Reddit client ID in strings.xml",
) {
compatibleWith("org.cygnusx1.continuum", "org.cygnusx1.continuum.debug")
execute {
document("res/values/strings.xml").use { document ->
document.documentElement.childNodes.findElementByAttributeValueOrThrow(
"name",
"default_client_id"
).textContent = Constants.NEW_CLIENT_ID
}
}
}

View file

@ -0,0 +1,132 @@
package app.revanced.patches.reddit.customclients.continuum.misc
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions
import app.revanced.patcher.fingerprint
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
internal val clientIdCheckFingerprint = fingerprint {
strings("client_id_pref_key")
}
@Suppress("unused")
val removeClientIdCheckPatch = bytecodePatch(
name = "Remove client ID check",
description = "Removes the dialog that prevents login with default client ID",
) {
compatibleWith("org.cygnusx1.continuum", "org.cygnusx1.continuum.debug")
execute {
// Find all classes and methods containing "client_id_pref_key"
var targetMethod: com.android.tools.smali.dexlib2.iface.Method? = null
var targetClassDef: com.android.tools.smali.dexlib2.iface.ClassDef? = null
var methodsWithClientIdPrefKey = 0
var methodsWithDialog = 0
classes.forEach { classDef ->
classDef.methods.forEach { method ->
// Check if method contains "client_id_pref_key" string
val hasClientIdPrefKey = method.implementation?.instructions?.any { instruction ->
instruction.getReference<StringReference>()?.string == "client_id_pref_key"
} == true
if (hasClientIdPrefKey) {
methodsWithClientIdPrefKey++
// Check if this method also has MaterialAlertDialogBuilder
val hasDialog = method.implementation?.instructions?.any {
it.opcode == Opcode.NEW_INSTANCE &&
it.getReference<TypeReference>()?.toString()?.contains("MaterialAlertDialogBuilder") == true
} == true
if (hasDialog) {
methodsWithDialog++
targetMethod = method
targetClassDef = classDef
return@forEach
}
}
}
if (targetMethod != null) return@forEach
}
if (targetMethod == null || targetClassDef == null) {
throw Exception("Could not find MainActivity method with client ID check")
}
// Now patch the method
val mutableMethod = proxy(targetClassDef).mutableClass
.methods.first { it.name == targetMethod!!.name && it.parameterTypes == targetMethod!!.parameterTypes }
mutableMethod.apply {
// First find where "client_id_pref_key" is loaded
val clientIdPrefKeyIndex = indexOfFirstInstructionOrThrow {
opcode == Opcode.CONST_STRING &&
getReference<StringReference>()?.string == "client_id_pref_key"
}
// Find the String.equals call that comes AFTER the client_id_pref_key reference
val equalsIndex = indexOfFirstInstructionOrThrow(clientIdPrefKeyIndex) {
opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.toString()?.contains("Ljava/lang/String;->equals") == true
}
// The next instruction is move-result
val moveResultIndex = equalsIndex + 1
// After that is if-eqz that branches on the equals result
val ifEqzIndex = moveResultIndex + 1
// Verify that this if-eqz is followed by MaterialAlertDialogBuilder
// to ensure we're removing the right code
val dialogCheck = indexOfFirstInstructionOrThrow(ifEqzIndex) {
opcode == Opcode.NEW_INSTANCE &&
getReference<TypeReference>()?.toString()?.contains("MaterialAlertDialogBuilder") == true
}
// Find the iget-object that loads this$0 - work backwards from equals
// Try to find it by looking for iget-object that references MainActivity
var igetObjectIndex = -1
try {
igetObjectIndex = indexOfFirstInstructionReversedOrThrow(equalsIndex - 1) {
opcode == Opcode.IGET_OBJECT &&
(toString().contains("this\$0") || toString().contains("MainActivity"))
}
} catch (e: Exception) {
// If we can't find MainActivity reference, just find the first iget-object
igetObjectIndex = indexOfFirstInstructionReversedOrThrow(equalsIndex - 1) {
opcode == Opcode.IGET_OBJECT
}
}
// Find where the normal flow resumes (new Intent creation for LoginActivity)
// We need to find the Intent that comes AFTER the dialog code
// Look for the MaterialAlertDialogBuilder first, then find Intent after it
val dialogBuilderIndex = indexOfFirstInstructionOrThrow(ifEqzIndex) {
opcode == Opcode.NEW_INSTANCE &&
getReference<TypeReference>()?.toString()?.contains("MaterialAlertDialogBuilder") == true
}
val intentIndex = indexOfFirstInstructionOrThrow(dialogBuilderIndex) {
opcode == Opcode.NEW_INSTANCE &&
getReference<TypeReference>()?.toString()?.contains("Landroid/content/Intent;") == true
}
// Double-check: look at a few instructions after to see if it's LoginActivity
val nextInstruction = getInstruction(intentIndex + 1)
// So remove from igetObjectIndex to intentIndex (exclusive)
val count = intentIndex - igetObjectIndex
removeInstructions(igetObjectIndex, count)
}
}
}