feat(Discord): Add Plugin loader patch

This commit is contained in:
oSumAtrIX 2024-09-11 16:19:46 +02:00
parent 5ffff1bd40
commit 9b083e6d84
No known key found for this signature in database
GPG Key ID: A9B3094ACDB604B4
5 changed files with 371 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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