Add files via upload

This commit is contained in:
OTKUSteyler 2025-11-21 23:54:09 +01:00 • committed by GitHub
parent aa0970913c
commit 007a6f8a59
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 980 additions and 576 deletions

View file

@ -0,0 +1,270 @@
package ShiggyXposed.xposed.modules
import android.content.Context
import android.util.AtomicFile
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import ShiggyXposed.xposed.modules.bridge.BridgeModule
import java.io.*
/**
* A module for caching versioned modules finds and blacklist, and assets mappings.
*
* See [ModulesCache] and [AssetsCache] for the cache structure.
*
* ## Methods
*
* ### Modules cache
*
* - `Shiggy.caches.modules.read() : { blacklist: number[], finds: { [filter: string]: { [id: string]: number } | null }, version: number } | null`
* - Reads the modules cache. Returns `null` if the cache file does not exist or is invalid.
*
* - `Shiggy.caches.modules.write(blacklist: number[], finds: { [filter: string]: { [id: string]: number } | null }) : void`
* - Writes the modules cache.
*
* ### Assets cache
*
* - `Shiggy.caches.assets.read() : { data: { [name: string]: { [type: string]: number } }, version: number } | null`
* - Reads the assets cache. Returns `null` if the cache file does not exist or is invalid.
*
* - `Shiggy.caches.assets.write(data: { [name: string]: { [type: string]: number } }) : void`
* - Writes the assets cache.
*
* The caches are versioned and tied to the app version code. When the app is updated,
* the caches are invalidated and new cache files are created.
*/
object CacheModule : Module() {
private const val CACHE_DIR = "revenge"
private const val MODULES_CACHE_PREFIX = "modules"
private const val ASSETS_CACHE_PREFIX = "assets"
private lateinit var modulesCache: ModulesCache
private lateinit var assetsCache: AssetsCache
override fun onContext(context: Context) = with(context) {
val revengeCacheDir = File(cacheDir, CACHE_DIR).apply { asDir() }
val (_, _, _, versionCode) = getAppInfo()
val modulesCacheFile = File(
revengeCacheDir, "$MODULES_CACHE_PREFIX.$versionCode"
).apply { asFile() }
val assetsCacheFile = File(
revengeCacheDir, "$ASSETS_CACHE_PREFIX.$versionCode"
).apply { asFile() }
BridgeModule.registerMethod("revenge.caches.modules.read") {
if (::modulesCache.isInitialized) modulesCache.toMap() else ModulesCache.loadFromFileOrNull(modulesCacheFile)
?.let {
modulesCache = it
it.toMap()
}
}
BridgeModule.registerMethod("revenge.caches.modules.write") {
val (blacklist, finds) = it
@Suppress("UNCHECKED_CAST")
modulesCache = ModulesCache(
blacklist as ArrayList<Double>, finds as HashMap<String, HashMap<String, Double>?>
).apply { saveToFile(modulesCacheFile) }
Log.i("Modules cache saved: ${modulesCacheFile.absolutePath} (blacklisted: ${blacklist.size}, finds: ${finds.size})")
}
BridgeModule.registerMethod("revenge.caches.assets.read") {
if (::assetsCache.isInitialized) assetsCache.toMap() else AssetsCache.loadFromFileOrNull(assetsCacheFile)
?.let {
assetsCache = it
it.toMap()
}
}
BridgeModule.registerMethod("revenge.caches.assets.write") {
val (data) = it
@Suppress("UNCHECKED_CAST")
assetsCache =
AssetsCache(data as HashMap<String, HashMap<String, Double>>).apply { saveToFile(assetsCacheFile) }
Log.i("Assets cache saved: ${assetsCacheFile.absolutePath} (count: ${data.size})")
}
}
}
private class CacheVersionMismatchException(expected: Int, actual: Int) :
Throwable("Expected cache version: $expected, but got version: $actual")
data class ModulesCache(
val blacklist: List<Double>, val finds: Map<String, Map<String, Double>?>
) {
fun saveToFile(file: File) {
val atomic = AtomicFile(file)
var fos: FileOutputStream? = null
try {
fos = atomic.startWrite()
val out = DataOutputStream(BufferedOutputStream(fos))
out.writeInt(VERSION)
// Write blacklist
out.writeInt(blacklist.size)
for (num in blacklist) out.writeInt(num.toInt())
// Write finds
out.writeInt(finds.size)
for ((filter, matches) in finds) {
out.writeUTF(filter)
if (matches == null) {
out.writeBoolean(false) // null marker
} else {
out.writeBoolean(true) // not null
out.writeInt(matches.size)
for ((id, flag) in matches) {
out.writeInt(id.toInt())
out.writeInt(flag.toInt())
}
}
}
out.flush()
atomic.finishWrite(fos)
} catch (t: Throwable) {
if (fos != null) atomic.failWrite(fos)
throw t
}
}
fun toMap(): Map<String, Any> {
val findsMap = finds.mapValues { (_, matches) -> matches?.map { (k, v) -> k to v }?.toMap() }
return mapOf("blacklist" to blacklist, "finds" to findsMap, "version" to VERSION)
}
companion object {
const val VERSION = 3
fun loadFromFileOrNull(file: File): ModulesCache? {
if (!file.exists() || file.length() <= 0L) return null
val atomic = AtomicFile(file)
try {
DataInputStream(BufferedInputStream(atomic.openRead())).use { input ->
val version = input.readInt()
if (version != VERSION) throw CacheVersionMismatchException(VERSION, version)
val blacklistSize = input.readInt()
val blacklist = MutableList(blacklistSize) { input.readInt().toDouble() }
val findsSize = input.readInt()
val finds = mutableMapOf<String, Map<String, Double>?>()
repeat(findsSize) {
val filter = input.readUTF()
val hasMatches = input.readBoolean()
if (!hasMatches) {
finds[filter] = null
} else {
val mapSize = input.readInt()
val matches = mutableMapOf<String, Double>()
repeat(mapSize) {
val id = input.readInt().toString()
val flag = input.readInt().toDouble()
matches[id] = flag
}
finds[filter] = matches
}
}
return ModulesCache(blacklist, finds)
}
} catch (e: CacheVersionMismatchException) {
Log.i("Modules cache version mismatch: ${e.message}")
} catch (e: EOFException) {
Log.e("Modules cache corrupt: ${e.message}")
} catch (e: IOException) {
Log.e("Failed to read modules cache: ${e.message}")
}
runCatching { file.delete() }
return null
}
}
}
data class AssetsCache(
val data: Map<String, Map<String, Double>>
) {
fun saveToFile(file: File) {
val atomic = AtomicFile(file)
var fos: FileOutputStream? = null
try {
fos = atomic.startWrite()
val out = DataOutputStream(BufferedOutputStream(fos))
out.writeInt(VERSION)
out.writeInt(data.size)
for ((name, mappings) in data) {
out.writeUTF(name)
out.writeInt(mappings.size)
for ((type, id) in mappings) {
out.writeUTF(type)
out.writeInt(id.toInt())
}
}
out.flush()
atomic.finishWrite(fos)
} catch (t: Throwable) {
if (fos != null) atomic.failWrite(fos)
throw t
}
}
fun toMap(): Map<String, Any> {
return mapOf(
"data" to data.mapValues { (_, mappings) ->
mappings.map { (type, id) -> type to id }.toMap()
}, "version" to VERSION
)
}
companion object {
const val VERSION = 2
fun loadFromFileOrNull(file: File): AssetsCache? {
if (!file.exists() || file.length() <= 0L) return null
val atomic = AtomicFile(file)
try {
DataInputStream(BufferedInputStream(atomic.openRead())).use { input ->
val version = input.readInt()
if (version != VERSION) throw CacheVersionMismatchException(VERSION, version)
val dataSize = input.readInt()
val data = mutableMapOf<String, Map<String, Double>>()
repeat(dataSize) {
val name = input.readUTF()
val mappingsSize = input.readInt()
val mappings = mutableMapOf<String, Double>()
repeat(mappingsSize) {
val type = input.readUTF()
val id = input.readInt().toDouble()
mappings[type] = id
}
data[name] = mappings
}
return AssetsCache(data)
}
} catch (e: CacheVersionMismatchException) {
Log.i("Assets cache version mismatch: ${e.message}")
} catch (e: EOFException) {
Log.e("Assets cache corrupt: ${e.message}")
} catch (e: IOException) {
Log.e("Failed to read assets cache: ${e.message}")
}
runCatching { file.delete() }
return null
}
}
}

View file

@ -1,21 +1,23 @@
package GoonXposed.xposed.modules
package ShiggyXposed.xposed.modules
import GoonXposed.xposed.Constants.Companion.TARGET_PACKAGE
import GoonXposed.xposed.Module
import android.content.res.Resources
import de.robv.android.xposed.callbacks.XC_LoadPackage
import ShiggyXposed.xposed.Constants.Companion.TARGET_PACKAGE
import ShiggyXposed.xposed.Module
/** Hooks [Resources.getIdentifier] to fix resource package name mismatch. */
class 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 } }
/**
* 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
}
}
}
}
}

View file

@ -1,76 +1,65 @@
package GoonXposed.xposed.modules
package ShiggyXposed.xposed.modules
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import GoonXposed.xposed.modules.HookScriptLoaderModule.Companion.PRELOADS_DIR
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 ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.HookStateHolder
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import ShiggyXposed.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
import kotlinx.coroutines.runBlocking
/**
* 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.
* 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://revenge.bundle` from the module's assets.
* 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.
*/
class HookScriptLoaderModule : Module() {
object HookScriptLoaderModule : Module() {
private lateinit var preloadsDir: File
private lateinit var mainScript: File
private lateinit var modulePath: String
private lateinit var resources: XModuleResources
companion object {
const val PRELOADS_DIR = "preloads"
}
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() }
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() }
preloadsDir = File(filesDir, PRELOADS_DIR).apply { asDir() }
mainScript = File(cacheDir, Constants.MAIN_SCRIPT_FILE).apply { asFile() }
listOf(
"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) }
}
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<*>) {
val loadScriptFromAssets =
instance.method(
"loadScriptFromAssets",
AssetManager::class.java,
String::class.java,
Boolean::class.javaPrimitiveType
)
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
)
val loadScriptFromFile = instance.method(
"loadScriptFromFile", String::class.java, String::class.java, Boolean::class.javaPrimitiveType
)
loadScriptFromAssets.hook {
before {
@ -85,45 +74,44 @@ class HookScriptLoaderModule : Module() {
runCustomScripts(loadScriptFromFile, loadScriptFromAssets)
}
}
}.onFailure {
Log.e("Failed to hook script loading methods in ${instance.name}:", it)
}
private fun HookScope.runCustomScripts(
loadScriptFromFile: Method,
loadScriptFromAssets: Method
) {
private fun HookScope.runCustomScripts(loadScriptFromFile: Method, loadScriptFromAssets: Method) {
Log.i("Running custom scripts...")
// TODO: Is there a better way to do this?
runBlocking { UpdaterModule.job?.join() }
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)
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)
if (!::resources.isInitialized) resources = XModuleResources.createInstance(modulePath, null)
XposedBridge.invokeOriginalMethod(
loadScriptFromAssets,
thisObject,
arrayOf(resources.assets, "assets://bundle.js", loadSynchronously)
loadScriptFromAssets,
thisObject,
arrayOf(resources.assets, "assets://Shiggy.bundle", loadSynchronously)
)
}
} catch (e: Throwable) {

View file

@ -1,4 +1,4 @@
package GoonXposed.xposed.modules
package ShiggyXposed.xposed.modules
import android.app.AlertDialog
import android.content.Context
@ -8,10 +8,10 @@ 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 ShiggyXposed.xposed.BuildConfig
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils
import ShiggyXposed.xposed.Utils.Log
import de.robv.android.xposed.XposedHelpers
import kotlinx.coroutines.*
import org.json.JSONArray
@ -19,9 +19,9 @@ import org.json.JSONObject
import java.io.File
import java.net.HttpURLConnection
import java.net.URL
import GoonXposed.xposed.Utils.Companion.reloadApp
import ShiggyXposed.xposed.Utils.Companion.reloadApp
class LogBoxModule : Module() {
object LogBoxModule : Module() {
lateinit var packageParam: XC_LoadPackage.LoadPackageParam
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
@ -143,7 +143,7 @@ class LogBoxModule : Module() {
}
val dialog = AlertDialog.Builder(context)
.setTitle("GoonXposed Recovery Menu")
.setTitle("ShiggyXposed Recovery Menu")
.setAdapter(adapter) { _, which ->
handleMenuSelection(context, which)
}
@ -260,7 +260,7 @@ class LogBoxModule : Module() {
}
val urlInput = EditText(context).apply {
hint = "http://localhost:4040/gooncord.js"
hint = "http://localhost:4040/shiggycord.js"
setTextColor(0xFFFFFFFF.toInt())
setBackgroundColor(0xFF2D2D2D.toInt())
setHintTextColor(0xFF888888.toInt())
@ -375,7 +375,7 @@ class LogBoxModule : Module() {
val config = JSONObject(configFile.readText())
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
customLoadUrl.put("enabled", false)
customLoadUrl.put("url", "http://localhost:4040/gooncord.js")
customLoadUrl.put("url", "http://localhost:4040/shiggycord.js")
config.put("customLoadUrl", customLoadUrl)
configFile.writeText(config.toString())
}
@ -407,7 +407,7 @@ class LogBoxModule : Module() {
val customLoadUrl = config.optJSONObject("customLoadUrl") ?: JSONObject()
customLoadUrl.put("enabled", false)
customLoadUrl.put("url", "http://localhost:4040/gooncord.js")
customLoadUrl.put("url", "http://localhost:4040/shiggycord.js")
config.put("customLoadUrl", customLoadUrl)
configFile.writeText(config.toString())
@ -503,7 +503,7 @@ class LogBoxModule : Module() {
if (configFile.exists()) {
val config = JSONObject(configFile.readText())
config.put("customLoadUrlEnabled", false)
config.put("customLoadUrl", "http://localhost:4040/GoonCord.js")
config.put("customLoadUrl", "http://localhost:4040/ShiggyCord.js")
configFile.writeText(config.toString())
}

View file

@ -4,11 +4,11 @@ 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 ShiggyXposed.xposed.BuildConfig
import ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Companion.JSON
import ShiggyXposed.xposed.modules.HookScriptLoaderModule.PRELOADS_DIR
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.io.File

View file

@ -0,0 +1,124 @@
package ShiggyXposed.xposed.modules
import android.content.Context
import android.util.AtomicFile
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import ShiggyXposed.xposed.modules.bridge.BridgeModule
import java.io.*
/**
* Manages the states of plugins, allowing reading and writing of plugin states to a file.
*
* ## Methods
*
* - `Shiggy.plugins.states.read(): { flags: { [pluginId: string]: number } }`
* - Reads the current plugin states from the file and returns them as a map.
*
* - `Shiggy.plugins.states.write(flags: { [pluginId: string]: number }): void`
* - Writes the provided plugin states to the file.
*/
class PluginsModule : Module() {
private companion object {
const val DATA_DIR = "Shiggy/plugins"
const val STATES_FILE = "states"
}
private lateinit var states: PluginStates
override fun onContext(context: Context) = with(context) {
val dataDir = File(filesDir, DATA_DIR)
val statesFile = File(
dataDir, STATES_FILE
).apply { asFile() }
BridgeModule.registerMethod("Shiggy.plugins.states.read") {
if (::states.isInitialized) states.toMap() else
PluginStates.loadFromFileOrNull(statesFile)?.let {
states = it
it.toMap()
}
}
BridgeModule.registerMethod("Shiggy.plugins.states.write") {
val (flags) = it
@Suppress("UNCHECKED_CAST")
states = PluginStates(
flags as Map<String, Double>
).apply { saveToFile(statesFile) }
Log.i("Plugin states saved: ${statesFile.absolutePath}")
}
}
}
private class UnsupportedPluginStatesVersionException(version: Int) :
Throwable("Unsupported plugin states version: $version")
data class PluginStates(
val flags: Map<String, Double>
) {
fun saveToFile(file: File) {
val atomic = AtomicFile(file)
var fos: FileOutputStream? = null
try {
fos = atomic.startWrite()
val out = DataOutputStream(BufferedOutputStream(fos))
out.writeInt(CURRENT_VERSION)
// Write flags
out.writeInt(flags.size)
for ((id, flags) in flags) {
out.writeUTF(id)
out.writeInt(flags.toInt())
}
out.flush()
atomic.finishWrite(fos)
} catch (t: Throwable) {
if (fos != null) atomic.failWrite(fos)
throw t
}
}
fun toMap(): Map<String, Any> = mapOf("flags" to flags)
companion object {
const val CURRENT_VERSION = 1
fun loadFromFileOrNull(file: File): PluginStates? {
if (!file.exists() || file.length() <= 0L) return null
val atomic = AtomicFile(file)
try {
DataInputStream(BufferedInputStream(atomic.openRead())).use { input ->
when (val version = input.readInt()) {
1 -> return loadV1FromFileOrNull(input)
else -> throw UnsupportedPluginStatesVersionException(version)
}
}
} catch (e: UnsupportedPluginStatesVersionException) {
Log.i(e.message!!)
} catch (e: EOFException) {
Log.e("Plugin states corrupt: ${e.message}")
} catch (e: IOException) {
Log.e("Failed to read plugin states: ${e.message}")
}
runCatching { file.delete() }
return null
}
private fun loadV1FromFileOrNull(input: DataInputStream): PluginStates {
val flagsSize = input.readInt()
val flags = mutableMapOf<String, Double>()
repeat(flagsSize) {
flags[input.readUTF()] = input.readInt().toDouble()
}
return PluginStates(flags)
}
}
}

View file

@ -1,13 +1,18 @@
package GoonXposed.xposed.modules
package ShiggyXposed.xposed.modules
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Utils.Log
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 ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils
import ShiggyXposed.xposed.Utils.Companion.JSON
import ShiggyXposed.xposed.Utils.Companion.reloadApp
import ShiggyXposed.xposed.Utils.Log
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
@ -15,134 +20,133 @@ import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import java.io.File
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 CustomLoadUrl(
val enabled: Boolean = false, val url: String = ""
)
@Serializable data class LoaderConfig(val customLoadUrl: CustomLoadUrl = CustomLoadUrl())
@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.
*/
class UpdaterModule : Module() {
object UpdaterModule : Module() {
private lateinit var config: LoaderConfig
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var error: Throwable? = null
private var lastActivity: WeakReference<Activity>? = null
private lateinit var cacheDir: File
private lateinit var bundle: File
private lateinit var etag: File
companion object {
var job: Job? = null
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 TIMEOUT_CACHED = 5000L
private const val TIMEOUT = 50000L
private const val ETAG_FILE = "etag.txt"
private const val CONFIG_FILE = "loader.json"
private const val DEFAULT_BUNDLE_URL =
"https://github.com/kmmiio99o/ShiggyCord/releases/latest/download/shiggycord.js"
private const val DEFAULT_BUNDLE_URL =
"https://github.com/OTKUSteyler/GoonCord/releases/download/1.1.5/goooncord.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<LoaderConfig>(configFile.readText())
} else LoaderConfig()
}.getOrDefault(LoaderConfig())
}
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() }
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")
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<LoaderConfig>(configFile.readText())
} else LoaderConfig()
}
.getOrDefault(LoaderConfig())
downloadScript()
}
private fun downloadScript(activity: Activity? = null) {
job =
scope.launch {
try {
HttpClient(CIO) {
expectSuccess = false
install(UserAgent) { agent = Constants.USER_AGENT }
install(HttpRedirect) {}
val response: HttpResponse = client.get(url) {
headers {
if (etag.exists() && bundle.exists()) {
append(HttpHeaders.IfNoneMatch, etag.readText())
}
.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) {
var timeoutMs = TIMEOUT_CACHED
if (!bundle.exists()) {
timeoutMs = TIMEOUT
}
timeout { requestTimeoutMillis = timeoutMs }
}
}
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) {}
}
}
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)
error = e
// 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("ShiggyCord 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) {
error ?: return
error = null
downloadScript(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()
}
}

View file

@ -1,9 +1,5 @@
package GoonXposed.xposed.modules.appearance
package ShiggyXposed.xposed.modules.appearance
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import GoonXposed.xposed.Utils.Log
import android.content.res.AssetManager
import android.graphics.Typeface
import android.graphics.Typeface.CustomFallbackBuilder
@ -13,6 +9,10 @@ import android.os.Build
import de.robv.android.xposed.XC_MethodReplacement
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage
import ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Companion.JSON
import ShiggyXposed.xposed.Utils.Log
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
@ -20,128 +20,106 @@ import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import java.io.File
import java.io.IOException
import kotlinx.coroutines.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.put
import java.io.File
import java.io.IOException
@Serializable
data class FontDefinition(
val name: String? = null,
val description: String? = null,
val spec: Int? = null,
val main: Map<String, String>,
val name: String? = null,
val description: String? = null,
val spec: Int? = null,
val main: Map<String, String>,
)
class FontsModule : Module() {
private companion object {
val EXTENSIONS = arrayOf("", "_bold", "_italic", "_bold_italic")
val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
const val FONTS_ASSET_PATH = "fonts/"
}
object FontsModule : Module() {
private val EXTENSIONS = arrayOf("", "_bold", "_italic", "_bold_italic")
private val FILE_EXTENSIONS = arrayOf(".ttf", ".otf")
private const val FONTS_ASSET_PATH = "fonts/"
private lateinit var fontsDir: File
private lateinit var fontsDownloadsDir: File
private var fontsAbsPath: String? = null
override fun buildPayload(builder: JsonObjectBuilder) {
builder.apply { put("fontPatch", 2) }
builder.apply {
put("fontPatch", 2)
}
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) =
with(packageParam) {
XposedHelpers.findAndHookMethod(
"com.facebook.react.views.text.ReactFontManager\$Companion",
classLoader,
"createAssetTypeface",
String::class.java,
Int::class.java,
"android.content.res.AssetManager",
object : XC_MethodReplacement() {
override fun replaceHookedMethod(param: MethodHookParam): Typeface? {
val fontFamilyName: String = param.args[0].toString()
val style: Int = param.args[1] as Int
val assetManager: AssetManager = param.args[2] as AssetManager
return createAssetTypeface(fontFamilyName, style, assetManager)
}
}
)
val fontDefFile =
File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json").apply {
asFile()
}
if (!fontDefFile.exists()) return@with
val fontDef =
try {
JSON.decodeFromString<FontDefinition>(fontDefFile.readText())
} catch (_: Throwable) {
return@with
}
fontsDownloadsDir =
File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").apply {
asDir()
}
fontsDir = File(fontsDownloadsDir, fontDef.name!!).apply { asDir() }
fontsAbsPath = fontsDir.absolutePath + "/"
fontsDir.listFiles()?.forEach { file ->
val fileName = file.name
if (!fileName.startsWith(".")) {
val fontName = fileName.split('.')[0]
if (fontDef.main.keys.none { it == fontName }) {
Log.i("Deleting font file: $fileName")
file.delete()
}
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
XposedHelpers.findAndHookMethod(
"com.facebook.react.views.text.ReactFontManager\$Companion",
classLoader,
"createAssetTypeface",
String::class.java,
Int::class.java,
"android.content.res.AssetManager",
object : XC_MethodReplacement() {
override fun replaceHookedMethod(param: MethodHookParam): Typeface? {
val fontFamilyName: String = param.args[0].toString()
val style: Int = param.args[1] as Int
val assetManager: AssetManager = param.args[2] as AssetManager
return createAssetTypeface(fontFamilyName, style, assetManager)
}
})
// These files should be downloaded by the JS side, but oh well
CoroutineScope(Dispatchers.IO).launch {
fontDef.main
.keys
.map { name ->
async {
val url = fontDef.main.getValue(name)
try {
Log.i("Downloading $name from $url")
val file =
File(
fontsDir,
"$name${FILE_EXTENSIONS.first { url.endsWith(it) }}"
)
.apply { asFile() }
if (file.exists()) return@async
val fontDefFile = File(appInfo.dataDir, "${Constants.FILES_DIR}/fonts.json").apply { asFile() }
if (!fontDefFile.exists()) return@with
val client =
HttpClient(CIO) {
install(UserAgent) {
agent = Constants.USER_AGENT
}
}
val fontDef = try {
JSON.decodeFromString<FontDefinition>(fontDefFile.readText())
} catch (_: Throwable) {
return@with
}
val response: HttpResponse = client.get(url)
if (response.status == HttpStatusCode.OK)
file.writeBytes(response.body())
fontsDownloadsDir = File(appInfo.dataDir, "${Constants.FILES_DIR}/downloads/fonts").apply { asDir() }
fontsDir = File(fontsDownloadsDir, fontDef.name!!).apply { asDir() }
fontsAbsPath = fontsDir.absolutePath + "/"
return@async
} catch (e: Throwable) {
Log.e("Failed to download fonts ($name from $url)", e)
}
}
}
.awaitAll()
fontsDir.listFiles()?.forEach { file ->
val fileName = file.name
if (!fileName.startsWith(".")) {
val fontName = fileName.split('.')[0]
if (fontDef.main.keys.none { it == fontName }) {
Log.i("Deleting font file: $fileName")
file.delete()
}
}
}
// These files should be downloaded by the JS side, but oh well
CoroutineScope(Dispatchers.IO).launch {
fontDef.main.keys.map { name ->
async {
val url = fontDef.main.getValue(name)
try {
Log.i("Downloading $name from $url")
val file =
File(fontsDir, "$name${FILE_EXTENSIONS.first { url.endsWith(it) }}").apply { asFile() }
if (file.exists()) return@async
val client = HttpClient(CIO) {
install(UserAgent) { agent = Constants.USER_AGENT }
}
val response: HttpResponse = client.get(url)
if (response.status == HttpStatusCode.OK) file.writeBytes(response.body())
return@async
} catch (e: Throwable) {
Log.e("Failed to download fonts ($name from $url)", e)
}
}
}.awaitAll()
}
}
private fun createAssetTypefaceWithFallbacks(
fontFamilyNames: Array<String>,
style: Int,
assetManager: AssetManager
fontFamilyNames: Array<String>, style: Int, assetManager: AssetManager
): Typeface? {
val fontFamilies: MutableList<FontFamily> = ArrayList()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@ -151,27 +129,22 @@ class FontsModule : Module() {
try {
for (fileExtension in FILE_EXTENSIONS) {
val (customName, refName) = fontFamilyName.split(":")
val file =
File(fontsDownloadsDir, "$customName/$refName.$fileExtension")
.apply { asFile() }
val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() }
val font = Font.Builder(file).build()
val family = FontFamily.Builder(font).build()
fontFamilies.add(family)
}
} catch (_: Throwable) {}
} catch (_: Throwable) {
}
for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) {
for (fileExtension in FILE_EXTENSIONS) {
val fileName =
StringBuilder()
.append(fontRootPath)
.append(fontFamilyName)
.append(fileExtension)
.toString()
StringBuilder().append(fontRootPath).append(fontFamilyName).append(fileExtension).toString()
try {
val builder =
if (fileName[0] == '/') Font.Builder(File(fileName))
else Font.Builder(assetManager, fileName)
val builder = if (fileName[0] == '/') Font.Builder(File(fileName)) else Font.Builder(
assetManager, fileName
)
val font = builder.build()
val family = FontFamily.Builder(font).build()
fontFamilies.add(family)
@ -187,8 +160,7 @@ class FontsModule : Module() {
}
// If there's some problem constructing fonts, fall back to the default behavior.
if (fontFamilies.isEmpty())
return createAssetTypeface(fontFamilyNames[0], style, assetManager)
if (fontFamilies.isEmpty()) return createAssetTypeface(fontFamilyNames[0], style, assetManager)
val fallbackBuilder = CustomFallbackBuilder(fontFamilies[0])
for (i in 1 until fontFamilies.size) {
@ -200,34 +172,25 @@ class FontsModule : Module() {
}
private fun createAssetTypeface(
fontFamilyName: String,
style: Int,
assetManager: AssetManager
fontFamilyName: String, style: Int, assetManager: AssetManager
): Typeface? {
// This logic attempts to safely check if the frontend code is attempting to use
// fallback fonts, and if it is, to use the fallback typeface creation logic.
var fontFamilyName: String = fontFamilyName
val fontFamilyNames =
fontFamilyName.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val fontFamilyNames = fontFamilyName.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
for (i in fontFamilyNames.indices) {
fontFamilyNames[i] = fontFamilyNames[i].trim()
}
// If there are multiple font family names:
// For newer versions of Android, construct a Typeface with fallbacks
// For older versions of Android, ignore all the fallbacks and just use the first font
// family
// For older versions of Android, ignore all the fallbacks and just use the first font family
if (fontFamilyNames.size > 1) {
fontFamilyName =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return createAssetTypefaceWithFallbacks(
fontFamilyNames,
style,
assetManager
)
} else {
fontFamilyNames[0]
}
fontFamilyName = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return createAssetTypefaceWithFallbacks(fontFamilyNames, style, assetManager)
} else {
fontFamilyNames[0]
}
}
val extension = EXTENSIONS[style]
@ -235,26 +198,20 @@ class FontsModule : Module() {
try {
for (fileExtension in FILE_EXTENSIONS) {
val (customName, refName) = fontFamilyName.split(":")
val file =
File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply {
asFile()
}
val file = File(fontsDownloadsDir, "$customName/$refName.$fileExtension").apply { asFile() }
if (!file.exists()) throw Exception()
return Typeface.createFromFile(file.absolutePath)
}
} catch (_: Throwable) {}
} catch (_: Throwable) {
}
// Lastly, after all those checks above, this is the original RN logic for
// getting the typeface.
for (fontRootPath in arrayOf(fontsAbsPath, FONTS_ASSET_PATH).filterNotNull()) {
for (fileExtension in FILE_EXTENSIONS) {
val fileName =
StringBuilder()
.append(fontRootPath)
.append(fontFamilyName)
.append(extension)
.append(fileExtension)
.toString()
StringBuilder().append(fontRootPath).append(fontFamilyName).append(extension).append(fileExtension)
.toString()
return try {
if (fileName[0] == '/') Typeface.createFromFile(fileName)

View file

@ -1,45 +1,43 @@
package GoonXposed.xposed.modules.appearance
package ShiggyXposed.xposed.modules.appearance
import GoonXposed.xposed.Module
import android.R.color
import android.app.AndroidAppHelper
import android.content.Context
import android.os.Build
import androidx.core.content.ContextCompat
import ShiggyXposed.xposed.Module
import kotlinx.serialization.json.*
import java.lang.ref.WeakReference
class SysColorsModule : Module() {
private lateinit var context: Context
object SysColorsModule : Module() {
private lateinit var context: WeakReference<Context>
private fun isSupported() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
override fun buildPayload(builder: JsonObjectBuilder) {
context = AndroidAppHelper.currentApplication()
context = WeakReference(AndroidAppHelper.currentApplication())
val accents = arrayOf("accent1", "accent2", "accent3", "neutral1", "neutral2")
val shades = arrayOf(0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000)
builder.apply {
put("isSysColorsSupported", isSupported())
if (isSupported())
putJsonObject("sysColors") {
for (accent in accents) putJsonArray(accent) {
for (shade in shades) {
val colorName = "system_" + accent + "_" + shade
if (isSupported()) putJsonObject("sysColors") {
for (accent in accents) putJsonArray(accent) {
for (shade in shades) {
val colorName = "system_" + accent + "_" + shade
val colorResourceId =
runCatching {
color::class.java.getField(colorName).getInt(null)
}
.getOrElse { 0 }
val colorResourceId = runCatching {
color::class.java.getField(colorName).getInt(null)
}.getOrElse { 0 }
add(convertToColor(colorResourceId))
}
}
add(convertToColor(colorResourceId))
}
}
}
}
}
private fun convertToColor(id: Int): String {
val clr = if (isSupported()) ContextCompat.getColor(context, id) else 0
val clr = if (isSupported()) ContextCompat.getColor(context.get()!!, id) else 0
return String.format("#%06X", 0xFFFFFF and clr)
}
}

View file

@ -1,42 +1,47 @@
package GoonXposed.xposed.module.appearance
package ShiggyXposed.xposed.modules.appearance
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Companion.JSON
import android.content.Context
import android.content.res.Resources
import androidx.core.graphics.toColorInt
import de.robv.android.xposed.callbacks.XC_LoadPackage
import java.io.File
import ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Companion.JSON
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.put
import java.io.File
@Serializable data class Author(val name: String, val id: String? = null)
@Serializable
data class Author(
val name: String, val id: String? = null
)
@Serializable
data class ThemeData(
val name: String,
val description: String? = null,
val authors: List<Author>? = null,
val spec: Int,
val semanticColors: Map<String, List<String>>? = null,
val rawColors: Map<String, String>? = null
val name: String,
val description: String? = null,
val authors: List<Author>? = null,
val spec: Int,
val semanticColors: Map<String, List<String>>? = null,
val rawColors: Map<String, String>? = null
)
@Serializable data class Theme(val id: String, val selected: Boolean, val data: ThemeData)
@Serializable
data class Theme(
val id: String, val selected: Boolean, val data: ThemeData
)
class ThemesModule : Module() {
object ThemesModule : Module() {
private lateinit var param: XC_LoadPackage.LoadPackageParam
private var theme: Theme? = null
private val rawColorMap = mutableMapOf<String, Int>()
private companion object {
const val THEME_FILE = "current-theme.json"
}
private const val THEME_FILE = "current-theme.json"
@ExperimentalSerializationApi
override fun buildPayload(builder: JsonObjectBuilder) {
@ -48,9 +53,7 @@ class ThemesModule : Module() {
}
private fun String.fromScreamingSnakeToCamelCase() =
this.split("_").joinToString("") { it ->
it.lowercase().replaceFirstChar { it.uppercase() }
}
this.split("_").joinToString("") { it -> it.lowercase().replaceFirstChar { it.uppercase() } }
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
param = packageParam
@ -58,7 +61,7 @@ class ThemesModule : Module() {
hookTheme()
}
private fun File.isValidish(): Boolean {
private fun File.isValidThemeFile(): Boolean {
if (!this.exists()) return false
val text = this.readText()
@ -66,11 +69,8 @@ class ThemesModule : Module() {
}
private fun getTheme(): Theme? {
val themeFile =
File(param.appInfo.dataDir, "${Constants.FILES_DIR}/${THEME_FILE}").apply {
asFile()
}
if (!themeFile.isValidish()) return null
val themeFile = File(param.appInfo.dataDir, "${Constants.FILES_DIR}/${THEME_FILE}").apply { asFile() }
if (!themeFile.isValidThemeFile()) return null
return try {
val themeText = themeFile.readText()
@ -106,34 +106,28 @@ class ThemesModule : Module() {
// If there's any rawColors value, hook the color getter
if (!theme.data.rawColors.isNullOrEmpty()) {
val getColorCompat =
themeManager.getDeclaredMethod(
"getColorCompat",
Resources::class.java,
Int::class.javaPrimitiveType,
Resources.Theme::class.java,
)
val getColorCompat = themeManager.getDeclaredMethod(
"getColorCompat",
Resources::class.java,
Int::class.javaPrimitiveType,
Resources.Theme::class.java,
)
val getColorCompatLegacy =
themeManager.getDeclaredMethod(
"getColorCompat",
Context::class.java,
Int::class.javaPrimitiveType
)
val getColorCompatLegacy = themeManager.getDeclaredMethod(
"getColorCompat", Context::class.java, Int::class.javaPrimitiveType
)
val patch =
MethodHookBuilder().run {
before {
val arg1 = args[0]
val resources =
if (arg1 is Context) arg1.resources else (arg1 as Resources)
val name = resources.getResourceEntryName(args[1] as Int)
val patch = MethodHookBuilder().run {
before {
val arg1 = args[0]
val resources = if (arg1 is Context) arg1.resources else (arg1 as Resources)
val name = resources.getResourceEntryName(args[1] as Int)
if (rawColorMap[name] != null) result = rawColorMap[name]
}
if (rawColorMap[name] != null) result = rawColorMap[name]
}
build()
}
build()
}
getColorCompat.hook(patch)
getColorCompatLegacy.hook(patch)
@ -153,8 +147,13 @@ class ThemesModule : Module() {
private fun hookThemeMethod(themeClass: Class<*>, methodName: String, themeValue: Int) {
try {
themeClass.getDeclaredMethod(methodName).let { method ->
method.hook { before { result = themeValue } }
method.hook {
before {
result = themeValue
}
}
}
} catch (_: NoSuchMethodException) {}
} catch (_: NoSuchMethodException) {
}
}
}

View file

@ -1,52 +1,94 @@
package GoonXposed.xposed.modules.bridge
package ShiggyXposed.xposed.modules.bridge
import GoonXposed.xposed.Module
import android.app.Activity
import android.app.AlertDialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.widget.Toast
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils
import java.io.File
class AdditionalBridgeMethodsModule : Module() {
override fun onContext(context: Context) =
with(context) {
BridgeModule.registerMethod("revenge.alertError") {
val (error, version) = it
val app = getAppInfo()
val errorString = "$error"
object AdditionalBridgeMethodsModule : Module() {
override fun onContext(context: Context) = with(context) {
BridgeModule.registerMethod("Shiggy.fs.getConstants") {
mapOf(
"data" to dataDir.absolutePath,
"files" to filesDir.absolutePath,
"cache" to cacheDir.absolutePath,
)
}
val clipboard =
applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as
ClipboardManager
val clip = ClipData.newPlainText("Stack Trace", errorString)
BridgeModule.registerMethod("Shiggy.fs.delete") {
val (path) = it
File(path as String).run {
if (this.isDirectory) this.deleteRecursively()
else this.delete()
}
}
AlertDialog.Builder(this)
.setTitle("GoonXposed Error")
.setMessage(
"""
GoonXposed: $version
BridgeModule.registerMethod("Shiggy.fs.exists") {
val (path) = it
File(path as String).exists()
}
BridgeModule.registerMethod("Shiggy.fs.read") { it ->
val (path) = it
val file = File(path as String).apply { openFileGuarded() }
file.bufferedReader().use { it.readText() }
}
BridgeModule.registerMethod("Shiggy.fs.write") {
val (path, contents) = it
val file = File(path as String).apply { openFileGuarded() }
file.writeText(contents as String)
}
}
override fun onActivity(activity: Activity) = with(activity) {
BridgeModule.registerMethod("Shiggy.alertError") {
val (error, version) = it
val app = getAppInfo()
val errorString = "$error"
val clipboard = applicationContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText("Stack Trace", errorString)
AlertDialog.Builder(this)
.setTitle("ShiggyCord Error")
.setMessage(
"""
ShiggyCord: $version
${app.name}: ${app.version} (${app.versionCode})
Device: ${Build.MANUFACTURER} ${Build.MODEL}
""".trimIndent() +
errorString
)
.setPositiveButton("OK") { dialog, _ -> dialog.dismiss() }
.setNeutralButton("Copy") { dialog, _ ->
clipboard.setPrimaryClip(clip)
Toast.makeText(
applicationContext,
"Copied stack trace",
Toast.LENGTH_SHORT
)
.show()
dialog.dismiss()
}
.show()
null
""".trimIndent() + errorString
)
.setPositiveButton("OK") { dialog, _ ->
dialog.dismiss()
}
}
.setNeutralButton("Copy") { dialog, _ ->
clipboard.setPrimaryClip(clip)
Toast.makeText(applicationContext, "Copied stack trace", Toast.LENGTH_SHORT).show()
dialog.dismiss()
}
.show()
null
}
BridgeModule.registerMethod("Shiggy.showRecoveryAlert") {
Utils.showRecoveryAlert(this)
}
}
private fun File.openFileGuarded() {
if (!this.exists()) throw Error("Path does not exist: $path")
if (!this.isFile) throw Error("Path is not a file: $path")
}
}

View file

@ -1,11 +1,10 @@
package GoonXposed.xposed.modules.bridge
package ShiggyXposed.xposed.modules.bridge
import GoonXposed.xposed.BuildConfig
import GoonXposed.xposed.Constants
import GoonXposed.xposed.Module
import GoonXposed.xposed.Utils.Log
import de.robv.android.xposed.callbacks.XC_LoadPackage
import java.io.File
import ShiggyXposed.xposed.BuildConfig
import ShiggyXposed.xposed.Constants
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
import java.lang.reflect.Method
/**
@ -24,15 +23,14 @@ typealias BridgeMethodArgs = ArrayList<Any>
* To call a method, pass an object with the following structure to a hooked method:
* ```js
* {
* revenge: {
* Shiggy: {
* method: "method.name",
* args: [arg1, arg2, ...]
* }
* }
* ```
*
* For methods that accept additional arguments, only the first object argument is used for the
* bridge call.
* For methods that accept additional arguments, only the first object argument is used for the bridge call.
*
* The result will be an object with either a `result` or `error` key:
* ```js
@ -47,153 +45,104 @@ typealias BridgeMethodArgs = ArrayList<Any>
* }
* ```
*/
class BridgeModule : Module() {
object BridgeModule : Module() {
private lateinit var readableMapGetString: Method
private lateinit var readableMapToHashMapMethod: Method
private lateinit var readableMapToHashMap: Method
private lateinit var argumentsMakeNative: Method
companion object {
private const val CALL_DATA_KEY = "revenge"
private const val METHOD_NAME_KEY = "method"
private const val METHOD_ARGS_KEY = "args"
private const val CALL_DATA_KEY = "Shiggy"
private const val METHOD_NAME_KEY = "method"
private const val METHOD_ARGS_KEY = "args"
private fun File.openFileGuarded() {
if (!this.exists()) throw Error("Path does not exist: $path")
if (!this.isFile) throw Error("Path is not a file: $path")
}
private val methods: MutableMap<String, BridgeMethodCallback> = mutableMapOf()
private val methods: MutableMap<String, BridgeMethodCallback> =
mutableMapOf(
"revenge.info" to
{
mapOf(
"name" to Constants.LOADER_NAME,
"version" to BuildConfig.VERSION_CODE
)
},
"revenge.test" to
{
mapOf(
"string" to "string",
"number" to 7256,
"array" to listOf("testing", 527737, listOf(true)),
"object" to mapOf("nested" to true),
"boolean" to false,
"args" to it,
)
},
"revenge.fs.delete" to
{
val (path) = it
File(path as String).run {
if (this.isDirectory) this.deleteRecursively()
else this.delete()
}
},
"revenge.fs.exists" to
{
val (path) = it
File(path as String).exists()
},
"revenge.fs.read" to
{ it ->
val (path) = it
val file = File(path as String).apply { openFileGuarded() }
file.bufferedReader().use { it.readText() }
},
"revenge.fs.write" to
{ it ->
val (path, contents) = it
val file = File(path as String).apply { openFileGuarded() }
file.writeText(contents as String)
}
)
/**
* Registers a bridge method that can be called from JavaScript.
*
* If the method is already registered, it will be overwritten.
*
* @param name The name of the method to register.
* @param callback The callback to invoke when the method is called.
*/
fun registerMethod(name: String, callback: BridgeMethodCallback) {
if (methods.containsKey(name))
Log.w("Bridge method already exists and will be overridden: $name")
methods[name] = callback
}
/**
* Registers a bridge method that can be called from JavaScript.
*
* If the method is already registered, it will be overwritten.
*
* @param name The name of the method to register.
* @param callback The callback to invoke when the method is called.
*/
fun registerMethod(name: String, callback: BridgeMethodCallback) {
if (methods.containsKey(name)) Log.w("Bridge method already exists and will be overridden: $name")
methods[name] = callback
}
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) {
with(packageParam) {
val arguments = classLoader.loadClass("com.facebook.react.bridge.Arguments")
val readableMap = classLoader.loadClass("com.facebook.react.bridge.ReadableMap")
val promise = classLoader.loadClass("com.facebook.react.bridge.Promise")
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val arguments = classLoader.loadClass("com.facebook.react.bridge.Arguments")
val readableMap = classLoader.loadClass("com.facebook.react.bridge.ReadableMap")
val promise = classLoader.loadClass("com.facebook.react.bridge.Promise")
val promiseResolve = promise.method("resolve", Object::class.java)
argumentsMakeNative = arguments.method("makeNativeObject", Object::class.java)
readableMapGetString = readableMap.method("getString", String::class.java)
readableMapToHashMapMethod = readableMap.method("toHashMap")
val promiseResolve = promise.method("resolve", Object::class.java)
argumentsMakeNative = arguments.method("makeNativeObject", Object::class.java)
readableMapGetString = readableMap.method("getString", String::class.java)
readableMapToHashMap = readableMap.method("toHashMap")
classLoader.loadClass("com.horcrux.svg.RNSVGRenderableManager").hookMethod(
"getBBox",
Double::class.javaObjectType,
readableMap
) {
classLoader.loadClass("com.horcrux.svg.RNSVGRenderableManager")
.hookMethod("getBBox", Double::class.javaObjectType, readableMap) {
before {
callBridgeMethod(readableMapToHashMapConverter(args[1]!!))?.let {
result = it.toNativeObject()
}
callBridgeMethod(readableMapToHashMap(args[1]!!))?.let { result = it.toNativeObject() }
}
}
classLoader.loadClass("com.facebook.react.modules.blob.FileReaderModule").hookMethod(
"readAsDataURL",
readableMap,
promise
) {
classLoader.loadClass("com.facebook.react.modules.blob.FileReaderModule")
.hookMethod("readAsDataURL", readableMap, promise) {
before {
// Avoid shadowing the outer `readableMap` and `promise` variables
val (readableMapArg, promiseArg) = args
callBridgeMethod(readableMapToHashMapConverter(readableMapArg!!))?.let {
promiseResolve.invoke(promiseArg, it.toNativeObject())
val (readableMap, promise) = args
callBridgeMethod(readableMapToHashMap(readableMap!!))?.let {
promiseResolve.invoke(promise, it.toNativeObject())
result = null
}
}
}
}
// Clear methods every package load. Plugins should re-register on each load.
methods.clear()
registerDefaultMethods()
return@with
}
private fun Any?.toNativeObject(): Any? =
argumentsMakeNative.invoke(
null,
when (this) {
Unit -> null
else -> this
}
private fun registerDefaultMethods() {
methods["Shiggy.info"] = {
mapOf(
"name" to Constants.LOADER_NAME, "version" to BuildConfig.VERSION_CODE
)
}
private fun readableMapToHashMapConverter(map: Any): HashMap<String, Any?> {
@Suppress("UNCHECKED_CAST")
return readableMapToHashMapMethod.invoke(map) as HashMap<String, Any?>
}
private fun callBridgeMethod(hashMap: HashMap<String, Any?>): Map<String, Any?>? {
try {
val pair = getBridgeCallData(hashMap) ?: return null
val (method, args) = pair
val ret = method(args).toNativeObject()
return mapOf("result" to ret)
} catch (e: Throwable) {
return mapOf("error" to e.toString())
methods["Shiggy.test"] = {
mapOf(
"string" to "string",
"number" to 7256,
"array" to listOf("testing", 527737, listOf(true)),
"object" to mapOf("nested" to true),
"boolean" to false,
"args" to it,
)
}
}
private fun getBridgeCallData(
hashMap: HashMap<String, Any?>
): Pair<BridgeMethodCallback, BridgeMethodArgs>? {
private fun Any?.toNativeObject(): Any? = argumentsMakeNative.invoke(
null, when (this) {
Unit -> null
else -> this
}
)
private fun readableMapToHashMap(map: Any): HashMap<String, Any?> {
@Suppress("UNCHECKED_CAST") return readableMapToHashMap.invoke(map) as HashMap<String, Any?>
}
private fun callBridgeMethod(hashMap: HashMap<String, Any?>): Map<String, Any?>? = try {
val (method, args) = getBridgeCallData(hashMap) ?: return null
val ret = method(args).toNativeObject()
mapOf("result" to ret)
} catch (e: Throwable) {
mapOf("error" to e.stackTraceToString())
}
private fun getBridgeCallData(hashMap: HashMap<String, Any?>): Pair<BridgeMethodCallback, BridgeMethodArgs>? {
@Suppress("UNCHECKED_CAST") val data = hashMap[CALL_DATA_KEY] as HashMap<String, Any?>?
data ?: return null

View file

@ -0,0 +1,46 @@
package ShiggyXposed.xposed.modules.no_track
import android.content.Context
import de.robv.android.xposed.callbacks.XC_LoadPackage
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
/**
* Hooks Discord's crash reporting to disable Sentry initialization.
* While Discord doesn't have Sentry auto-initialization on by default, we still hook the content provider just to be safe.
*/
object BlockCrashReportingModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val crashReportingClass = classLoader.safeLoadClass("com.discord.crash_reporting.CrashReporting")
crashReportingClass?.apply {
// Hooking this will result in a crash after a few seconds, since Discord expects initialization to complete when setting a Sentry tag.
// So we also hook isDisabled to make sure those calls are no-ops.
hookMethod(
"init", Context::class.java, String::class.java
) {
before {
Log.i("Blocked CrashReporting initialization")
result = null
}
}
runCatching {
hookMethod("isDisabled") {
before {
Log.i("Forced CrashReporting.isDisabled() to true")
result = true
}
}
}
val sentryInitProviderClass = classLoader.safeLoadClass("io.sentry.android.core.SentryInitProvider")
sentryInitProviderClass?.hookMethod("onCreate") {
before {
Log.i("Blocked SentryInitProvider initialization")
result = true
}
}
return@with
}
}
}

View file

@ -0,0 +1,25 @@
package ShiggyXposed.xposed.modules.no_track
import android.content.Context
import de.robv.android.xposed.callbacks.XC_LoadPackage
import ShiggyXposed.xposed.Module
import ShiggyXposed.xposed.Utils.Log
/**
* Hooks Discord's deep links tracking to disable AppsFlyer initialization.
*/
object BlockDeepLinksTrackingModule : Module() {
override fun onLoad(packageParam: XC_LoadPackage.LoadPackageParam) = with(packageParam) {
val deepLinksClass = classLoader.safeLoadClass("com.discord.deep_link.DeepLinks")
deepLinksClass?.hookMethod(
"init", Context::class.java
) {
before {
Log.i("Blocked DeepLinks tracking initialization")
result = null
}
}
return@with
}
}