feat(Boost/Sync for Reddit): Add Fix Redgifs patch (#5725)

This commit is contained in:
Eric Ahn
2025-09-04 14:29:58 -07:00
committed by GitHub
parent b340769cf3
commit c66c42e946
15 changed files with 400 additions and 0 deletions

View File

@@ -1,4 +1,6 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:boostforreddit:stub"))
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
}

View File

@@ -0,0 +1,22 @@
package app.revanced.extension.boostforreddit;
import app.revanced.extension.shared.fixes.redgifs.BaseFixRedgifsApiPatch;
import okhttp3.OkHttpClient;
/**
* @noinspection unused
*/
public class FixRedgifsApiPatch extends BaseFixRedgifsApiPatch {
static {
INSTANCE = new FixRedgifsApiPatch();
}
public String getDefaultUserAgent() {
// Boost uses a static user agent for Redgifs API calls
return "Boost";
}
public static OkHttpClient createClient() {
return new OkHttpClient.Builder().addInterceptor(INSTANCE).build();
}
}

View File

@@ -1,3 +1,4 @@
dependencies {
implementation(project(":extensions:shared:library"))
compileOnly(libs.okhttp)
}

View File

@@ -18,4 +18,5 @@ android {
dependencies {
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
}

View File

@@ -0,0 +1,71 @@
package app.revanced.extension.shared.fixes.redgifs;
import androidx.annotation.NonNull;
import org.json.JSONException;
import java.io.IOException;
import java.net.HttpURLConnection;
import app.revanced.extension.shared.Logger;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public abstract class BaseFixRedgifsApiPatch implements Interceptor {
protected static BaseFixRedgifsApiPatch INSTANCE;
public abstract String getDefaultUserAgent();
@NonNull
@Override
public Response intercept(@NonNull Chain chain) throws IOException {
Request request = chain.request();
if (!request.url().host().equals("api.redgifs.com")) {
return chain.proceed(request);
}
String userAgent = getDefaultUserAgent();
if (request.header("Authorization") != null) {
Response response = chain.proceed(request.newBuilder().header("User-Agent", userAgent).build());
if (response.isSuccessful()) {
return response;
}
// It's possible that the user agent is being overwritten later down in the interceptor
// chain, so make sure we grab the new user agent from the request headers.
userAgent = response.request().header("User-Agent");
response.close();
}
try {
RedgifsTokenManager.RedgifsToken token = RedgifsTokenManager.refreshToken(userAgent);
// Emulate response for old OAuth endpoint
if (request.url().encodedPath().equals("/v2/oauth/client")) {
String responseBody = RedgifsTokenManager.getEmulatedOAuthResponseBody(token);
return new Response.Builder()
.message("OK")
.code(HttpURLConnection.HTTP_OK)
.protocol(Protocol.HTTP_1_1)
.request(request)
.header("Content-Type", "application/json")
.body(ResponseBody.create(
responseBody, MediaType.get("application/json")))
.build();
}
Request modifiedRequest = request.newBuilder()
.header("Authorization", "Bearer " + token.getAccessToken())
.header("User-Agent", userAgent)
.build();
return chain.proceed(modifiedRequest);
} catch (JSONException ex) {
Logger.printException(() -> "Could not parse Redgifs response", ex);
throw new IOException(ex);
}
}
}

View File

@@ -0,0 +1,94 @@
package app.revanced.extension.shared.fixes.redgifs;
import static app.revanced.extension.shared.requests.Route.Method.GET;
import androidx.annotation.GuardedBy;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import app.revanced.extension.shared.requests.Requester;
/**
* Manages Redgifs token lifecycle.
*/
public class RedgifsTokenManager {
public static class RedgifsToken {
// Expire after 23 hours to provide some breathing room
private static final long EXPIRY_SECONDS = 23 * 60 * 60;
private final String accessToken;
private final long refreshTimeInSeconds;
public RedgifsToken(String accessToken, long refreshTime) {
this.accessToken = accessToken;
this.refreshTimeInSeconds = refreshTime;
}
public String getAccessToken() {
return accessToken;
}
public long getExpiryTimeInSeconds() {
return refreshTimeInSeconds + EXPIRY_SECONDS;
}
public boolean isValid() {
if (accessToken == null) return false;
return getExpiryTimeInSeconds() >= System.currentTimeMillis() / 1000;
}
}
public static final String REDGIFS_API_HOST = "https://api.redgifs.com";
private static final String GET_TEMPORARY_TOKEN = REDGIFS_API_HOST + "/v2/auth/temporary";
@GuardedBy("itself")
private static final Map<String, RedgifsToken> tokenMap = new HashMap<>();
private static String getToken(String userAgent) throws IOException, JSONException {
HttpURLConnection connection = (HttpURLConnection) new URL(GET_TEMPORARY_TOKEN).openConnection();
connection.setFixedLengthStreamingMode(0);
connection.setRequestMethod(GET.name());
connection.setRequestProperty("User-Agent", userAgent);
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("Accept", "application/json");
connection.setUseCaches(false);
JSONObject responseObject = Requester.parseJSONObject(connection);
return responseObject.getString("token");
}
public static RedgifsToken refreshToken(String userAgent) throws IOException, JSONException {
synchronized(tokenMap) {
// Reference: https://github.com/JeffreyCA/Apollo-ImprovedCustomApi/pull/67
RedgifsToken token = tokenMap.get(userAgent);
if (token != null && token.isValid()) {
return token;
}
// Copy user agent from original request if present because Redgifs verifies
// that the user agent in subsequent requests matches the one in the OAuth token.
String accessToken = getToken(userAgent);
long refreshTime = System.currentTimeMillis() / 1000;
token = new RedgifsToken(accessToken, refreshTime);
tokenMap.put(userAgent, token);
return token;
}
}
public static String getEmulatedOAuthResponseBody(RedgifsToken token) throws JSONException {
// Reference: https://github.com/JeffreyCA/Apollo-ImprovedCustomApi/pull/67
JSONObject responseObject = new JSONObject();
responseObject.put("access_token", token.accessToken);
responseObject.put("expiry_time", token.getExpiryTimeInSeconds() - (System.currentTimeMillis() / 1000));
responseObject.put("scope", "read");
responseObject.put("token_type", "Bearer");
return responseObject.toString();
}
}

View File

@@ -2,4 +2,5 @@ dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:syncforreddit:stub"))
compileOnly(libs.annotation)
compileOnly(libs.okhttp)
}

View File

@@ -0,0 +1,22 @@
package app.revanced.extension.syncforreddit;
import app.revanced.extension.shared.fixes.redgifs.BaseFixRedgifsApiPatch;
import okhttp3.OkHttpClient;
/**
* @noinspection unused
*/
public class FixRedgifsApiPatch extends BaseFixRedgifsApiPatch {
static {
INSTANCE = new FixRedgifsApiPatch();
}
public String getDefaultUserAgent() {
// To be filled in by patch
return "";
}
public static OkHttpClient install(OkHttpClient.Builder builder) {
return builder.addInterceptor(INSTANCE).build();
}
}

View File

@@ -512,6 +512,13 @@ public final class app/revanced/patches/reddit/ad/general/HideAdsPatchKt {
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/reddit/customclients/FixRedgifsApiPatchKt {
public static final field CREATE_NEW_CLIENT_METHOD Ljava/lang/String;
public static final field INSTALL_NEW_CLIENT_METHOD Ljava/lang/String;
public static final fun fixRedgifsApiPatch (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch;
public static synthetic fun fixRedgifsApiPatch$default (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/reddit/customclients/FixSLinksPatchKt {
public static final field RESOLVE_S_LINK_METHOD Ljava/lang/String;
public static final field SET_ACCESS_TOKEN_METHOD Ljava/lang/String;
@@ -540,6 +547,10 @@ public final class app/revanced/patches/reddit/customclients/boostforreddit/fix/
public static final fun getFixAudioMissingInDownloadsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/reddit/customclients/boostforreddit/fix/redgifs/FixRedgifsApiPatchKt {
public static final fun getFixRedgifsApi ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/reddit/customclients/boostforreddit/fix/slink/FixSLinksPatchKt {
public static final field EXTENSION_CLASS_DESCRIPTOR Ljava/lang/String;
public static final fun getFixSlinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
@@ -610,6 +621,10 @@ public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/redgifs/FixRedgifsApiPatchKt {
public static final fun getFixRedgifsApi ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/slink/FixSLinksPatchKt {
public static final field EXTENSION_CLASS_DESCRIPTOR Ljava/lang/String;
public static final fun getFixSLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;

View File

@@ -0,0 +1,17 @@
package app.revanced.patches.reddit.customclients
import app.revanced.patcher.patch.BytecodePatchBuilder
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.bytecodePatch
const val INSTALL_NEW_CLIENT_METHOD = "install(Lokhttp3/OkHttpClient${'$'}Builder;)Lokhttp3/OkHttpClient;"
const val CREATE_NEW_CLIENT_METHOD = "createClient()Lokhttp3/OkHttpClient;"
fun fixRedgifsApiPatch(
extensionPatch: Patch<*>,
block: BytecodePatchBuilder.() -> Unit = {},
) = bytecodePatch(name = "Fix Redgifs API") {
dependsOn(extensionPatch)
block()
}

View File

@@ -0,0 +1,20 @@
package app.revanced.patches.reddit.customclients.boostforreddit.fix.redgifs
import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal val createOkHttpClientFingerprint = fingerprint {
accessFlags(AccessFlags.PRIVATE)
opcodes(
Opcode.NEW_INSTANCE,
Opcode.INVOKE_DIRECT,
Opcode.NEW_INSTANCE,
Opcode.INVOKE_DIRECT,
Opcode.NEW_INSTANCE,
Opcode.INVOKE_DIRECT,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT
)
custom { _, classDef -> classDef.sourceFile == "RedGifsAPIv2.java" }
}

View File

@@ -0,0 +1,38 @@
package app.revanced.patches.reddit.customclients.boostforreddit.fix.redgifs
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patches.reddit.customclients.CREATE_NEW_CLIENT_METHOD
import app.revanced.patches.reddit.customclients.boostforreddit.misc.extension.sharedExtensionPatch
import app.revanced.patches.reddit.customclients.fixRedgifsApiPatch
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/boostforreddit/FixRedgifsApiPatch;"
@Suppress("unused")
val fixRedgifsApi = fixRedgifsApiPatch(
extensionPatch = sharedExtensionPatch
) {
compatibleWith("com.rubenmayayo.reddit")
execute {
// region Patch Redgifs OkHttp3 client.
createOkHttpClientFingerprint.method.apply {
val index = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>()
reference?.name == "build" && reference.definingClass == "Lokhttp3/OkHttpClient\$Builder;"
}
replaceInstruction(
index,
"""
invoke-static { }, ${EXTENSION_CLASS_DESCRIPTOR}->$CREATE_NEW_CLIENT_METHOD
"""
)
}
// endregion
}
}

View File

@@ -0,0 +1,39 @@
package app.revanced.patches.reddit.customclients.sync.syncforreddit.fix.redgifs
import app.revanced.patcher.extensions.InstructionExtensions.instructions
import app.revanced.patcher.fingerprint
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.writeRegister
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction11n
internal val createOkHttpClientFingerprint = fingerprint {
accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC)
returns("V")
parameters()
custom { method, classDef ->
// There are four functions (each creating a client) defined in this file with very similar fingerprints.
// We're looking for the one that only creates one object (the builder) and sets client options true
// (thus never reloading the register with a 0).
classDef.sourceFile == "OkHttpHelper.java" &&
method.instructions.count { it.opcode == Opcode.NEW_INSTANCE } == 1 &&
method.indexOfFirstInstruction {
opcode == Opcode.CONST_4 && writeRegister == 1 && (this as Instruction11n).narrowLiteral == 0
} == -1
}
}
internal val getDefaultUserAgentFingerprint = fingerprint {
custom { method, classDef ->
method.name == "getDefaultUserAgent" && classDef.type == EXTENSION_CLASS_DESCRIPTOR
}
}
internal val getOriginalUserAgentFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returns("Ljava/lang/String;")
parameters()
custom { _, classDef -> classDef.sourceFile == "AccountSingleton.java" }
}

View File

@@ -0,0 +1,56 @@
package app.revanced.patches.reddit.customclients.sync.syncforreddit.fix.redgifs
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patches.reddit.customclients.INSTALL_NEW_CLIENT_METHOD
import app.revanced.patches.reddit.customclients.fixRedgifsApiPatch
import app.revanced.patches.reddit.customclients.sync.syncforreddit.extension.sharedExtensionPatch
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/syncforreddit/FixRedgifsApiPatch;"
@Suppress("unused")
val fixRedgifsApi = fixRedgifsApiPatch(
extensionPatch = sharedExtensionPatch
) {
compatibleWith(
"com.laurencedawson.reddit_sync",
"com.laurencedawson.reddit_sync.pro",
"com.laurencedawson.reddit_sync.dev",
)
execute {
// region Patch Redgifs OkHttp3 client.
createOkHttpClientFingerprint.method.apply {
val index = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>()
reference?.name == "build" && reference.definingClass == "Lokhttp3/OkHttpClient\$Builder;"
}
val register = getInstruction<FiveRegisterInstruction>(index).registerC
replaceInstruction(
index,
"""
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->$INSTALL_NEW_CLIENT_METHOD
"""
)
}
getDefaultUserAgentFingerprint.method.apply {
addInstructions(
0,
"""
invoke-static { }, ${getOriginalUserAgentFingerprint.method}
move-result-object v0
return-object v0
"""
)
}
// endregion
}
}

View File

@@ -132,6 +132,7 @@ internal val Instruction.registersUsed: List<Int>
get() = when (this) {
is FiveRegisterInstruction -> {
when (registerCount) {
0 -> listOf()
1 -> listOf(registerC)
2 -> listOf(registerC, registerD)
3 -> listOf(registerC, registerD, registerE)