Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions extensions/instagram/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:instagram:stub"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package app.revanced.extension.instagram.misc.followbackindicator;

import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.instagram.misc.followbackindicator.Helper;
import com.instagram.common.session.UserSession;

@SuppressWarnings("unused")
public class FollowBackIndicatorPatch {

public static void indicator(UserSession userSession, Object profileInfoObject, Object badgeObject){
try {
String loggedInUserId = userSession.getUserId();
Object viewingProfileUserObject = Helper.getViewingProfileUserObject(profileInfoObject);
String viewingProfileUserId = Helper.getViewingProfileUserId(viewingProfileUserObject);

// If the logged in user id is same as viewing profile, then no need to display the badge.
if(loggedInUserId.equals(viewingProfileUserId)) return;

Boolean followed_by = Helper.getFollowbackInfo(viewingProfileUserObject);
String indicatorText = followed_by ? "Follows you" : "Does not follow you";
Helper.setInternalBadgeText(badgeObject,indicatorText);

} catch (Exception ex){
Logger.printException(() -> "Failed follow back indicator", ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package app.revanced.extension.instagram.misc.followbackindicator;

import java.lang.reflect.Method;
import android.widget.TextView;
import android.view.View;
import java.lang.reflect.Field;
@SuppressWarnings("unused")
public class Helper {

/**
* Given method name and class object, this function invokes
* the method on the object by bypassing access restrictions.
*
* @param clsObj The object on which to invoke the method.
* @param methodName The name of the method to invoke.
* @return The return value of the invoked method as Object.
* @throws Exception If an exception occurs during the method invocation.
**/
private static Object invokeMethod(Object clsObj, String methodName) throws Exception {
return clsObj.getClass().getDeclaredMethod(methodName).invoke(clsObj);
}

/**
* Given profile info object, this function return user object.
*
* @param classObject profile info object.
* @return The user data as Object.
* @throws Exception If an exception occurs during the method invocation.
**/
public static Object getViewingProfileUserObject(Object classObject)throws Exception{
Class<?> clazz = classObject.getClass();
Field field = clazz.getDeclaredField("FieldName");
field.setAccessible(true);
return field.get(classObject);
}

/**
* Given user object, this function returns if an user
* is following the logged it user or not.
*
* @param userObject The viewing profile user object.
* @return The boolean follow back value.
* @throws Exception If an exception occurs during the method invocation.
**/
public static Boolean getFollowbackInfo(Object userObject) throws Exception {
Class<?> clazz = Class.forName("className");
Method method = clazz.getDeclaredMethod("methodName", userObject.getClass());
method.setAccessible(true);
Object result = method.invoke(null, userObject);
return (Boolean) result;
}


/**
* Given user object, this function returns user's id.
*
* @param userObject The viewing profile user object.
* @return The user ID as string.
* @throws Exception If an exception occurs during the method invocation.
**/
public static String getViewingProfileUserId(Object userObject) throws Exception {
return (String) Helper.invokeMethod(userObject, "getId");
}

/**
* Given badge object and text, this function,
* sets text to the badge and makes it visible.
*
* @param badgeObject The viewing profile user object.
* @param text String text to set in badge label.
* @throws Exception If an exception occurs during the method invocation.
**/
public static void setInternalBadgeText(Object badgeObject,String text) throws Exception {
TextView badgeView = (TextView) Helper.invokeMethod(badgeObject,"getView");
badgeView.setVisibility(View.VISIBLE);
badgeView.setText(text);
}
}
17 changes: 17 additions & 0 deletions extensions/instagram/stub/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
plugins {
alias(libs.plugins.android.library)
}

android {
namespace = "app.revanced.extension"
compileSdk = 34

defaultConfig {
minSdk = 21
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
1 change: 1 addition & 0 deletions extensions/instagram/stub/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.instagram.common.session;

public class UserSession {
public String getUserId() {
return "";
}
}
4 changes: 4 additions & 0 deletions patches/api/patches.api
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,10 @@ public final class app/revanced/patches/instagram/misc/extension/SharedExtension
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}

public final class app/revanced/patches/instagram/misc/followBackIndicator/FollowBackIndicatorPatchKt {
public static final fun getFollowBackIndicatorPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}

public final class app/revanced/patches/instagram/misc/links/OpenLinksExternallyPatchKt {
public static final fun getOpenLinksExternallyPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package app.revanced.patches.instagram.misc.followBackIndicator
import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags

internal const val INTERNAL_BADGE_TARGET_STRING = "bindInternalBadges"
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/instagram/misc/followbackindicator/"
internal const val EXTENSION_HELPER_CLASS_DESCRIPTOR = "${EXTENSION_CLASS_DESCRIPTOR}Helper;"

internal val bindInternalBadgeFingerprint = fingerprint {
strings(INTERNAL_BADGE_TARGET_STRING)
}

internal val bindRowViewTypesFingerprint = fingerprint {
strings("NONE should not map to item type")
}

internal val nametagResultCardViewSetButtonMethodFingerprint = fingerprint {
returns("V")
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
custom { method, classDef ->
classDef.type.endsWith("NametagResultCardView;") && method.parameters.size == 3
}
}

internal val getFollowbackInfoExtensionFingerprint = fingerprint {
custom { method, classDef ->
method.name == "getFollowbackInfo" && classDef.type == EXTENSION_HELPER_CLASS_DESCRIPTOR
}
}

internal val getViewingProfileUserObjectExtensionFingerprint = fingerprint {
custom { method, classDef ->
method.name == "getViewingProfileUserObject" && classDef.type == EXTENSION_HELPER_CLASS_DESCRIPTOR
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package app.revanced.patches.instagram.misc.followBackIndicator

import app.revanced.patcher.Fingerprint
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.instagram.misc.extension.sharedExtensionPatch
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference

@Suppress("unused")
val followBackIndicatorPatch = bytecodePatch(
name = "Follow back indicator",
description = "Adds a label in profile page, indicating if an user is follows you back.",
use = true,
) {
dependsOn(sharedExtensionPatch)

compatibleWith("com.instagram.android")

execute {
/**
* This function replaces a string instruction with a new one.
*
* @param index The index of the string constant.
* @param value The new string to be replaced with.
*/
fun Fingerprint.changeString(
index: Int,
value: String,
) {
method.instructions.filter { it.opcode == Opcode.CONST_STRING }[index].let { instruction ->
val register = (instruction as BuilderInstruction21c).registerA
method.replaceInstruction(instruction.location.index, "const-string v$register, \"$value\"")
}
}

// This fingerprint is used to identify the static method and its defining class,
// which is used for identifying a user's follow back status.
nametagResultCardViewSetButtonMethodFingerprint.method.apply {
val moveResultIndex = instructions.first { it.opcode == Opcode.MOVE_RESULT }.location.index
val invokeStaticMethodReference = getInstruction(moveResultIndex - 1).getReference<MethodReference>()

val methodDefClassName = invokeStaticMethodReference!!.definingClass.removePrefix("L").replace("/", ".").removeSuffix(";")
getFollowbackInfoExtensionFingerprint.changeString(0,methodDefClassName)

val methodName = invokeStaticMethodReference.name
getFollowbackInfoExtensionFingerprint.changeString(1,methodName)
}

// This constant stores the value of the obfuscated profile info class,
// which is later used to find the index of the parameter.
var profileInfoClassName:String

// This fingerprint is used to identify field name in obfuscated profile info class,
// that holds user data.
bindRowViewTypesFingerprint.method.apply {
val igetObjectInstruction = instructions.first { it.opcode == Opcode.IGET_OBJECT }
val fieldReference = igetObjectInstruction.getReference<FieldReference>()
val userObjectFieldName = fieldReference!!.name
getViewingProfileUserObjectExtensionFingerprint.changeString(0,userObjectFieldName)
profileInfoClassName = fieldReference.definingClass
}

// This fingerprint is used to identify the internal badge, which is used for displaying follow back status.
bindInternalBadgeFingerprint.method.apply {
val internalBadgeStringIndex = bindInternalBadgeFingerprint.stringMatches!![0].index

// Identify the profile info in the method parameter, which is later passed to our custom hook.
val profileInfoParameter = parameters.indexOfFirst { it.type == profileInfoClassName }

val internalBadgeInstructionIndex = indexOfFirstInstruction(internalBadgeStringIndex, Opcode.IGET_OBJECT)
val internalBadgeInstruction = getInstruction<TwoRegisterInstruction>(internalBadgeInstructionIndex)
// Internal badge is an element/view, which is used internally to mark developers.
// We hook and update its text to display the follow back status.
val internalBadgeRegistry = internalBadgeInstruction.registerA
// User profile page (obfuscated) contains all the elements that are present on the user page.
// We are hooking it in order to find user session, which is used to get info on logged in user.
val userProfilePageRegistry = internalBadgeInstruction.registerB

// Finding the necessary dummy registries.
val dummyRegistryInstructionIndex = indexOfFirstInstruction(internalBadgeInstructionIndex + 1, Opcode.IGET_OBJECT)
val dummyRegistry1 = getInstruction<TwoRegisterInstruction>(dummyRegistryInstructionIndex).registerA
val dummyRegistry2 = getInstruction<OneRegisterInstruction>(internalBadgeStringIndex).registerA

// Instruction to which the call needs to transfer after our hook.
val invokeStaticRangeIndex = indexOfFirstInstruction(internalBadgeInstructionIndex, Opcode.INVOKE_STATIC_RANGE)

val userSessionClassName = "Lcom/instagram/common/session/UserSession;"
// Finding the user profile page (obfuscated) class name.
val userProfilePageElementsClassName = internalBadgeInstruction.getReference<FieldReference>()!!.definingClass
// Finding the user session field.
val userSessionFieldName = classes.find { it.type == userProfilePageElementsClassName }!!.fields.first { it.type == userSessionClassName }.name

// Added instructions:
// Get the user session.
// Move the profile info parameter to a suitable registry.
// Call our hook, which will update the badge.
addInstructionsWithLabels(
internalBadgeInstructionIndex + 1,
"""
iget-object v$dummyRegistry1, v$userProfilePageRegistry, $userProfilePageElementsClassName->$userSessionFieldName:$userSessionClassName
move-object/from16 v$dummyRegistry2, p$profileInfoParameter

invoke-static {v$dummyRegistry1,v$dummyRegistry2, v$internalBadgeRegistry}, ${EXTENSION_CLASS_DESCRIPTOR}FollowBackIndicatorPatch;->indicator($userSessionClassName Ljava/lang/Object;Ljava/lang/Object;)V
goto :revanced
""".trimIndent(),
ExternalLabel("revanced", getInstruction(invokeStaticRangeIndex)),
)
}
}
}