diff --git a/.gitignore b/.gitignore index 67531bcb..899b7cc6 100644 --- a/.gitignore +++ b/.gitignore @@ -127,4 +127,5 @@ node_modules/ # Ignore IDEA files .idea/ -/src/main/kotlin/crimera/patches/twitter/test* \ No newline at end of file +/src/main/kotlin/crimera/patches/twitter/test* +bin/ \ No newline at end of file diff --git a/src/main/kotlin/crimera/patches/twitter/misc/settings/SettingsPatch.kt b/src/main/kotlin/crimera/patches/twitter/misc/settings/SettingsPatch.kt index 4bbdc653..ef99983f 100644 --- a/src/main/kotlin/crimera/patches/twitter/misc/settings/SettingsPatch.kt +++ b/src/main/kotlin/crimera/patches/twitter/misc/settings/SettingsPatch.kt @@ -34,15 +34,18 @@ object SettingsPatch : BytecodePatch( ) { private const val INTEGRATIONS_PACKAGE = "Lapp/revanced/integrations/twitter" const val UTILS_DESCRIPTOR = "$INTEGRATIONS_PACKAGE/Utils" - private const val ACTIVITY_HOOK_CLASS = "Lapp/revanced/integrations/twitter/settings/ActivityHook;" - private const val DEEPLINK_HOOK_CLASS = "Lapp/revanced/integrations/twitter/settings/DeepLink;" + private const val ACTIVITY_SETTINGS_CLASS = "$INTEGRATIONS_PACKAGE/settings" + private const val ACTIVITY_HOOK_CLASS = "$ACTIVITY_SETTINGS_CLASS/ActivityHook;" + private const val DEEPLINK_HOOK_CLASS = "$ACTIVITY_SETTINGS_CLASS/DeepLink;" private const val ADD_PREF_DESCRIPTOR = "$UTILS_DESCRIPTOR;->addPref([Ljava/lang/String;Ljava/lang/String;)[Ljava/lang/String;" const val PREF_DESCRIPTOR = "$INTEGRATIONS_PACKAGE/Pref" const val PATCHES_DESCRIPTOR = "$INTEGRATIONS_PACKAGE/patches" const val CUSTOMISE_DESCRIPTOR = "$PATCHES_DESCRIPTOR/customise/Customise" - const val SSTS_DESCRIPTOR = "invoke-static {}, $INTEGRATIONS_PACKAGE/settings/SettingsStatus;" + const val NATIVE_DESCRIPTOR = "$PATCHES_DESCRIPTOR/nativeFeatures" + + const val SSTS_DESCRIPTOR = "invoke-static {}, $ACTIVITY_SETTINGS_CLASS/SettingsStatus;" const val FSTS_DESCRIPTOR = "invoke-static {}, $INTEGRATIONS_PACKAGE/patches/FeatureSwitchPatch;" private const val START_ACTIVITY_DESCRIPTOR = diff --git a/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeDownloader/NativeDownloaderHooksPatch.kt b/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeDownloader/NativeDownloaderHooksPatch.kt deleted file mode 100644 index 42441b2c..00000000 --- a/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeDownloader/NativeDownloaderHooksPatch.kt +++ /dev/null @@ -1,223 +0,0 @@ -package crimera.patches.twitter.misc.shareMenu.nativeDownloader - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.getInstructions -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patcher.fingerprint.MethodFingerprintResult -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c -import com.android.tools.smali.dexlib2.dexbacked.reference.DexBackedMethodReference -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.reference.FieldReference - -internal abstract class NativeDownloaderMethodFingerprint( - private val methodName: String, -) : MethodFingerprint(customFingerprint = { methodDef, classDef -> - methodDef.name == methodName && classDef.toString() == "Lapp/revanced/integrations/twitter/patches/NativeDownloader;" - }) - -internal object TweetObjectFingerprint : MethodFingerprint(strings = listOf("https://x.com/%1\$s/status/%2\$d")) - -internal object GetTweetClassFingerprint : NativeDownloaderMethodFingerprint("getTweetClass") - -internal object GetTweetIdFingerprint : NativeDownloaderMethodFingerprint("getTweetId") - -internal object GetTweetUsernameFingerprint : NativeDownloaderMethodFingerprint("getTweetUsername") - -internal object GetTweetProfileNameFingerprint : NativeDownloaderMethodFingerprint("getTweetProfileName") - -internal object GetTweetUserIdFingerprint : NativeDownloaderMethodFingerprint("getTweetUserId") - -internal object GetTweetMediaFingerprint : NativeDownloaderMethodFingerprint("getTweetMedia") - -internal object GetImageUrlFFingerprint : NativeDownloaderMethodFingerprint("getImageUrlField") - -internal object GetVideoUrlFFingerprint : NativeDownloaderMethodFingerprint("getVideoUrlField") - -internal object GetVideoCodecFFingerprint : NativeDownloaderMethodFingerprint("getVideoCodecField") - -internal object GetVideoDataClassFingerprint: NativeDownloaderMethodFingerprint("getVideoDataClass") - -internal object GetUserNameMethodCaller : MethodFingerprint( - returnType = "V", - strings = - listOf( - "Ref_ID (Tweet ID)", - "Name", - "User Name", - ), -) - -internal object ImageUrlFieldFingeprint : MethodFingerprint( - returnType = "V", - strings = - listOf( - "MediaEntity.mediaUrl ", - ), - customFingerprint = { methodDef, classDef -> - classDef.endsWith("TweetMediaView;") - }, -) - -internal object VideoUrlFieldFingeprint : MethodFingerprint( - returnType = "V", - strings = - listOf( - "video/mp4", - "video/webm", - "application/x-mpegURL", - ), - customFingerprint = { methodDef, classDef -> - methodDef.name == "" - }, -) - -internal object VideoDataClassFingerprint: MethodFingerprint( - strings = listOf( - "codecs=\"" - ) -) - -@Patch( - compatiblePackages = [CompatiblePackage("com.twitter.android")], -) -class NativeDownloaderHooksPatch : - BytecodePatch( - setOf( - GetTweetClassFingerprint, - GetTweetIdFingerprint, - GetTweetUsernameFingerprint, - GetTweetMediaFingerprint, - GetUserNameMethodCaller, - TweetObjectFingerprint, - GetTweetProfileNameFingerprint, - GetTweetUserIdFingerprint, - ImageUrlFieldFingeprint, - VideoUrlFieldFingeprint, - GetImageUrlFFingerprint, - GetVideoUrlFFingerprint, - GetVideoCodecFFingerprint, - VideoDataClassFingerprint, - GetVideoDataClassFingerprint - ), - ) { - private fun MutableMethod.changeFirstString(value: String) { - this.getInstructions().firstOrNull { it.opcode == Opcode.CONST_STRING }?.let { instruction -> - val register = (instruction as BuilderInstruction21c).registerA - this.replaceInstruction(instruction.location.index, "const-string v$register, \"$value\"") - } ?: throw PatchException("const-string not found for method: ${this.name}") - } - - private fun MethodFingerprintResult.getMethodName(index: Int): String = - (this.mutableMethod.getInstruction(index).reference as DexBackedMethodReference).name - - override fun execute(context: BytecodeContext) { - val getTweetObjectResult = TweetObjectFingerprint.result ?: throw PatchException("bruh") - - val tweetObjectClass = getTweetObjectResult.classDef - val tweetObjectClassName = - tweetObjectClass - .toString() - .removePrefix("L") - .removeSuffix(";") - .replace("/", ".") - GetTweetClassFingerprint.result?.mutableMethod?.changeFirstString(tweetObjectClassName) - ?: throw GetTweetClassFingerprint.exception -// ------------ - val getIdMethod = - tweetObjectClass.methods.firstOrNull { mutableMethod -> - mutableMethod.name == "getId" - } ?: throw PatchException("getIdMethod not found") - GetTweetIdFingerprint.result?.mutableMethod?.changeFirstString(getIdMethod.name) - ?: throw GetTweetIdFingerprint.exception -// ------------ - val getUserNameMethodCaller = - GetUserNameMethodCaller.result ?: throw PatchException("Could not find UserNameMethodCaller fingerprint") - var getUsernameMethod = "" - var getProfileNameMethod = "" - getUserNameMethodCaller.scanResult.stringsScanResult!!.matches.forEach { match -> - val str = match.string - if (str == "Name") { - getProfileNameMethod = getUserNameMethodCaller.getMethodName(match.index + 1) - } - if (str == "User Name") { - getUsernameMethod = getUserNameMethodCaller.getMethodName(match.index + 1) - } - } - GetTweetUsernameFingerprint.result?.mutableMethod?.changeFirstString(getUsernameMethod) - ?: throw GetTweetUsernameFingerprint.exception - - GetTweetProfileNameFingerprint.result?.mutableMethod?.changeFirstString(getProfileNameMethod) - ?: throw GetTweetProfileNameFingerprint.exception -// ------------ - val getTweetUserIdMethod = - getTweetObjectResult.classDef.methods - .last { - it.returnType.equals("J") - }.name - GetTweetUserIdFingerprint.result?.mutableMethod?.changeFirstString(getTweetUserIdMethod) - ?: throw GetTweetUserIdFingerprint.exception -// ------------ - val getMediaObjectMethod = - tweetObjectClass.methods.firstOrNull { methodDef -> - methodDef.implementation - ?.instructions - ?.map { it.opcode } - ?.toList() == - listOf( - Opcode.IGET_OBJECT, - Opcode.IGET_OBJECT, - Opcode.IGET_OBJECT, - Opcode.IGET_OBJECT, - Opcode.RETURN_OBJECT, - ) - } ?: throw PatchException("getMediaObject not found") - GetTweetMediaFingerprint.result?.mutableMethod?.changeFirstString(getMediaObjectMethod.name) - ?: throw GetTweetMediaFingerprint.exception -// ------------ - ImageUrlFieldFingeprint.result ?.let { - it.scanResult.stringsScanResult!!.matches.forEach { match -> - if (match.string == "MediaEntity.mediaUrl ") { - var imageUrlFieldName = - (it.mutableMethod.getInstruction(match.index + 3).reference as FieldReference).name - - GetImageUrlFFingerprint.result?.mutableMethod?.changeFirstString(imageUrlFieldName) - ?: throw GetTweetMediaFingerprint.exception - } - } - } - -// ------------ - VideoUrlFieldFingeprint.result ?.let { - var strFields = - it.classDef.fields.filter { - it.type == "Ljava/lang/String;" - } - - GetVideoUrlFFingerprint.result?.mutableMethod?.changeFirstString(strFields[0].name) - ?: throw GetVideoUrlFFingerprint.exception - GetVideoCodecFFingerprint.result?.mutableMethod?.changeFirstString(strFields[1].name) - ?: throw GetVideoCodecFFingerprint.exception - } - - // end - - VideoDataClassFingerprint.result ?.let { - val className = it.classDef.toString() - .removePrefix("L") - .removeSuffix(";") - .replace("/", ".") - - GetVideoDataClassFingerprint.result?.mutableMethod?.changeFirstString(className) - ?: throw GetVideoDataClassFingerprint.exception - - } ?: throw VideoDataClassFingerprint.exception - } -} diff --git a/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeDownloader/NativeDownloaderPatch.kt b/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeDownloader/NativeDownloaderPatch.kt index 95e83733..790c85c1 100644 --- a/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeDownloader/NativeDownloaderPatch.kt +++ b/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeDownloader/NativeDownloaderPatch.kt @@ -17,13 +17,15 @@ import com.android.tools.smali.dexlib2.iface.MethodParameter import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.formats.* import com.android.tools.smali.dexlib2.iface.reference.MethodReference -import com.android.tools.smali.dexlib2.iface.reference.Reference import crimera.patches.twitter.misc.settings.SettingsPatch import crimera.patches.twitter.misc.settings.fingerprints.SettingsStatusLoadFingerprint import crimera.patches.twitter.misc.shareMenu.fingerprints.ActionEnumsFingerprint import crimera.patches.twitter.misc.shareMenu.fingerprints.ShareMenuButtonFuncCallFingerprint import crimera.patches.twitter.misc.shareMenu.hooks.ShareMenuButtonAddHook import crimera.patches.twitter.misc.shareMenu.hooks.ShareMenuButtonInitHook +import crimera.patches.twitter.models.ExtMediaEntityPatch +import crimera.patches.twitter.models.TweetEntityPatch +import crimera.patches.twitter.models.extractDescriptors class InitMethod( private val validator: () -> Unit, @@ -138,7 +140,7 @@ val MutableList.indexOfLastFilledNewArrayRange @Patch( name = "Custom downloader", description = "Requires X 11.0.0-release.0 or higher.", - dependencies = [SettingsPatch::class, NativeDownloaderHooksPatch::class, ResourceMappingPatch::class], + dependencies = [SettingsPatch::class, TweetEntityPatch::class, ExtMediaEntityPatch::class, ResourceMappingPatch::class], compatiblePackages = [CompatiblePackage("com.twitter.android")], use = true, ) @@ -152,7 +154,7 @@ object NativeDownloaderPatch : BytecodePatch( ActionEnumsFingerprint, ), ) { - var offset: Boolean = false + // var offset: Boolean = false override fun execute(context: BytecodeContext) { // TODO: make this whole button addition in a new function or a class? @@ -226,7 +228,7 @@ object NativeDownloaderPatch : BytecodePatch( check-cast v$timelineRefReg, ${timelineRef.reference.extractDescriptors()[0]} iget-object v1, v$timelineRefReg, ${timelineRef.reference} - invoke-static {v$activityRefReg, v1}, ${SettingsPatch.PATCHES_DESCRIPTOR}/NativeDownloader;->downloader(Landroid/content/Context;Ljava/lang/Object;)V + invoke-static {v$activityRefReg, v1}, ${SettingsPatch.NATIVE_DESCRIPTOR}/NativeDownloader;->downloader(Landroid/content/Context;Ljava/lang/Object;)V """.trimIndent(), viewDebugDialogReference, ) @@ -234,8 +236,3 @@ object NativeDownloaderPatch : BytecodePatch( SettingsStatusLoadFingerprint.enableSettings("nativeDownloader") } } - -fun Reference.extractDescriptors(): List { - val regex = Regex("L[^;]+;") - return regex.findAll(this.toString()).map { it.value }.toList() -} diff --git a/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeReaderMode/NativeReaderModePatch.kt b/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeReaderMode/NativeReaderModePatch.kt new file mode 100644 index 00000000..20c7fa8e --- /dev/null +++ b/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeReaderMode/NativeReaderModePatch.kt @@ -0,0 +1,121 @@ +package crimera.patches.twitter.misc.shareMenu.nativeReaderMode + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.annotation.CompatiblePackage +import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patches.shared.misc.mapping.ResourceMappingPatch +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21c +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22c +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c +import crimera.patches.twitter.misc.settings.SettingsPatch +import crimera.patches.twitter.misc.settings.fingerprints.SettingsStatusLoadFingerprint +import crimera.patches.twitter.misc.shareMenu.fingerprints.ActionEnumsFingerprint +import crimera.patches.twitter.misc.shareMenu.fingerprints.ShareMenuButtonFuncCallFingerprint +import crimera.patches.twitter.misc.shareMenu.hooks.ShareMenuButtonAddHook +import crimera.patches.twitter.misc.shareMenu.hooks.ShareMenuButtonInitHook +import crimera.patches.twitter.models.extractDescriptors + +@Patch( + name = "Native reader mode", + description = "Requires X 11.0.0-release.0 or higher.", + dependencies = [SettingsPatch::class, ResourceMappingPatch::class], + compatiblePackages = [CompatiblePackage("com.twitter.android")], + use = true, +) +@Suppress("unused") +object NativeReaderModePatch : BytecodePatch( + setOf( + ShareMenuButtonFuncCallFingerprint, + ShareMenuButtonInitHook, + SettingsStatusLoadFingerprint, + ShareMenuButtonAddHook, + ActionEnumsFingerprint, + ), +) { + override fun execute(context: BytecodeContext) { + val actionName = "ReaderMode" + + // Add action + val downloadActionReference = ActionEnumsFingerprint.addAction(actionName, ActionEnumsFingerprint.result!!) + + // Register button + ShareMenuButtonAddHook.registerButton(actionName, "enableNativeReaderMode") + val viewDebugDialogReference = + ( + ShareMenuButtonAddHook.result + ?.method + ?.implementation + ?.instructions + ?.first { it.opcode == Opcode.SGET_OBJECT } as Instruction21c + ).reference + + // Set Button Text + ShareMenuButtonInitHook.setButtonText(actionName, "piko_title_native_reader_mode") + ShareMenuButtonInitHook.setButtonIcon(actionName, "ic_vector_book_stroke_on") + + // TODO: handle possible nulls + val buttonFunc = ShareMenuButtonFuncCallFingerprint.result + val buttonFuncMethod = + ShareMenuButtonFuncCallFingerprint.result + ?.method + ?.implementation + ?.instructions + ?.toList() + + val deleteStatusLoc = + buttonFunc + ?.scanResult + ?.stringsScanResult + ?.matches + ?.first { it.string == "Delete Status" } + ?.index + ?: throw PatchException("Delete status not found") + + @Suppress("ktlint:standard:property-naming") + val OkLoc = + buttonFunc + ?.scanResult + ?.stringsScanResult + ?.matches + ?.first { it.string == "OK" } + ?.index + ?: throw PatchException("OK not found") + + val conversationalRepliesLoc = + buttonFunc.scanResult.stringsScanResult + ?.matches + ?.first { + it.string == + "conversational_replies_android_pinned_replies_creation_enabled" + }?.index + ?: throw PatchException("conversational_replies_android_pinned_replies_creation_enabled not found") + + val timelineRef = + ( + buttonFuncMethod + ?.filterIndexed { i, ins -> + i > conversationalRepliesLoc && ins.opcode == Opcode.IGET_OBJECT + }?.first() as Instruction22c? + ) ?: throw PatchException("Failed to find timelineRef") + val timelineRefReg = (buttonFuncMethod?.get(deleteStatusLoc - 1) as Instruction35c).registerD + + val activityRefReg = (buttonFuncMethod[OkLoc - 3] as Instruction35c).registerD + + // Add Button function + ShareMenuButtonFuncCallFingerprint.addButtonInstructions( + downloadActionReference, + """ + check-cast v$timelineRefReg, ${timelineRef.reference.extractDescriptors()[0]} + iget-object v1, v$timelineRefReg, ${timelineRef.reference} + + invoke-static {v$activityRefReg, v1}, ${SettingsPatch.NATIVE_DESCRIPTOR}/readerMode/ReaderModeUtils;->launchReaderMode(Landroid/content/Context;Ljava/lang/Object;)V + """.trimIndent(), + viewDebugDialogReference, + ) + + SettingsStatusLoadFingerprint.enableSettings("nativeReaderMode") + } +} diff --git a/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeTranslator/NativeTranslatorHooksPatch.kt b/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeTranslator/NativeTranslatorHooksPatch.kt deleted file mode 100644 index d8538a7b..00000000 --- a/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeTranslator/NativeTranslatorHooksPatch.kt +++ /dev/null @@ -1,121 +0,0 @@ -package crimera.patches.twitter.misc.shareMenu.nativeTranslator - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.getInstructions -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.reference.FieldReference - -val MethodFingerprint.exception: PatchException - get() = PatchException("${this.javaClass.name} is not found") - -internal abstract class NativeTranslatorMethodFingerprint( - private val methodName: String, -) : MethodFingerprint(customFingerprint = { methodDef, classDef -> - methodDef.name == methodName && classDef.toString() == "Lapp/revanced/integrations/twitter/patches/translator/Constants;" - }) - -internal object ShortTweetObjectFingerprint : MethodFingerprint(strings = listOf("wrap(...)", "NONE")) - -internal object LongTweetObjectFingerprint : MethodFingerprint(strings = listOf("NoteTweet(id=", ", text=")) - -internal object TweetObjectFingerprint : MethodFingerprint(strings = listOf("https://x.com/%1\$s/status/%2\$d")) - -internal object TweetinfoObjectFingerprint : MethodFingerprint( - strings = - listOf( - "flags", - "lang", - "supplemental_language", - ), - customFingerprint = { methodDef, classDef -> - methodDef.parameters.size == 2 && classDef.contains("/tdbh/") - }, -) - -internal object GetShortTextMtdFingerprint : NativeTranslatorMethodFingerprint("getShortTextMethodName") - -internal object GetLongTextMtdFingerprint : NativeTranslatorMethodFingerprint("getLongTextMethodName") - -internal object GetLongTextFldFingerprint : NativeTranslatorMethodFingerprint("getLongTextFieldName") - -internal object GetTweetInfoFldFingerprint : NativeTranslatorMethodFingerprint("getTweetInfoField") - -internal object GetLangFldFingerprint : NativeTranslatorMethodFingerprint("getLangField") - -@Patch( - compatiblePackages = [CompatiblePackage("com.twitter.android")], -) -class NativeTranslatorHooksPatch : - BytecodePatch( - setOf( - GetShortTextMtdFingerprint, - GetLongTextMtdFingerprint, - GetLongTextFldFingerprint, - TweetObjectFingerprint, - TweetinfoObjectFingerprint, - ShortTweetObjectFingerprint, - LongTweetObjectFingerprint, - GetTweetInfoFldFingerprint, - GetLangFldFingerprint, - ), - ) { - private fun MutableMethod.changeFirstString(value: String) { - this.getInstructions().firstOrNull { it.opcode == Opcode.CONST_STRING }?.let { instruction -> - this.replaceInstruction(instruction.location.index, "const-string v0, \"$value\"") - } ?: throw PatchException("const-string not found for method: ${this.name}") - } - - override fun execute(context: BytecodeContext) { - val getTweetObjectFingerprint = TweetObjectFingerprint.result ?: throw PatchException("TweetObjectFingerprint not found") - val shortTweetObjectFingerprint = - ShortTweetObjectFingerprint.result ?: throw PatchException("ShortTweetObjectFingerprint not found") - val longTweetObjectFingerprint = LongTweetObjectFingerprint.result ?: throw PatchException("LongTweetObjectFingerprint not found") - - val tweetObjectClass = getTweetObjectFingerprint.classDef - val methods = tweetObjectClass.methods - - methods.forEach { - if (it.returnType.contains(shortTweetObjectFingerprint.classDef)) { - GetShortTextMtdFingerprint.result?.mutableMethod?.changeFirstString(it.name) - ?: throw GetShortTextMtdFingerprint.exception - } else if (it.returnType.contains(longTweetObjectFingerprint.classDef)) { - GetLongTextMtdFingerprint.result?.mutableMethod?.changeFirstString(it.name) - ?: throw GetLongTextMtdFingerprint.exception - } - } - val longTweetTextField = - longTweetObjectFingerprint.classDef.fields - .first { it.type == "Ljava/lang/String;" } - .name - - GetLongTextFldFingerprint.result?.mutableMethod?.changeFirstString(longTweetTextField) - ?: throw GetLongTextFldFingerprint.exception - - TweetinfoObjectFingerprint.result ?.let { - it.scanResult.stringsScanResult!!.matches.forEach { match -> - if (match.string == "lang") { - val ref = (it.mutableMethod.getInstruction(match.index + 1).reference as FieldReference) - GetLangFldFingerprint.result?.mutableMethod?.changeFirstString(ref.name) - ?: throw GetLangFldFingerprint.exception - GetTweetInfoFldFingerprint.result?.mutableMethod?.changeFirstString( - tweetObjectClass.fields - .first { - it.type == - ref.definingClass - }.name, - ) - ?: throw GetTweetInfoFldFingerprint.exception - } - } - } - } -} diff --git a/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeTranslator/NativeTranslatorPatch.kt b/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeTranslator/NativeTranslatorPatch.kt index cc8d2daf..a583a844 100644 --- a/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeTranslator/NativeTranslatorPatch.kt +++ b/src/main/kotlin/crimera/patches/twitter/misc/shareMenu/nativeTranslator/NativeTranslatorPatch.kt @@ -16,12 +16,14 @@ import crimera.patches.twitter.misc.shareMenu.fingerprints.ActionEnumsFingerprin import crimera.patches.twitter.misc.shareMenu.fingerprints.ShareMenuButtonFuncCallFingerprint import crimera.patches.twitter.misc.shareMenu.hooks.ShareMenuButtonAddHook import crimera.patches.twitter.misc.shareMenu.hooks.ShareMenuButtonInitHook -import crimera.patches.twitter.misc.shareMenu.nativeDownloader.extractDescriptors +import crimera.patches.twitter.models.TweetEntityPatch +import crimera.patches.twitter.models.TweetInfoEntityPatch +import crimera.patches.twitter.models.extractDescriptors @Patch( name = "Custom translator", description = "Requires X 11.0.0-release.0 or higher.", - dependencies = [SettingsPatch::class, NativeTranslatorHooksPatch::class, ResourceMappingPatch::class], + dependencies = [SettingsPatch::class, TweetEntityPatch::class, TweetInfoEntityPatch::class, ResourceMappingPatch::class], compatiblePackages = [CompatiblePackage("com.twitter.android")], use = true, ) @@ -107,7 +109,7 @@ object NativeTranslatorPatch : BytecodePatch( check-cast v$timelineRefReg, ${timelineRef.reference.extractDescriptors()[0]} iget-object v1, v$timelineRefReg, ${timelineRef.reference} - invoke-static {v$activityRefReg, v1}, ${SettingsPatch.PATCHES_DESCRIPTOR}/translator/NativeTranslator;->translate(Landroid/content/Context;Ljava/lang/Object;)V + invoke-static {v$activityRefReg, v1}, ${SettingsPatch.NATIVE_DESCRIPTOR}/translator/NativeTranslator;->translate(Landroid/content/Context;Ljava/lang/Object;)V """.trimIndent(), viewDebugDialogReference, ) diff --git a/src/main/kotlin/crimera/patches/twitter/models/ExtMediaEntityPatch.kt b/src/main/kotlin/crimera/patches/twitter/models/ExtMediaEntityPatch.kt new file mode 100644 index 00000000..399be44b --- /dev/null +++ b/src/main/kotlin/crimera/patches/twitter/models/ExtMediaEntityPatch.kt @@ -0,0 +1,57 @@ +package crimera.patches.twitter.models + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.extensions.InstructionExtensions.getInstructions +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.annotation.CompatiblePackage +import app.revanced.patcher.patch.annotation.Patch +import com.android.tools.smali.dexlib2.Opcode + +@Patch( + compatiblePackages = [CompatiblePackage("com.twitter.android")], +) +class ExtMediaEntityPatch : + BytecodePatch( + setOf( + ExtMediaHighResVideoMethodFinder, + ExtMediaHighResVideoFingerprint, + ExtMediaGetImageMethodFinder, + ExtMediaGetImageFingerprint, + ), + ) { + override fun execute(context: BytecodeContext) { + val highResVideoMethodFinderResult = + ExtMediaHighResVideoMethodFinder.result ?: throw PatchException("Could not find ExtMediaHighResVideoMethodFinder fingerprint") + + highResVideoMethodFinderResult.scanResult.stringsScanResult!!.matches.forEach { match -> + val str = match.string + if (str == "null cannot be cast to non-null type com.twitter.model.dm.attachment.DMMediaAttachment") { + val inst = + highResVideoMethodFinderResult.mutableMethod.getInstructions().first { + it.opcode == Opcode.INVOKE_VIRTUAL && + it.location.index > match.index + } + val methodName = highResVideoMethodFinderResult.getMethodName(inst.location.index) + ExtMediaHighResVideoFingerprint.changeFirstString(methodName) + return@forEach + } + } + +// ------------ + val imageMethodResult = + ExtMediaGetImageMethodFinder.result ?: throw PatchException("Could not find ExtMediaGetImageMethodFinder fingerprint") + + val imageFieldName = + imageMethodResult.getFieldName( + imageMethodResult.mutableMethod + .getInstructions() + .last { + it.opcode == + Opcode.IGET_OBJECT + }.location.index, + ) + ExtMediaGetImageFingerprint.changeFirstString(imageFieldName) +// ------------ + } +} diff --git a/src/main/kotlin/crimera/patches/twitter/models/Fingerprints.kt b/src/main/kotlin/crimera/patches/twitter/models/Fingerprints.kt new file mode 100644 index 00000000..3d50b53c --- /dev/null +++ b/src/main/kotlin/crimera/patches/twitter/models/Fingerprints.kt @@ -0,0 +1,118 @@ +package crimera.patches.twitter.models + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstructions +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.fingerprint.MethodFingerprint +import app.revanced.patcher.fingerprint.MethodFingerprintResult +import app.revanced.patcher.patch.PatchException +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c +import com.android.tools.smali.dexlib2.dexbacked.reference.DexBackedMethodReference +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.Reference + +fun Reference.extractDescriptors(): List { + val regex = Regex("L[^;]+;") + return regex.findAll(this.toString()).map { it.value }.toList() +} + +abstract class EntityMethodFingerprint( + private val className: String, + private val methodName: String, +) : MethodFingerprint(customFingerprint = { methodDef, classDef -> + methodDef.name == methodName && classDef.toString() == "Lapp/revanced/integrations/twitter/model/$className;" + }) + +fun EntityMethodFingerprint.changeStringAt( + index: Int, + value: String, +) { + val method = this.result?.mutableMethod + method?.getInstructions()?.filter { it.opcode == Opcode.CONST_STRING }?.get(index)?.let { instruction -> + val register = (instruction as BuilderInstruction21c).registerA + method.replaceInstruction(instruction.location.index, "const-string v$register, \"$value\"") + } ?: throw PatchException("const-string not found for method: ${method?.name} at $index") +} + +fun EntityMethodFingerprint.changeFirstString(value: String) { + this.changeStringAt(0, value) +} + +fun MethodFingerprintResult.getReference(index: Int): Reference = (this.mutableMethod.getInstruction(index).reference) + +fun MethodFingerprintResult.getMethodName(index: Int): String = (this.getReference(index) as DexBackedMethodReference).name + +fun MethodFingerprintResult.getFieldName(index: Int): String = (this.getReference(index) as FieldReference).name + +// --------------- Tweet +internal object TweetObjectFingerprint : MethodFingerprint(strings = listOf("https://x.com/%1\$s/status/%2\$d")) + +internal object TweetUsernameFingerprint : EntityMethodFingerprint("Tweet", "getTweetUsername") + +internal object TweetProfileNameFingerprint : EntityMethodFingerprint("Tweet", "getTweetProfileName") + +internal object TweetUserIdFingerprint : EntityMethodFingerprint("Tweet", "getTweetUserId") + +internal object TweetMediaFingerprint : EntityMethodFingerprint("Tweet", "getMedias") + +internal object TweetInfoFingerprint : EntityMethodFingerprint("Tweet", "getTweetInfo") + +internal object TweetLongTextFingerprint : EntityMethodFingerprint("Tweet", "getLongText") + +internal object TweetShortTextFingerprint : EntityMethodFingerprint("Tweet", "getShortText") + +internal object GetUserNameMethodCaller : MethodFingerprint( + returnType = "V", + strings = + listOf( + "Ref_ID (Tweet ID)", + "Name", + "User Name", + ), +) + +internal object TweetMediaEntityClassFingerprint : MethodFingerprint(strings = listOf("EntityList{mEntities=")) + +// --------------- Extended Media Entity + +internal object ExtMediaHighResVideoMethodFinder : MethodFingerprint( + strings = + listOf( + "long_press_menu", + "null cannot be cast to non-null type com.twitter.model.dm.attachment.DMMediaAttachment", + ), +) + +internal object ExtMediaHighResVideoFingerprint : EntityMethodFingerprint("ExtMediaEntities", "getHighResVideo") + +internal object ExtMediaGetImageMethodFinder : MethodFingerprint( + strings = + listOf( + "type", + "id", + ), + customFingerprint = { _, classDef -> + classDef.toString().contains("Lcom/twitter/model/json/unifiedcard/JsonAppStoreData;") + }, +) + +internal object ExtMediaGetImageFingerprint : EntityMethodFingerprint("ExtMediaEntities", "getImageUrl") + +// --------------- TweetInfo +internal object TweetInfoObjectFingerprint : MethodFingerprint( + strings = + listOf( + "flags", + "lang", + "supplemental_language", + ), + customFingerprint = { methodDef, classDef -> + methodDef.parameters.size == 2 && classDef.contains("/tdbh/") + }, +) + +internal object TweetLangFingerprint : EntityMethodFingerprint("TweetInfo", "getLang") + +internal object LongTweetObjectFingerprint : MethodFingerprint(strings = listOf("NoteTweet(id=", ", text=")) diff --git a/src/main/kotlin/crimera/patches/twitter/models/TweetEntityPatch.kt b/src/main/kotlin/crimera/patches/twitter/models/TweetEntityPatch.kt new file mode 100644 index 00000000..784e5ede --- /dev/null +++ b/src/main/kotlin/crimera/patches/twitter/models/TweetEntityPatch.kt @@ -0,0 +1,107 @@ +package crimera.patches.twitter.models + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.annotation.CompatiblePackage +import app.revanced.patcher.patch.annotation.Patch +import com.android.tools.smali.dexlib2.Opcode + +@Patch( + compatiblePackages = [CompatiblePackage("com.twitter.android")], +) +class TweetEntityPatch : + BytecodePatch( + setOf( + TweetUsernameFingerprint, + TweetMediaFingerprint, + GetUserNameMethodCaller, + TweetObjectFingerprint, + TweetProfileNameFingerprint, + TweetUserIdFingerprint, + LongTweetObjectFingerprint, + TweetShortTextFingerprint, + TweetLongTextFingerprint, + TweetMediaEntityClassFingerprint, + ), + ) { + override fun execute(context: BytecodeContext) { + val getUserNameMethodCaller = + GetUserNameMethodCaller.result ?: throw PatchException("Could not find UserNameMethodCaller fingerprint") + + getUserNameMethodCaller.scanResult.stringsScanResult!!.matches.forEach { match -> + val str = match.string + if (str == "Name") { + val methodName = getUserNameMethodCaller.getMethodName(match.index + 1) + TweetUsernameFingerprint.changeFirstString(methodName) + } + if (str == "User Name") { + val methodName = getUserNameMethodCaller.getMethodName(match.index + 1) + TweetProfileNameFingerprint.changeFirstString(methodName) + } + } +// ------------ + val getTweetObjectResult = TweetObjectFingerprint.result ?: throw PatchException("getTweetObjectResult fingerprint not found") + + val tweetObjectClass = getTweetObjectResult.classDef + + val tweetObjectMethods = tweetObjectClass.methods + + val getTweetUserIdMethod = + getTweetObjectResult.classDef.methods + .last { + it.returnType == "J" + }.name + TweetUserIdFingerprint.changeFirstString(getTweetUserIdMethod) +// ------------ + val getMediaObjectMethod = + tweetObjectMethods.firstOrNull { methodDef -> + methodDef.implementation + ?.instructions + ?.map { it.opcode } + ?.toList() == + listOf( + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.RETURN_OBJECT, + ) + } ?: throw PatchException("getMediaObject not found") + TweetMediaFingerprint.changeFirstString(getMediaObjectMethod.name) + + val extMediaClassResult = + TweetMediaEntityClassFingerprint.result ?: throw PatchException("TweetMediaEntityClassFingerprint not found") + val extMediaListField = + extMediaClassResult.classDef.fields + .first { it.type.contains("List") } + .name + TweetMediaFingerprint.changeStringAt(1, extMediaListField) +// ------------ + val longTweetObjectResult = + LongTweetObjectFingerprint.result ?: throw PatchException("Could not find LongTweetObjectFingerprint fingerprint") + val getNoteTweetMethod = + tweetObjectMethods + .firstOrNull { it.returnType.contains("notetweet") } + ?.name + ?: throw PatchException("getNoteTweetMethod not found") + TweetLongTextFingerprint.changeFirstString(getNoteTweetMethod) + + val longTextField = + longTweetObjectResult.classDef.fields + .first { it.type == "Ljava/lang/String;" } + .name + TweetLongTextFingerprint.changeStringAt(1, longTextField) + +// --------------------- + val tweetEntityMethod = + tweetObjectMethods + .lastOrNull { it.returnType.contains("/entity/") } + ?.name + ?: throw PatchException("getTweetEntityMethod not found") + TweetShortTextFingerprint.changeFirstString(tweetEntityMethod) + +// End +// --------------------- + } +} diff --git a/src/main/kotlin/crimera/patches/twitter/models/TweetInfoEntityPatch.kt b/src/main/kotlin/crimera/patches/twitter/models/TweetInfoEntityPatch.kt new file mode 100644 index 00000000..83c2accf --- /dev/null +++ b/src/main/kotlin/crimera/patches/twitter/models/TweetInfoEntityPatch.kt @@ -0,0 +1,49 @@ +package crimera.patches.twitter.models + +import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.annotation.CompatiblePackage +import app.revanced.patcher.patch.annotation.Patch +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +@Patch( + compatiblePackages = [CompatiblePackage("com.twitter.android")], +) +class TweetInfoEntityPatch : + BytecodePatch( + setOf( + TweetInfoObjectFingerprint, + TweetLangFingerprint, + TweetInfoFingerprint, + TweetObjectFingerprint, + ), + ) { + override fun execute(context: BytecodeContext) { + val tweetInfoObjectResult = + TweetInfoObjectFingerprint.result ?: throw PatchException("Could not find TweetInfoObjectFingerprint fingerprint") + + +// ------------ + val tweetObjectResult = + TweetObjectFingerprint.result ?: throw PatchException("Could not find TweetObjectFingerprint fingerprint") + + tweetInfoObjectResult.scanResult.stringsScanResult!!.matches.forEach { match -> + val str = match.string + if (str == "lang") { + val ref = tweetInfoObjectResult.getReference(match.index + 1) as FieldReference + TweetLangFingerprint.changeFirstString(ref.name) + + var infoField = tweetObjectResult.classDef.fields + .first { + it.type == + ref.definingClass + }.name + TweetInfoFingerprint.changeFirstString(infoField) + return + } + } + +// ------------ + } +} diff --git a/src/main/kotlin/crimera/patches/twitter/timeline/forceHD/ForceHDPatch.kt b/src/main/kotlin/crimera/patches/twitter/timeline/forceHD/ForceHDPatch.kt index 11bc72ba..0abbacf0 100644 --- a/src/main/kotlin/crimera/patches/twitter/timeline/forceHD/ForceHDPatch.kt +++ b/src/main/kotlin/crimera/patches/twitter/timeline/forceHD/ForceHDPatch.kt @@ -16,7 +16,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22c import crimera.patches.twitter.misc.settings.SettingsPatch import crimera.patches.twitter.misc.settings.fingerprints.SettingsStatusLoadFingerprint import crimera.patches.twitter.misc.shareMenu.nativeDownloader.exception -import crimera.patches.twitter.misc.shareMenu.nativeDownloader.extractDescriptors +import crimera.patches.twitter.models.extractDescriptors object PlayerSupportFingerprint : MethodFingerprint( customFingerprint = { methodDef, classDef -> diff --git a/src/main/kotlin/crimera/patches/twitter/timeline/hideCommunityBadge/HideCommunityBadge.kt b/src/main/kotlin/crimera/patches/twitter/timeline/hideCommunityBadge/HideCommunityBadge.kt index 0ff03162..b28f17c8 100644 --- a/src/main/kotlin/crimera/patches/twitter/timeline/hideCommunityBadge/HideCommunityBadge.kt +++ b/src/main/kotlin/crimera/patches/twitter/timeline/hideCommunityBadge/HideCommunityBadge.kt @@ -13,7 +13,7 @@ import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22c import crimera.patches.twitter.misc.settings.SettingsPatch import crimera.patches.twitter.misc.settings.fingerprints.SettingsStatusLoadFingerprint -import crimera.patches.twitter.misc.shareMenu.nativeDownloader.extractDescriptors +import crimera.patches.twitter.models.extractDescriptors object CommModelFingerprint : MethodFingerprint( strings = diff --git a/src/main/kotlin/crimera/patches/twitter/timeline/showSourceLabel/ShowSourceLabel.kt b/src/main/kotlin/crimera/patches/twitter/timeline/showSourceLabel/ShowSourceLabel.kt index 8aa45666..7a363785 100644 --- a/src/main/kotlin/crimera/patches/twitter/timeline/showSourceLabel/ShowSourceLabel.kt +++ b/src/main/kotlin/crimera/patches/twitter/timeline/showSourceLabel/ShowSourceLabel.kt @@ -15,7 +15,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22c import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c import crimera.patches.twitter.misc.settings.SettingsPatch import crimera.patches.twitter.misc.settings.fingerprints.SettingsStatusLoadFingerprint -import crimera.patches.twitter.misc.shareMenu.nativeDownloader.extractDescriptors +import crimera.patches.twitter.models.extractDescriptors object SourceLabelFingerprint : MethodFingerprint( strings = listOf("show_tweet_source_disabled"), diff --git a/src/main/resources/twitter/settings/values/strings.xml b/src/main/resources/twitter/settings/values/strings.xml index fd636085..c873762d 100644 --- a/src/main/resources/twitter/settings/values/strings.xml +++ b/src/main/resources/twitter/settings/values/strings.xml @@ -44,6 +44,20 @@ Google translator Google translator V2 + Reader mode + Analyse this thread + More about the author + Source + Published + Total Posts + Text only mode + Hide quoted posts + No Grok analyses + Copy post link + Delete reader mode\'s cache + Cache deleted successfully + Cache deletion failed + Ads Promoted posts