Merge branch 'github/fork/Swakshan/Instagram/followback-indicator' into 'dev'

feat(Instagram): Add `Follow back indicator` patch

See merge request ReVanced/revanced-patches!6564
This commit is contained in:
Placeholder github Source User 2026-03-25 00:02:45 +00:00
commit 3abbd1f6fc
9 changed files with 292 additions and 0 deletions

View file

@ -1,5 +1,6 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:instagram:stub"))
}
android {

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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
}
}

View file

@ -0,0 +1 @@
<manifest/>

View file

@ -0,0 +1,7 @@
package com.instagram.common.session;
public class UserSession {
public String getUserId() {
return "";
}
}

View file

@ -345,6 +345,10 @@ public final class app/revanced/patches/instagram/misc/extension/SharedExtension
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/Patch;
}
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/Patch;
}

View file

@ -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
}
}

View file

@ -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)),
)
}
}
}