Compare commits
1 commit
main
...
github/for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28d26bff9b |
5 changed files with 236 additions and 0 deletions
|
|
@ -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 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 static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue