package me.eternal.purrfect.common.scripting import android.os.Handler import android.widget.Toast import kotlinx.coroutines.* import me.eternal.purrfect.common.scripting.bindings.AbstractBinding import me.eternal.purrfect.common.scripting.bindings.BindingsContext import me.eternal.purrfect.common.scripting.impl.JavaInterfaces import me.eternal.purrfect.common.scripting.impl.Networking import me.eternal.purrfect.common.scripting.impl.Protobuf import me.eternal.purrfect.common.util.ktx.KavaRefFieldBridge import me.eternal.purrfect.common.scripting.ktx.contextScope import me.eternal.purrfect.common.scripting.ktx.putFunction import me.eternal.purrfect.common.scripting.ktx.scriptable import me.eternal.purrfect.common.scripting.ktx.scriptableObject import me.eternal.purrfect.common.scripting.type.ModuleInfo import me.eternal.purrfect.common.scripting.type.Permissions import me.eternal.purrfect.common.scripting.ui.InterfaceManager import org.mozilla.javascript.Function import org.mozilla.javascript.ScriptableObject import org.mozilla.javascript.Undefined import org.mozilla.javascript.Wrapper import java.io.Reader import java.lang.reflect.Modifier import kotlin.reflect.KClass class JSModule( private val scriptRuntime: ScriptRuntime, val moduleInfo: ModuleInfo, private val reader: Reader, ) { val coroutineScope = CoroutineScope(Dispatchers.IO) private val moduleBindings = mutableMapOf() private lateinit var moduleObject: ScriptableObject private val moduleBindingContext by lazy { BindingsContext( moduleInfo = moduleInfo, runtime = scriptRuntime, module = this, ) } fun load(block: ScriptableObject.() -> Unit) { contextScope { val classLoader = scriptRuntime.androidContext.classLoader moduleObject = initSafeStandardObjects() moduleObject.putConst("module", moduleObject, scriptableObject { putConst("info", this, scriptableObject { putConst("name", this, moduleInfo.name) putConst("version", this, moduleInfo.version) putConst("displayName", this, moduleInfo.displayName) putConst("description", this, moduleInfo.description) putConst("author", this, moduleInfo.author) putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion) putConst("minPSVersion", this, moduleInfo.minPSVersion) putConst("minSEVersion", this, moduleInfo.minPSVersion) putConst("grantedPermissions", this, moduleInfo.grantedPermissions) }) }) scriptRuntime.logger.apply { moduleObject.putConst("console", moduleObject, scriptableObject { putFunction("log") { info(argsToString(it)) } putFunction("warn") { warn(argsToString(it)) } putFunction("error") { error(argsToString(it)) } putFunction("debug") { debug(argsToString(it)) } putFunction("info") { info(argsToString(it)) } putFunction("trace") { verbose(argsToString(it)) } putFunction("verbose") { verbose(argsToString(it)) } }) } registerBindings( JavaInterfaces(), InterfaceManager(), Networking(), Protobuf() ) moduleObject.putFunction("setField") { args -> val obj = args?.get(0) ?: return@putFunction Undefined.instance val name = args[1].toString() val value = args[2] runCatching { KavaRefFieldBridge.getField(obj, name)?.javaClass?.name }.getOrNull()?.let { typeName -> KavaRefFieldBridge.setField(obj, name, value.toPrimitiveValue(lazy { typeName })) } ?: return@putFunction Undefined.instance Undefined.instance } moduleObject.putFunction("getField") { args -> val obj = args?.get(0) ?: return@putFunction Undefined.instance val name = args[1].toString() runCatching { KavaRefFieldBridge.getField(obj, name) }.getOrNull() ?: Undefined.instance } moduleObject.putFunction("sleep") { args -> val time = args?.get(0) as? Number ?: return@putFunction Undefined.instance Thread.sleep(time.toLong()) Undefined.instance } moduleObject.putFunction("findClass") { val className = it?.get(0).toString() val useModClassLoader = it?.getOrNull(1) as? Boolean ?: false if (useModClassLoader) moduleInfo.ensurePermissionGranted(Permissions.UNSAFE_CLASSLOADER) runCatching { if (useModClassLoader) this::class.java.classLoader?.loadClass(className) else classLoader.loadClass(className) }.onFailure { throwable -> scriptRuntime.logger.error("Failed to load class $className", throwable) }.getOrNull() } moduleObject.putFunction("type") { args -> val className = args?.get(0).toString() val useModClassLoader = args?.getOrNull(1) as? Boolean ?: false if (useModClassLoader) moduleInfo.ensurePermissionGranted(Permissions.UNSAFE_CLASSLOADER) val clazz = runCatching { if (useModClassLoader) this::class.java.classLoader?.loadClass(className) else classLoader.loadClass(className) }.getOrNull() ?: return@putFunction Undefined.instance scriptableObject("JavaClassWrapper") { val newInstance: (Array?) -> Any? = { args -> val constructor = clazz.declaredConstructors.find { (args ?: emptyArray()).isSameParameters(it.parameterTypes) }?.also { it.isAccessible = true } ?: throw IllegalArgumentException("Constructor not found with args ${argsToString(args)}") constructor.newInstance(*args ?: emptyArray()) } putFunction("__new__") { newInstance(it) } clazz.declaredMethods.filter { Modifier.isStatic(it.modifiers) }.forEach { method -> putFunction(method.name) { args -> val declaredMethod = clazz.declaredMethods.find { it.name == method.name && (args ?: emptyArray()).isSameParameters(it.parameterTypes) }?.also { it.isAccessible = true } ?: throw IllegalArgumentException("Method ${method.name} not found with args ${argsToString(args)}") declaredMethod.invoke(null, *args ?: emptyArray()) } } // Expose static fields if they can be read through class reflection. clazz.declaredFields.filter { Modifier.isStatic(it.modifiers) }.forEach { field -> defineProperty( field.name, { runCatching { field.get(null) }.getOrNull() }, { value -> runCatching { field.set(null, value) } }, 0 ) } if (get("newInstance") == null) { putFunction("newInstance") { newInstance(it) } } } } moduleObject.putFunction("logInfo") { args -> scriptRuntime.logger.info(argsToString(args)) Undefined.instance } moduleObject.putFunction("logError") { args -> scriptRuntime.logger.error(argsToString(arrayOf(args?.get(0))), args?.getOrNull(1) as? Throwable ?: Throwable()) Undefined.instance } moduleObject.putFunction("setTimeout") { val function = it?.get(0) as? Function ?: return@putFunction Undefined.instance val time = it[1] as? Number ?: 0 return@putFunction coroutineScope.launch { delay(time.toLong()) contextScope { function.call(this, this@putFunction, this@putFunction, emptyArray()) } } } moduleObject.putFunction("setInterval") { val function = it?.get(0) as? Function ?: return@putFunction Undefined.instance val time = it[1] as? Number ?: 0 return@putFunction coroutineScope.launch { while (true) { delay(time.toLong()) contextScope { function.call(this, this@putFunction, this@putFunction, emptyArray()) } } } } arrayOf("clearInterval", "clearTimeout").forEach { moduleObject.putFunction(it) { args -> val job = args?.get(0) as? Job ?: return@putFunction Undefined.instance runCatching { job.cancel() } Undefined.instance } } for (toastFunc in listOf("longToast", "shortToast")) { moduleObject.putFunction(toastFunc) { args -> Handler(scriptRuntime.androidContext.mainLooper).post { Toast.makeText( scriptRuntime.androidContext, args?.joinToString(" ") ?: "", if (toastFunc == "longToast") Toast.LENGTH_LONG else Toast.LENGTH_SHORT ).show() } Undefined.instance } } block(moduleObject) moduleBindings.forEach { (_, instance) -> instance.context = moduleBindingContext runCatching { instance.onInit() }.onFailure { scriptRuntime.logger.error("Failed to init binding ${instance.name}", it) } } moduleObject.putFunction("require") { args -> val bindingName = args?.get(0).toString() val (namespace, path) = bindingName.takeIf { it.startsWith("@") && it.contains("/") }?.let { it.substring(1).substringBefore("/") to it.substringAfter("/") } ?: (null to "") when (namespace) { "modules" -> scriptRuntime.getModuleByName(path)?.moduleObject?.scriptable("module")?.scriptable("exports") else -> moduleBindings[bindingName]?.getObject() } } } contextScope(shouldOptimize = scriptRuntime.config().scripting.disableOptimization.getNullable() != true) { evaluateReader(moduleObject, reader, moduleInfo.name, 1, null) } } fun unload() { callFunction("module.onUnload") runCatching { coroutineScope.cancel("Module unloaded") } moduleBindings.entries.removeIf { (name, binding) -> runCatching { binding.onDispose() }.onFailure { scriptRuntime.logger.error("Failed to dispose binding $name", it) } true } } fun callFunction(name: String, vararg args: Any?) { contextScope { name.split(".").also { split -> val function = split.dropLast(1).fold(moduleObject) { obj, key -> obj.get(key, obj) as? ScriptableObject ?: return@contextScope Unit }.get(split.last(), moduleObject) as? Function ?: return@contextScope Unit runCatching { function.call(this, moduleObject, moduleObject, args) }.onFailure { scriptRuntime.logger.error("Error while calling function $name", it) } } } } fun registerBindings(vararg bindings: AbstractBinding) { bindings.forEach { moduleBindings[it.name] = it.apply { context = moduleBindingContext } } } fun onBridgeConnected(reloaded: Boolean = false) { if (reloaded) { moduleBindings.values.forEach { binding -> runCatching { binding.onBridgeReloaded() }.onFailure { scriptRuntime.logger.error("Failed to call onBridgeConnected for binding ${binding.name}", it) } } } callFunction("module.onBridgeConnected", reloaded) } @Suppress("UNCHECKED_CAST") fun getBinding(clazz: KClass): T? { return moduleBindings.values.find { clazz.isInstance(it) } as? T } private fun argsToString(args: Array?): String { return args?.joinToString(" ") { when (it) { is Wrapper -> it.unwrap().let { value -> if (value is Throwable) value.message + "\n" + value.stackTraceToString() else value.toString() } else -> it.toString() } } ?: "null" } }