Skip to content
This repository has been archived by the owner on Oct 26, 2024. It is now read-only.

feat(Discord): Add Plugin loader patch #690

Closed
wants to merge 1 commit into from
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@

package app.revanced.integrations.discord.plugin;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Color;
import android.os.Build;
import app.revanced.integrations.shared.react.BaseRemoteReactPreloadScriptBootstrapper;
import com.facebook.react.bridge.CatalystInstanceImpl;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Objects;


public class BunnyBootstrapper extends BaseRemoteReactPreloadScriptBootstrapper {
private WeakReference<Context> context;

private JSONObject theme;
private final HashMap<String, Integer> RESOURCE_COLORS = new HashMap<>();
private final HashMap<String, int[]> COMPONENT_COLORS = new HashMap<>();


@Override
protected void initialize(Context context) {
this.context = new WeakReference<>(context);

download(
"https://raw.githubusercontent.com/pyoncord/detta-builds/main/bunny.js",
getWorkingDirectoryFile("bunny.bundle"),
1024
);

readThemeFile();
}

@Override
public void loadPreloadScripts(CatalystInstanceImpl instance) {
var config = new JSONObject();
try {
config.put("loaderName", "ReVanced");
config.put("loaderVersion", "1.0.0");
config.put("hasThemeSupport", true);
buildThemeConfig(config);
buildSysColorsConfig(config);
} catch (Exception e) {
throw new RuntimeException(e);
}

instance.setGlobalVariable("__PYON_LOADER__", config.toString());
super.loadPreloadScripts(instance);
}

private void buildThemeConfig(JSONObject config) throws JSONException {
config.put("storedTheme", theme);
}

private void buildSysColorsConfig(JSONObject config) throws JSONException {
boolean isSystemColorsSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;

config.put("isSysColorsSupported", isSystemColorsSupported);

if (isSystemColorsSupported) {
var context = this.context.get();
var resources = context.getResources();
var packageName = context.getPackageName();

String[] accents = {"accent1", "accent2", "accent3", "neutral1", "neutral2"};
int[] shades = {0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000};

var colors = new JSONObject() {{
for (String accent : accents) {
var accentColors = new JSONArray() {{
for (int shade : shades) {
@SuppressLint("DiscouragedApi")
var colorResourceId = resources.getIdentifier(
"system_" + accent + "_" + shade,
"color",
packageName
);
var color = colorResourceId == 0 ? 0 : context.getColor(colorResourceId);
var hexColor = String.format("#%06X", (0xFFFFFF & color));

put(hexColor);
}
}};

put(accent, accentColors);
}
}};

config.put("sysColors", colors);
}
}

public int hookColorDark(String themeKey, int originalColor) {
return getColor(themeKey, originalColor, true);
}

public int hookColorLight(String themeKey, int originalColor) {
return getColor(themeKey, originalColor, false);
}

public int hookRawColor(Object contextOrResource, int id, int originalColor) {
return readRawColor(contextOrResource, id, originalColor);
}

private int getColor(String colorName, int originalColor, boolean isDark) {
waitUntilInitialized();

var colors = COMPONENT_COLORS.get(colorName);
if (colors == null) {
return originalColor;
}

if (isDark) {
return colors[0];
}

// Only if there are two colors in the array we return the light color
if (colors.length == 2) {
return colors[1];
}

return originalColor;
}

private void readThemeFile() {
var themeFile = getWorkingDirectoryFile("pyoncord/theme.json");
var legacyThemeFile = getWorkingDirectoryFile("vendetta_theme.json");

if (legacyThemeFile.exists() && !legacyThemeFile.renameTo(themeFile)) {
throw new RuntimeException("Failed to rename theme file");
}

if (!themeFile.exists()) {
return;
}

try {
theme = new JSONObject(read(themeFile, 256));
} catch (JSONException e) {
throw new RuntimeException(e);
}

readThemeColors();
}

private int hexStringToColorInt(String hexString) {
var parsed = Color.parseColor(hexString);
return (hexString.length() == 7) ? parsed : parsed & 0xFFFFFF | (parsed >>> 24);
}

private void readThemeColors() {
try {
var data = theme.getJSONObject("data");

var jsonRawColors = data.getJSONObject("rawColors");
var jsonSemanticColors = data.getJSONObject("semanticColors");

for (var colors = jsonRawColors.keys(); colors.hasNext(); ) {
var colorKey = colors.next();
int color = hexStringToColorInt(jsonRawColors.getString(colorKey));
RESOURCE_COLORS.put(colorKey.toLowerCase(), color);
}

for (var colors = jsonSemanticColors.keys(); colors.hasNext(); ) {
var componentName = colors.next();
var componentColors = jsonSemanticColors.getJSONArray(componentName);

int[] value;
if (componentColors.length() == 1) {
value = new int[]{hexStringToColorInt(componentColors.getString(0))};
} else {
value = new int[]{
hexStringToColorInt(componentColors.getString(0)),
hexStringToColorInt(componentColors.getString(1))
};
}

COMPONENT_COLORS.put(componentName, value);
}
} catch (JSONException e) {
throw new RuntimeException(e);
}
}

public int readRawColor(Object contextOrResource, int id, int originalColor) {
waitUntilInitialized();

Resources resources;
if (contextOrResource instanceof Context) {
resources = ((Context) contextOrResource).getResources();
} else {
resources = (Resources) contextOrResource;
}

var name = resources.getResourceEntryName(id);
var color = RESOURCE_COLORS.get(name);

return Objects.requireNonNullElse(color, originalColor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package app.revanced.integrations.discord.plugin;

import android.app.Activity;
import com.facebook.react.bridge.CatalystInstanceImpl;

@SuppressWarnings("unused")
public final class BunnyBootstrapperPatch {
private final static BunnyBootstrapper INSTANCE = new BunnyBootstrapper();

public static void hookOnCreate(Activity mainActivity) {
INSTANCE.hookOnCreate(mainActivity);
}

public static void hookLoadScriptFromFile(CatalystInstanceImpl instance) {
INSTANCE.hookLoadScriptFromFile(instance);
}

public static int hookColorDark(String themeKey, int originalColor) {
return INSTANCE.hookColorDark(themeKey, originalColor);
}

public static int hookColorLight(String themeKey, int originalColor) {
return INSTANCE.hookColorLight(themeKey, originalColor);
}

public static int hookRawColor(Object contextOrResource, int id, int originalColor) {
return INSTANCE.hookRawColor(contextOrResource, id, originalColor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package app.revanced.integrations.shared.react;

import android.app.Activity;
import android.content.Context;
import com.facebook.react.bridge.CatalystInstanceImpl;

import java.io.*;

public abstract class BaseReactPreloadScriptBootstrapper {
private Thread initializeThread;
private File workingDirectory;

protected abstract void initialize(Context context);

public final void hookOnCreate(Activity mainActivity) {
workingDirectory = mainActivity.getFilesDir();
if (!workingDirectory.exists() && !workingDirectory.mkdirs()) {
throw new RuntimeException("Failed to create working directory");
}

initializeThread = new Thread(() -> initialize(mainActivity));
initializeThread.start();
}

public final void hookLoadScriptFromFile(CatalystInstanceImpl instance) {
waitUntilInitialized();
loadPreloadScripts(instance);
}

protected void loadPreloadScripts(CatalystInstanceImpl instance) {
final var preloadScripts = workingDirectory.listFiles(pathname ->
pathname.isFile() && pathname.getName().endsWith(".bundle"));
assert preloadScripts != null;

for (final var preloadScript : preloadScripts) {
final var path = preloadScript.getAbsolutePath();
instance.loadPreloadScriptFromFile(path, path, false);
}
}

protected void waitUntilInitialized() {
try {
initializeThread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

protected final File getWorkingDirectoryFile(String name) {
return new File(workingDirectory, name);
}

protected final void write(InputStream inputStream, File file, int bufferSize) {
try (final var fileOutputStream = new FileOutputStream(file)) {
final var buffer = new byte[bufferSize];

int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

protected final String read(File file, int bufferSize) {
try (final var fileInputStream = new FileInputStream(file)) {
final var buffer = new byte[bufferSize];
final var stringBuilder = new StringBuilder();

int bytesRead;
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
stringBuilder.append(new String(buffer, 0, bytesRead));
}

return stringBuilder.toString();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package app.revanced.integrations.shared.react;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

@SuppressWarnings("unused")
public abstract class BaseRemoteReactPreloadScriptBootstrapper extends BaseReactPreloadScriptBootstrapper {
protected final void download(String url, File preloadScriptFile, int bufferSize) {
final var eTagFile = getWorkingDirectoryFile(preloadScriptFile.getName() + ".etag");

try {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
if (eTagFile.exists() && preloadScriptFile.exists()) {
connection.setRequestProperty("If-None-Match", read(eTagFile, 256));
}
connection.connect();

if (connection.getResponseCode() == 304) {
connection.disconnect();
return;
}

if (connection.getResponseCode() != 200) {
throw new RuntimeException("Failed to download the preload script: " + connection.getResponseCode());
}

final var eTagHeader = connection.getHeaderField("ETag");
if (eTagHeader != null) {
write(new ByteArrayInputStream(eTagHeader.getBytes()), eTagFile, 256);
}

write(connection.getInputStream(), preloadScriptFile, bufferSize);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.facebook.react.bridge;

import android.content.res.AssetManager;

public class CatalystInstanceImpl {
public native void setGlobalVariable(String propName, String jsonValue);

public void loadScriptFromAssets(AssetManager assetManager, String assetURL, boolean loadSynchronously) {
}

public void loadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously) {
}

public void loadPreloadScriptFromFile(String fileName, String sourceURL, boolean loadSynchronously) {
}
}
Loading