diff --git a/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt b/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt deleted file mode 100644 index 79a1322..0000000 --- a/app/src/main/kotlin/io/github/revenge/xposed/Utils.kt +++ /dev/null @@ -1,32 +0,0 @@ -package GoonXposed.xposed - -import android.app.AndroidAppHelper -import android.content.Intent -import kotlin.system.exitProcess -import kotlinx.serialization.json.Json - -class Utils { - companion object { - val JSON = Json { ignoreUnknownKeys = true } - - fun reloadApp() { - val application = AndroidAppHelper.currentApplication() - val intent = - application.packageManager.getLaunchIntentForPackage(application.packageName) - application.startActivity(Intent.makeRestartActivityTask(intent!!.component)) - exitProcess(0) - } - } - - object Log { - fun e(msg: String) = android.util.Log.e(Constants.LOG_TAG, msg) - fun e(msg: String, throwable: Throwable) = - android.util.Log.e(Constants.LOG_TAG, msg, throwable) - fun i(msg: String) = android.util.Log.i(Constants.LOG_TAG, msg) - fun i(msg: String, throwable: Throwable) = - android.util.Log.i(Constants.LOG_TAG, msg, throwable) - fun w(msg: String) = android.util.Log.w(Constants.LOG_TAG, msg) - fun w(msg: String, throwable: Throwable) = - android.util.Log.w(Constants.LOG_TAG, msg, throwable) - } -} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/FixResourcesModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/FixResourcesModule.kt deleted file mode 100644 index c6f39fe..0000000 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/FixResourcesModule.kt +++ /dev/null @@ -1,23 +0,0 @@ -package GoonXposed.xposed.modules - -import android.content.res.Resources -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE -import GoonXposed.xposed.Module - -/** - * Hooks [Resources.getIdentifier] to fix resource package name mismatch. - */ -object FixResourcesModule : Module() { - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - if (packageName != TARGET_PACKAGE) { - Resources::class.java.hookMethod( - "getIdentifier", String::class.java, String::class.java, String::class.java - ) { - before { - if (args[2] == packageName) args[2] = TARGET_PACKAGE - } - } - } - } -} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt deleted file mode 100644 index a1dbf30..0000000 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/HookScriptLoaderModule.kt +++ /dev/null @@ -1,121 +0,0 @@ -package GoonXposed.xposed.modules - -import android.content.res.AssetManager -import android.content.res.XModuleResources -import de.robv.android.xposed.IXposedHookZygoteInit -import de.robv.android.xposed.XposedBridge -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Constants -import GoonXposed.xposed.HookStateHolder -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Log -import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.runBlocking -import java.io.File -import java.lang.reflect.Method - -/** - * Hooks React Native's script loading methods to load custom scripts and bundles. - * - * Preload scripts should be placed in the [PRELOADS_DIR] directory inside the module's files directory. - * - * The main bundle should be placed in the [Constants.CACHE_DIR] directory named [Constants.MAIN_SCRIPT_FILE]. - * If the bundle file does not exist, it will attempt to load `assets://Shiggy.bundle` from the module's assets. - */ -object HookScriptLoaderModule : Module() { - private lateinit var preloadsDir: File - private lateinit var mainScript: File - - private lateinit var modulePath: String - private lateinit var resources: XModuleResources - - const val PRELOADS_DIR = "preloads" - - override fun onInit(startupParam: IXposedHookZygoteInit.StartupParam) { - this@HookScriptLoaderModule.modulePath = startupParam.modulePath - } - - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - val cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { asDir() } - val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { asDir() } - - preloadsDir = File(filesDir, PRELOADS_DIR).apply { asDir() } - mainScript = File(cacheDir, Constants.MAIN_SCRIPT_FILE).apply { asFile() } - - listOf( - "com.facebook.react.runtime.ReactInstance\$loadJSBundle$1", - "com.facebook.react.runtime.ReactInstance$1", - // TODO: Remove once Discord fully switches to Bridgeless - "com.facebook.react.bridge.CatalystInstanceImpl" - ).mapNotNull { classLoader.safeLoadClass(it) }.forEach { hook(it) } - } - - private fun hook(instance: Class<*>) = runCatching { - val loadScriptFromAssets = instance.method( - "loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType - ) - - val loadScriptFromFile = instance.method( - "loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType - ) - - loadScriptFromAssets.hook { - before { - Log.i("Received call to loadScriptFromAssets: ${args[1]} (sync: ${args[2]})") - runCustomScripts(loadScriptFromFile, loadScriptFromAssets) - } - } - - loadScriptFromFile.hook { - before { - Log.i("Received call to loadScriptFromFile: ${args[0]} (sync: ${args[2]})") - runCustomScripts(loadScriptFromFile, loadScriptFromAssets) - } - } - }.onFailure { - Log.e("Failed to hook script loading methods in ${instance.name}:", it) - } - - private fun HookScope.runCustomScripts(loadScriptFromFile: Method, loadScriptFromAssets: Method) { - Log.i("Running custom scripts...") - - runBlocking { - val ready = async { HookStateHolder.readyDeferred.join() } - val download = async { UpdaterModule.downloadScript().join() } - - awaitAll(ready, download) - } - - val loadSynchronously = args[2] - val runScriptFile = { file: File -> - Log.i("Loading script: ${file.absolutePath}") - - XposedBridge.invokeOriginalMethod( - loadScriptFromFile, thisObject, arrayOf(file.absolutePath, file.absolutePath, loadSynchronously) - ) - - Unit - } - - try { - preloadsDir.walk().filter { it.isFile }.forEach(runScriptFile) - - if (mainScript.exists()) runScriptFile(mainScript) - else { - Log.i("Main script does not exist, falling back") - - if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null) - - XposedBridge.invokeOriginalMethod( - loadScriptFromAssets, - thisObject, - arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously) - ) - } - } catch (e: Throwable) { - Log.e("Unable to run scripts:", e) - } - } -} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt deleted file mode 100644 index 41042fc..0000000 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/LogBoxModule.kt +++ /dev/null @@ -1,601 +0,0 @@ -package GoonXposed.xposed.modules - -import android.app.AlertDialog -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.widget.EditText -import android.widget.Toast -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.BuildConfig -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils -import GoonXposed.xposed.Utils.Log -import de.robv.android.xposed.XposedHelpers -import kotlinx.coroutines.* -import org.json.JSONArray -import org.json.JSONObject -import java.io.File -import java.net.HttpURLConnection -import java.net.URL -import GoonXposed.xposed.Utils.Companion.reloadApp - -object LogBoxModule : Module() { - lateinit var packageParam: XC_LoadPackage.LoadPackageParam - private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - this@LogBoxModule.packageParam = packageParam - - val dcdReactNativeHostClass = classLoader.loadClass("com.discord.bridge.DCDReactNativeHost") - val getUseDeveloperSupportMethod = dcdReactNativeHostClass.methods.first { it.name == "getUseDeveloperSupport" } - - getUseDeveloperSupportMethod.hook { - before { - result = true - } - } - - listOf( - "com.facebook.react.devsupport.BridgeDevSupportManager", - "com.facebook.react.devsupport.BridgelessDevSupportManager" - ).mapNotNull { packageParam.classLoader.safeLoadClass(it) }.forEach { - hookDevSupportManager(it) - } - - return@with - } - - private fun hookDevSupportManager(clazz: Class<*>) { - val showDevOptionsDialogMethod = clazz.methods.first { it.name == "showDevOptionsDialog" } - - showDevOptionsDialogMethod.hook { - before { - try { - val context = getContextFromDevSupport(clazz, thisObject) - if (context != null) { - showRecoveryMenu(context) - } - result = null - } catch (e: Exception) { - Log.e("Failed to show recovery menu: ${e.message}") - } - } - } - } - - private fun getContextFromDevSupport(clazz: Class<*>, instance: Any?): Context? { - return try { - val mReactInstanceDevHelperField = XposedHelpers.findField(clazz, "mReactInstanceDevHelper") - val helper = mReactInstanceDevHelperField.get(instance) - val getCurrentActivityMethod = helper?.javaClass?.methods?.first { it.name == "getCurrentActivity" } - getCurrentActivityMethod?.invoke(helper) as? Context - } catch (e: Exception) { - Log.e("Failed to get context: ${e.message}") - null - } - } - - private fun reloadReactNative(context: Context) { - try { - val reactInstanceManagerClass = - packageParam.classLoader.loadClass("com.facebook.react.ReactInstanceManager") - val reactApplicationClass = packageParam.classLoader.loadClass("com.facebook.react.ReactApplication") - - val app = context.applicationContext - - if (reactApplicationClass.isInstance(app)) { - val getReactNativeHostMethod = reactApplicationClass.methods.first { it.name == "getReactNativeHost" } - val reactNativeHost = getReactNativeHostMethod.invoke(app) - - val getReactInstanceManagerMethod = - reactNativeHost.javaClass.methods.first { it.name == "getReactInstanceManager" } - val reactInstanceManager = getReactInstanceManagerMethod.invoke(reactNativeHost) - - if (reactInstanceManager != null) { - val reloadMethod = - reactInstanceManagerClass.methods.first { it.name == "recreateReactContextInBackground" } - reloadMethod.invoke(reactInstanceManager) - } else { - showError(context, "Error", "Could not get React Instance Manager") - } - } else { - showError(context, "Error", "Application is not a React Application") - } - } catch (e: Exception) { - Log.e("Failed to Reload App: ${e.message}") - showError(context, "Failed to reload", "Error: ${e.message}") - } - } - - private fun showRecoveryMenu(context: Context) { - val options = arrayOf( - if (isSafeModeEnabled(context)) "Disable Safe Mode" else "Enable Safe Mode", - "Refetch Bundle", - "Load Custom Bundle", - "Reload App", - "Clear Cache & Reset" - ) - - val adapter = - object : android.widget.ArrayAdapter(context, android.R.layout.select_dialog_item, options) { - override fun getView( - position: Int, - convertView: android.view.View?, - parent: android.view.ViewGroup - ): android.view.View { - val view = super.getView(position, convertView, parent) - (view as? android.widget.TextView)?.apply { - setTextColor(0xFFFFFFFF.toInt()) - textSize = 14f - setPadding( - (16 * context.resources.displayMetrics.density).toInt(), - (12 * context.resources.displayMetrics.density).toInt(), - (16 * context.resources.displayMetrics.density).toInt(), - (12 * context.resources.displayMetrics.density).toInt() - ) - setTypeface(typeface, android.graphics.Typeface.NORMAL) - } - view.setBackgroundColor(0xFF2D2D2D.toInt()) - return view - } - } - - val dialog = AlertDialog.Builder(context) - .setTitle("GoonXposed Recovery Menu") - .setAdapter(adapter) { _, which -> - handleMenuSelection(context, which) - } - .setNegativeButton("Close", null) - .create() - - dialog.setOnShowListener { - dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.apply { - setTextColor(0xFFBB86FC.toInt()) - setBackgroundColor(0xFF1E1E1E.toInt()) - textSize = 12f - setPadding( - (20 * context.resources.displayMetrics.density).toInt(), - (8 * context.resources.displayMetrics.density).toInt(), - (20 * context.resources.displayMetrics.density).toInt(), - (8 * context.resources.displayMetrics.density).toInt() - ) - } - - dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt())) - - val titleId = context.resources.getIdentifier("alertTitle", "id", "android") - val titleView = dialog.findViewById(titleId) - titleView?.apply { - setTextColor(0xFFBB86FC.toInt()) - textSize = 16f - setTypeface(typeface, android.graphics.Typeface.BOLD) - } - - dialog.listView?.setBackgroundColor(0xFF1E1E1E.toInt()) - dialog.listView?.divider = android.graphics.drawable.ColorDrawable(0xFF444444.toInt()) - dialog.listView?.dividerHeight = 1 - } - dialog.show() - } - - private fun handleMenuSelection(context: Context, index: Int) { - when (index) { - 0 -> toggleSafeMode(context) - 1 -> showConfirmAction( - context, "Refetch Bundle", - "This will download the latest bundle from the official server." - ) { refetchBundle(context) } - - 2 -> showCustomBundleDialog(context) - 3 -> reloadApp() - 4 -> showConfirmAction( - context, "Clear Cache & Reset", - "This will clear all cached bundles and reset to default settings." - ) { clearCacheAndReset(context) } - } - } - - private fun showConfirmAction(context: Context, title: String, message: String, action: () -> Unit) { - val dialog = AlertDialog.Builder(context) - .setTitle(title) - .setMessage(message) - .setPositiveButton("Confirm") { _, _ -> action() } - .setNegativeButton("Cancel", null) - .create() - - dialog.setOnShowListener { - dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.apply { - setTextColor(0xFFFFFFFF.toInt()) - setBackgroundColor(0xFF1E1E1E.toInt()) - textSize = 12f - } - dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.apply { - setTextColor(0xFFBB86FC.toInt()) - setBackgroundColor(0xFF1E1E1E.toInt()) - textSize = 12f - setTypeface(typeface, android.graphics.Typeface.BOLD) - } - - dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt())) - - val titleId = context.resources.getIdentifier("alertTitle", "id", "android") - val titleView = dialog.findViewById(titleId) - titleView?.apply { - setTextColor(0xFFBB86FC.toInt()) - textSize = 16f - } - - val messageId = context.resources.getIdentifier("message", "id", "android") - val messageView = dialog.findViewById(messageId) - messageView?.apply { - setTextColor(0xFFFFFFFF.toInt()) - textSize = 14f - setPadding( - (5 * context.resources.displayMetrics.density).toInt(), - paddingTop, - (5 * context.resources.displayMetrics.density).toInt(), - paddingBottom - ) - } - } - dialog.show() - } - - private fun showCustomBundleDialog(context: Context) { - val filesDir = File(context.filesDir, "pyoncord") - val configFile = File(filesDir, "loader.json") - var currentUrl: String? = null - - if (configFile.exists()) { - try { - val json = JSONObject(configFile.readText()) - val customLoadUrl = json.optJSONObject("customLoadUrl") - if (customLoadUrl != null && customLoadUrl.optBoolean("enabled", false)) { - currentUrl = customLoadUrl.optString("url", "") - } - } catch (_: Exception) { - } - } - - val urlInput = EditText(context).apply { - hint = "http://localhost:4040/shiggycord.js" - setTextColor(0xFFFFFFFF.toInt()) - setBackgroundColor(0xFF2D2D2D.toInt()) - setHintTextColor(0xFF888888.toInt()) - inputType = android.text.InputType.TYPE_TEXT_VARIATION_URI - setText(currentUrl ?: "") - gravity = android.view.Gravity.START - textAlignment = android.view.View.TEXT_ALIGNMENT_VIEW_START - textSize = 14f - setPadding( - (12 * context.resources.displayMetrics.density).toInt(), - (10 * context.resources.displayMetrics.density).toInt(), - (12 * context.resources.displayMetrics.density).toInt(), - (10 * context.resources.displayMetrics.density).toInt() - ) - } - - val container = android.widget.FrameLayout(context) - val params = android.widget.FrameLayout.LayoutParams( - (context.resources.displayMetrics.widthPixels * 0.85).toInt(), - android.widget.FrameLayout.LayoutParams.WRAP_CONTENT - ) - params.leftMargin = (15 * context.resources.displayMetrics.density).toInt() - params.rightMargin = (15 * context.resources.displayMetrics.density).toInt() - urlInput.layoutParams = params - container.addView(urlInput) - - val dialog = AlertDialog.Builder(context) - .setTitle("Custom Bundle URL") - .setView(container) - .setPositiveButton("Save", null) - .setNegativeButton("Cancel", null) - .create() - - dialog.setOnShowListener { - dialog.getButton(AlertDialog.BUTTON_NEGATIVE)?.apply { - setTextColor(0xFFBB86FC.toInt()) - setBackgroundColor(0xFF1E1E1E.toInt()) - textSize = 12f - } - dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.apply { - setTextColor(0xFFBB86FC.toInt()) - setBackgroundColor(0xFF1E1E1E.toInt()) - textSize = 12f - setTypeface(typeface, android.graphics.Typeface.BOLD) - } - - dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt())) - - val titleId = context.resources.getIdentifier("alertTitle", "id", "android") - val titleView = dialog.findViewById(titleId) - titleView?.apply { - setTextColor(0xFFBB86FC.toInt()) - textSize = 16f - } - } - - dialog.show() - - dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener { - try { - val url = urlInput.text?.toString()?.trim() ?: "" - if (url.isNotEmpty()) { - setCustomBundleURL(context, url) - dialog.dismiss() - } else { - disableCustomBundle(context) - dialog.dismiss() - } - } catch (e: Exception) { - Toast.makeText(context, "Failed to set custom bundle: ${e.message}", Toast.LENGTH_LONG).show() - } - } - } - - private fun showError(context: Context, title: String, message: String?) { - val dialog = AlertDialog.Builder(context) - .setTitle(title) - .setMessage(message ?: "An unknown error occurred") - .setPositiveButton("Close", null) - .create() - - dialog.setOnShowListener { - dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.apply { - setTextColor(0xFFBB86FC.toInt()) - setBackgroundColor(0xFF1E1E1E.toInt()) - textSize = 12f - } - - dialog.window?.setBackgroundDrawable(android.graphics.drawable.ColorDrawable(0xFF1E1E1E.toInt())) - - val titleId = context.resources.getIdentifier("alertTitle", "id", "android") - val titleView = dialog.findViewById(titleId) - titleView?.setTextColor(0xFFBB86FC.toInt()) - - val messageId = context.resources.getIdentifier("message", "id", "android") - val messageView = dialog.findViewById(messageId) - messageView?.setTextColor(0xFFFFFFFF.toInt()) - } - dialog.show() - } - - private fun clearCacheAndReset(context: Context) { - scope.launch(Dispatchers.IO) { - try { - val pyoncordDir = getPyoncordDirectory(context) - val bundleFile = File(pyoncordDir, "bundle.js") - val configFile = File(pyoncordDir, "loader.json") - - bundleFile.delete() - - if (configFile.exists()) { - val config = JSONObject(configFile.readText()) - val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject() - customLoadUrl.put("enabled", false) - customLoadUrl.put("url", "http://localhost:4040/shiggycord.js") - config.put("customLoadUrl", customLoadUrl) - configFile.writeText(config.toString()) - } - - withContext(Dispatchers.Main) { - Toast.makeText(context, "Cache cleared successfully", Toast.LENGTH_SHORT).show() - reloadApp() - } - - } catch (e: Exception) { - Log.e("Error clearing cache: ${e.message}") - withContext(Dispatchers.Main) { - Toast.makeText(context, "Failed to clear cache", Toast.LENGTH_SHORT).show() - } - } - } - } - - private fun disableCustomBundle(context: Context) { - try { - val configFile = File(getPyoncordDirectory(context), "loader.json") - val config = if (configFile.exists()) { - JSONObject(configFile.readText()) - } else { - JSONObject().apply { - put("loadReactDevTools", false) - } - } - - val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject() - customLoadUrl.put("enabled", false) - customLoadUrl.put("url", "http://localhost:4040/shiggycord.js") - config.put("customLoadUrl", customLoadUrl) - - configFile.writeText(config.toString()) - Toast.makeText(context, "Custom bundle disabled", Toast.LENGTH_SHORT).show() - reloadApp() - } catch (e: Exception) { - Toast.makeText(context, "Failed to disable custom bundle", Toast.LENGTH_SHORT).show() - } - } - - private fun isSafeModeEnabled(context: Context): Boolean { - return try { - val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS") - if (!settingsFile.exists()) return false - - val json = JSONObject(settingsFile.readText()) - json.optJSONObject("safeMode")?.optBoolean("enabled", false) ?: false - } catch (e: Exception) { - Log.e("Error checking safe mode: ${e.message}") - false - } - } - - private fun toggleSafeMode(context: Context) { - try { - val settingsFile = File(context.filesDir, "vd_mmkv/VENDETTA_SETTINGS") - val themeFile = File(context.filesDir, "vd_mmkv/VENDETTA_THEMES") - - settingsFile.parentFile?.mkdirs() - - val settings = if (settingsFile.exists()) { - JSONObject(settingsFile.readText()) - } else { - JSONObject() - } - - val safeMode = settings.optJSONObject("safeMode") ?: JSONObject() - val currentState = safeMode.optBoolean("enabled", false) - val newState = !currentState - - safeMode.put("enabled", newState) - - if (newState && themeFile.exists()) { - val theme = JSONObject(themeFile.readText()) - val themeId = theme.optString("id") - if (themeId.isNotEmpty()) { - safeMode.put("currentThemeId", themeId) - themeFile.delete() - } - } - - settings.put("safeMode", safeMode) - settingsFile.writeText(settings.toString()) - - Toast.makeText(context, "Safe Mode ${if (newState) "Enabled" else "Disabled"}", Toast.LENGTH_SHORT).show() - - reloadApp() - - } catch (e: Exception) { - Log.e("Error toggling safe mode: ${e.message}") - showError(context, "Failed to toggle safe mode", e.message) - } - } - - private fun refetchBundle(context: Context) { - try { - val pyoncordDir = getPyoncordDirectory(context) - val bundleFile = File(pyoncordDir, "bundle.js") - val backupFile = File(pyoncordDir, "bundle.js.backup") - - if (bundleFile.exists()) { - backupFile.delete() - bundleFile.renameTo(backupFile) - Log.e("Bundle moved to backup") - } - - reloadApp() - - } catch (e: Exception) { - Log.e("Error refetching bundle: ${e.message}") - showError(context, "Failed to refetch bundle", e.message) - } - } - - private fun resetBundle(context: Context) { - try { - val pyoncordDir = getPyoncordDirectory(context) - val bundleFile = File(pyoncordDir, "bundle.js") - val configFile = File(pyoncordDir, "loader_config.json") - - bundleFile.delete() - - if (configFile.exists()) { - val config = JSONObject(configFile.readText()) - config.put("customLoadUrlEnabled", false) - config.put("customLoadUrl", "http://localhost:4040/ShiggyCord.js") - configFile.writeText(config.toString()) - } - - reloadApp() - } catch (e: Exception) { - Log.e("Error resetting bundle: ${e.message}") - showError(context, "Failed to reset bundle", e.message) - } - } - - private fun validateAndLoadBundle(context: Context, urlString: String) { - scope.launch(Dispatchers.IO) { - try { - val url = URL(urlString) - if (url.protocol != "http" && url.protocol != "https") { - withContext(Dispatchers.Main) { - showError(context, "Invalid URL", "URL must start with http:// or https://") - } - return@launch - } - - val connection = url.openConnection() as HttpURLConnection - connection.requestMethod = "GET" - connection.connectTimeout = 10000 - connection.readTimeout = 10000 - - val responseCode = connection.responseCode - if (responseCode != 200) { - withContext(Dispatchers.Main) { - showError(context, "Error", "Server returned error $responseCode") - } - return@launch - } - - val contentType = connection.contentType - if (contentType != null && !contentType.contains("javascript") && !contentType.contains("text")) { - withContext(Dispatchers.Main) { - showError(context, "Invalid Content", "URL must point to a JavaScript file") - } - return@launch - } - - withContext(Dispatchers.Main) { - setCustomBundleURL(context, urlString) - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - showError(context, "Connection Error", "Could not reach URL: ${e.message}") - } - } - } - } - - private fun setCustomBundleURL(context: Context, url: String) { - try { - val pyoncordDir = getPyoncordDirectory(context) - val configFile = File(pyoncordDir, "loader.json") - - val config = if (configFile.exists()) { - JSONObject(configFile.readText()) - } else { - JSONObject().apply { - put("loadReactDevTools", false) - } - } - - val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject() - customLoadUrl.put("enabled", true) - customLoadUrl.put("url", url) - config.put("customLoadUrl", customLoadUrl) - - configFile.writeText(config.toString()) - - File(pyoncordDir, "bundle.js").delete() - - Toast.makeText(context, "Custom bundle set", Toast.LENGTH_SHORT).show() - - reloadApp() - } catch (e: Exception) { - Log.e("Error setting custom bundle URL: ${e.message}") - showError(context, "Failed to save configuration", e.message) - } - } - - private fun getPyoncordDirectory(context: Context): File { - val dir = File(context.filesDir, "pyoncord") - if (!dir.exists()) { - dir.mkdirs() - } - return dir - } - - override fun onContext(context: Context) { - } -} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/PayloadGlobalModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/PayloadGlobalModule.kt deleted file mode 100644 index 03d0601..0000000 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/PayloadGlobalModule.kt +++ /dev/null @@ -1,80 +0,0 @@ -package GoonXposed.xposed.modules - -import android.content.res.AssetManager -import de.robv.android.xposed.XC_MethodHook -import de.robv.android.xposed.XposedBridge -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.BuildConfig -import GoonXposed.xposed.Constants -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils.Companion.JSON -import GoonXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.put -import java.io.File - -// TODO: Remove this redundant module and merge its functionality into AdditionalBridgeMethodsModule if needed -class PayloadGlobalModule(private val modules: List) : Module() { - private companion object { - const val GLOBAL_NAME = "__PYON_LOADER__" - } - - private fun getPayloadString(): String = JSON.encodeToString( - buildJsonObject { - put("loaderName", Constants.LOADER_NAME) - put("loaderVersion", BuildConfig.VERSION_NAME) - @Suppress("DEPRECATION") for (module in modules) module.buildPayload(this) - }) - - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - val catalystInstance = classLoader.safeLoadClass("com.facebook.react.bridge.CatalystInstanceImpl") - val scriptLoader = classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance\$loadJSBundle$1") - ?: classLoader.safeLoadClass("com.facebook.react.runtime.ReactInstance$1") - - val setGlobalVariable: (XC_MethodHook.MethodHookParam, String, String) -> Unit = { param, key, json -> - runCatching { - // Attempt old CatalystInstanceImpl method - catalystInstance!!.method( - "setGlobalVariable", String::class.java, String::class.java - ).invoke(param.thisObject, key, json) - }.onFailure { - // Failed (likely because it's a stub), use a workaround - val preloadsDir = File("${appInfo.dataDir}/${Constants.FILES_DIR}", PRELOADS_DIR) - File(preloadsDir, "rv_globals_$key.js").apply { - writeText("this[${JSON.encodeToString(key)}]=$json") - - XposedBridge.invokeOriginalMethod( - scriptLoader!!.method( - "loadScriptFromFile", - String::class.java, - String::class.java, - Boolean::class.javaPrimitiveType - ), param.thisObject, arrayOf(absolutePath, absolutePath, param.args[2]) - ) - - delete() - } - } - } - - val hook = MethodHookBuilder().run { - before { - setGlobalVariable(param, GLOBAL_NAME, getPayloadString()) - } - - build() - } - - listOf(catalystInstance, scriptLoader).forEach { if (it != null) hook(it, hook) } - } - - private fun hook(instance: Class<*>, hook: XC_MethodHook) { - instance.method( - "loadScriptFromAssets", AssetManager::class.java, String::class.java, Boolean::class.javaPrimitiveType, - ).hook(hook) - - instance.method( - "loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType - ).hook(hook) - } -} diff --git a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt b/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt deleted file mode 100644 index d8cc257..0000000 --- a/app/src/main/kotlin/io/github/revenge/xposed/modules/UpdaterModule.kt +++ /dev/null @@ -1,152 +0,0 @@ -package GoonXposed.xposed.modules - -import android.app.Activity -import android.app.AlertDialog -import android.content.Context -import android.util.AtomicFile -import android.widget.Toast -import androidx.core.util.writeBytes -import de.robv.android.xposed.callbacks.XC_LoadPackage -import GoonXposed.xposed.Constants -import GoonXposed.xposed.Module -import GoonXposed.xposed.Utils -import GoonXposed.xposed.Utils.Companion.JSON -import GoonXposed.xposed.Utils.Companion.reloadApp -import GoonXposed.xposed.Utils.Log -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import kotlinx.coroutines.* -import kotlinx.serialization.Serializable -import java.io.File -import java.lang.ref.WeakReference - -@Serializable -data class CustomLoadUrl( - val enabled: Boolean = false, val url: String = "" -) - -@Serializable -data class LoaderConfig( - val customLoadUrl: CustomLoadUrl = CustomLoadUrl() -) - -/** - * Module that updates the JS bundle by downloading it from a remote URL. - * - * Shows dialogs when failed allowing retry. - */ -object UpdaterModule : Module() { - private lateinit var config: LoaderConfig - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - private var lastActivity: WeakReference? = null - - private lateinit var cacheDir: File - private lateinit var bundle: File - private lateinit var etag: File - - private const val TIMEOUT_CACHED = 5000L - private const val TIMEOUT = 10000L - private const val ETAG_FILE = "etag.txt" - private const val CONFIG_FILE = "loader.json" - - private const val DEFAULT_BUNDLE_URL = - "https://github.com/OTKUSteyler/GoonManager/releases/latest/download/gooncord.js" - - override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) { - cacheDir = File(appInfo.dataDir, Constants.CACHE_DIR).apply { mkdirs() } - val filesDir = File(appInfo.dataDir, Constants.FILES_DIR).apply { mkdirs() } - - bundle = File(cacheDir, Constants.MAIN_SCRIPT_FILE) - etag = File(cacheDir, ETAG_FILE) - - val configFile = File(filesDir, CONFIG_FILE) - - config = runCatching { - if (configFile.exists()) { - JSON.decodeFromString(configFile.readText()) - } else LoaderConfig() - }.getOrDefault(LoaderConfig()) - } - - fun downloadScript(activity: Activity? = null): Job = scope.launch { - try { - HttpClient(CIO) { - expectSuccess = false - install(UserAgent) { agent = Constants.USER_AGENT } - install(HttpRedirect) {} - }.use { client -> - val url = config.customLoadUrl.takeIf { it.enabled }?.url ?: DEFAULT_BUNDLE_URL - Log.i("Fetching JS bundle from: $url") - - val response: HttpResponse = client.get(url) { - headers { - if (etag.exists() && bundle.exists()) { - append(HttpHeaders.IfNoneMatch, etag.readText()) - } - } - - // Retries don't need timeout - if (activity == null) { - timeout { - requestTimeoutMillis = if (!bundle.exists()) TIMEOUT else TIMEOUT_CACHED - } - } - } - - when (response.status) { - HttpStatusCode.OK -> { - val bytes: ByteArray = response.body() - AtomicFile(bundle).writeBytes(bytes) - - val newTag = response.headers[HttpHeaders.ETag] - if (!newTag.isNullOrEmpty()) etag.writeText(newTag) else etag.delete() - - Log.i("Bundle updated (${bytes.size} bytes)") - - // This is a retry, so we show a dialog - if (activity != null) { - withContext(Dispatchers.Main) { - AlertDialog.Builder(activity).setTitle("GoonCord Update Successful") - .setMessage("A reload is required for changes to take effect.") - .setPositiveButton("Reload") { dialog, _ -> - reloadApp() - dialog.dismiss() - }.setCancelable(false).show() - } - } - } - - HttpStatusCode.NotModified -> { - Log.i("Server responded with 304, no changes") - } - - else -> { - throw ResponseException(response, "Received status: ${response.status}") - } - } - } - } catch (e: Throwable) { - Log.e("Failed to download script", e) - showErrorDialog(e) - } - } - - override fun onActivity(activity: Activity) { - lastActivity = WeakReference(activity) - } - - fun showErrorDialog(e: Throwable) { - val activity = lastActivity?.get() ?: return - } - - fun resetLoaderConfig(context: Context) { - val filesDir = File(context.dataDir, Constants.FILES_DIR) - val config = File(filesDir, CONFIG_FILE) - if (config.exists()) config.delete() - } -}