Skip to content

Commit

Permalink
feat(Twitter): Added Custom translator patch
Browse files Browse the repository at this point in the history
  • Loading branch information
swakwork committed Nov 2, 2024
1 parent 2daa292 commit 737fb9e
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package crimera.patches.twitter.misc.shareMenu.hooks

import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.getInstructions
import app.revanced.patcher.fingerprint.MethodFingerprint
import app.revanced.patcher.patch.PatchException
import app.revanced.patches.shared.misc.mapping.ResourceMappingPatch
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.Reference
Expand All @@ -24,14 +24,15 @@ object ShareMenuButtonInitHook : MethodFingerprint(
fun setButtonText(
matchString: String,
stringId: String,
offset: Int = 0,
) {
val result = result ?: throw PatchException("ShareMenuButtonInitHook not found")
val method = result.mutableMethod

result.scanResult.stringsScanResult!!.matches.forEach { match ->
val matchStr = match.string
if (matchStr == matchString) {
val loc = match.index
val loc = match.index + offset
val r = method.getInstruction<OneRegisterInstruction>(loc).registerA
method.addInstructions(
loc + 1,
Expand All @@ -49,6 +50,7 @@ object ShareMenuButtonInitHook : MethodFingerprint(
fun setButtonIcon(
buttonReference: Reference,
iconStr: String,
offset: Int = 0,
) {
val result = result ?: throw PatchException("ShareMenuButtonInitHook not found")
val allMethods = result.mutableClass.methods
Expand All @@ -57,12 +59,21 @@ object ShareMenuButtonInitHook : MethodFingerprint(
instructions.filter { it.opcode == Opcode.SGET_OBJECT }.forEach { instruction ->
val ref = (instruction as ReferenceInstruction).reference.toString()
if (ref == buttonReference.toString()) {
val index = instruction.location.index
val r = method.getInstruction<OneRegisterInstruction>(index + 1).registerA
var index = instruction.location.index + offset
index =
method
.getInstructions()
.first { it.opcode == Opcode.INVOKE_VIRTUAL && it.location.index > index }
.location.index
val r = method.getInstruction<BuilderInstruction35c>(index).registerE
val iconId = ResourceMappingPatch["drawable", iconStr]
method.addInstruction(
index + 2,
"const v$r, $iconId",
method.addInstructions(
index,
"""
const v$r, $iconId
invoke-static {v$r}, Ljava/lang/Integer;->valueOf(I)Ljava/lang/Integer;
move-result-object v$r
""".trimIndent(),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ object NativeDownloaderPatch : BytecodePatch(
ShareMenuButtonHook,
),
) {
var offset: Boolean = false

override fun execute(context: BytecodeContext) {
val result =
ShareMenuButtonFuncCallFingerprint.result
Expand Down Expand Up @@ -100,5 +102,6 @@ object NativeDownloaderPatch : BytecodePatch(
ShareMenuButtonInitHook.setButtonIcon(buttonReference, "ic_vector_incoming")

SettingsStatusLoadFingerprint.enableSettings("nativeDownloader")
offset = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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, _ ->
methodDef.parameters.size == 2
},
)

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<ReferenceInstruction>(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
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package crimera.patches.twitter.misc.shareMenu.nativeTranslator

import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
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 app.revanced.patches.shared.misc.mapping.ResourceMappingPatch
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22c
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import crimera.patches.twitter.misc.settings.SettingsPatch
import crimera.patches.twitter.misc.settings.fingerprints.SettingsStatusLoadFingerprint
import crimera.patches.twitter.misc.shareMenu.fingerprints.ShareMenuButtonFuncCallFingerprint
import crimera.patches.twitter.misc.shareMenu.hooks.ShareMenuButtonAddHook
import crimera.patches.twitter.misc.shareMenu.hooks.ShareMenuButtonHook
import crimera.patches.twitter.misc.shareMenu.hooks.ShareMenuButtonInitHook
import crimera.patches.twitter.misc.shareMenu.nativeDownloader.NativeDownloaderPatch

@Patch(
name = "Custom translator",
description = "",
dependencies = [SettingsPatch::class, NativeTranslatorHooksPatch::class, ResourceMappingPatch::class],
compatiblePackages = [CompatiblePackage("com.twitter.android")],
use = true,
)
@Suppress("unused")
object NativeTranslatorPatch : BytecodePatch(
setOf(
ShareMenuButtonFuncCallFingerprint,
ShareMenuButtonInitHook,
SettingsStatusLoadFingerprint,
ShareMenuButtonAddHook,
ShareMenuButtonHook,
),
) {
override fun execute(context: BytecodeContext) {
val result =
ShareMenuButtonFuncCallFingerprint.result
?: throw PatchException("ShareMenuButtonFuncCallFingerprint not found")

val DD = "${SettingsPatch.PATCHES_DESCRIPTOR}/translator/NativeTranslator;"

val method = result.mutableMethod
val instructions = method.getInstructions()

// one click func
var targetIndex: Int = 0
var refReg: Int = 0
result.scanResult.stringsScanResult!!.matches.forEach { stringMatch ->
val str = stringMatch.string
if (str.contains("click") && refReg == 0) {
val movObj = method.getInstruction<TwoRegisterInstruction>(stringMatch.index - 1)
refReg = movObj.registerA
} else if (str.contains("spaces?id=")) {
targetIndex =
instructions.last { it.location.index < stringMatch.index && it.opcode == Opcode.CHECK_CAST }.location.index + 1
return@forEach
}
}
if (targetIndex == 0 || refReg == 0) {
throw PatchException("hook not found")
}

// inject func
val postObj = method.getInstruction<BuilderInstruction22c>(targetIndex)

val postObjReg = postObj.registerA
val ctxReg = postObj.registerB

method.addInstructions(
targetIndex + 1,
"""
invoke-virtual/range{v$refReg .. v$refReg}, Ljava/lang/ref/Reference;->get()Ljava/lang/Object;
move-result-object v$ctxReg
check-cast v$ctxReg, Landroid/app/Activity;
invoke-static {v$ctxReg, v$postObjReg}, $DD->translate(Landroid/content/Context;Ljava/lang/Object;)V
return-void
""".trimIndent(),
)

// show icon always
val buttonReference =
ShareMenuButtonHook.buttonReference("SendToSpacesSandbox")
?: throw PatchException("ShareMenuButtonHook not found")

ShareMenuButtonAddHook.addButton(buttonReference, "enableNativeTranslator")

// text func
var offset = 0
if (NativeDownloaderPatch.offset) {
offset = 3
}
ShareMenuButtonInitHook.setButtonText("View in Spaces Sandbox", "translate_tweet_show", offset)

// icon
ShareMenuButtonInitHook.setButtonIcon(buttonReference, "ic_vector_birdwatch", 0)

SettingsStatusLoadFingerprint.enableSettings("nativeTranslator")
}
}
5 changes: 5 additions & 0 deletions src/main/resources/twitter/settings/values/arrays.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,9 @@
<item>@string/piko_pref_customisation_reply_sorting_remember</item>
</string-array>

<string-array name="piko_array_translators">
<item>@string/piko_native_translator_google</item>
<item>@string/piko_native_translator_google_v2</item>
</string-array>

</resources>
7 changes: 7 additions & 0 deletions src/main/resources/twitter/settings/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,11 @@
<string name="piko_single_page_settings">Single settings page</string>
<string name="piko_single_page_settings_desc">View settings in a single page</string>

<string name="piko_native_translator">Enable native post translation</string>
<string name="piko_native_translator_provider">Translation providers</string>
<string name="piko_native_translator_to_lang">Translate to</string>
<string name="piko_native_translator_zero_text">No text found to translate</string>
<string name="piko_native_translator_google">Google translator</string>
<string name="piko_native_translator_google_v2">Google translator V2</string>

</resources>

0 comments on commit 737fb9e

Please sign in to comment.