JSModule.kt (13474B) - raw
1 package me.rhunk.snapenhance.common.scripting 2 3 import android.os.Handler 4 import android.widget.Toast 5 import kotlinx.coroutines.* 6 import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding 7 import me.rhunk.snapenhance.common.scripting.bindings.BindingsContext 8 import me.rhunk.snapenhance.common.scripting.impl.JavaInterfaces 9 import me.rhunk.snapenhance.common.scripting.impl.Networking 10 import me.rhunk.snapenhance.common.scripting.impl.Protobuf 11 import me.rhunk.snapenhance.common.scripting.ktx.contextScope 12 import me.rhunk.snapenhance.common.scripting.ktx.putFunction 13 import me.rhunk.snapenhance.common.scripting.ktx.scriptable 14 import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject 15 import me.rhunk.snapenhance.common.scripting.type.ModuleInfo 16 import me.rhunk.snapenhance.common.scripting.type.Permissions 17 import me.rhunk.snapenhance.common.scripting.ui.InterfaceManager 18 import org.mozilla.javascript.Function 19 import org.mozilla.javascript.ScriptableObject 20 import org.mozilla.javascript.Undefined 21 import org.mozilla.javascript.Wrapper 22 import java.io.Reader 23 import java.lang.reflect.Modifier 24 import kotlin.reflect.KClass 25 26 class JSModule( 27 private val scriptRuntime: ScriptRuntime, 28 val moduleInfo: ModuleInfo, 29 private val reader: Reader, 30 ) { 31 val coroutineScope = CoroutineScope(Dispatchers.IO) 32 private val moduleBindings = mutableMapOf<String, AbstractBinding>() 33 private lateinit var moduleObject: ScriptableObject 34 35 private val moduleBindingContext by lazy { 36 BindingsContext( 37 moduleInfo = moduleInfo, 38 runtime = scriptRuntime, 39 module = this, 40 ) 41 } 42 43 fun load(block: ScriptableObject.() -> Unit) { 44 contextScope { 45 val classLoader = scriptRuntime.androidContext.classLoader 46 moduleObject = initSafeStandardObjects() 47 moduleObject.putConst("module", moduleObject, scriptableObject { 48 putConst("info", this, scriptableObject { 49 putConst("name", this, moduleInfo.name) 50 putConst("version", this, moduleInfo.version) 51 putConst("displayName", this, moduleInfo.displayName) 52 putConst("description", this, moduleInfo.description) 53 putConst("author", this, moduleInfo.author) 54 putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion) 55 putConst("minSEVersion", this, moduleInfo.minSEVersion) 56 putConst("grantedPermissions", this, moduleInfo.grantedPermissions) 57 }) 58 }) 59 60 scriptRuntime.logger.apply { 61 moduleObject.putConst("console", moduleObject, scriptableObject { 62 putFunction("log") { info(argsToString(it)) } 63 putFunction("warn") { warn(argsToString(it)) } 64 putFunction("error") { error(argsToString(it)) } 65 putFunction("debug") { debug(argsToString(it)) } 66 putFunction("info") { info(argsToString(it)) } 67 putFunction("trace") { verbose(argsToString(it)) } 68 putFunction("verbose") { verbose(argsToString(it)) } 69 }) 70 } 71 72 registerBindings( 73 JavaInterfaces(), 74 InterfaceManager(), 75 Networking(), 76 Protobuf() 77 ) 78 79 moduleObject.putFunction("setField") { args -> 80 val obj = args?.get(0) ?: return@putFunction Undefined.instance 81 val name = args[1].toString() 82 val value = args[2] 83 val field = obj.javaClass.declaredFields.find { it.name == name } ?: return@putFunction Undefined.instance 84 field.isAccessible = true 85 field.set(obj, value.toPrimitiveValue(lazy { field.type.name })) 86 Undefined.instance 87 } 88 89 moduleObject.putFunction("getField") { args -> 90 val obj = args?.get(0) ?: return@putFunction Undefined.instance 91 val name = args[1].toString() 92 val field = obj.javaClass.declaredFields.find { it.name == name } ?: return@putFunction Undefined.instance 93 field.isAccessible = true 94 field.get(obj) 95 } 96 97 moduleObject.putFunction("sleep") { args -> 98 val time = args?.get(0) as? Number ?: return@putFunction Undefined.instance 99 Thread.sleep(time.toLong()) 100 Undefined.instance 101 } 102 103 moduleObject.putFunction("findClass") { 104 val className = it?.get(0).toString() 105 val useModClassLoader = it?.getOrNull(1) as? Boolean ?: false 106 if (useModClassLoader) moduleInfo.ensurePermissionGranted(Permissions.UNSAFE_CLASSLOADER) 107 108 runCatching { 109 if (useModClassLoader) this::class.java.classLoader?.loadClass(className) 110 else classLoader.loadClass(className) 111 }.onFailure { throwable -> 112 scriptRuntime.logger.error("Failed to load class $className", throwable) 113 }.getOrNull() 114 } 115 116 moduleObject.putFunction("type") { args -> 117 val className = args?.get(0).toString() 118 val useModClassLoader = args?.getOrNull(1) as? Boolean ?: false 119 if (useModClassLoader) moduleInfo.ensurePermissionGranted(Permissions.UNSAFE_CLASSLOADER) 120 121 val clazz = runCatching { 122 if (useModClassLoader) this::class.java.classLoader?.loadClass(className) else classLoader.loadClass(className) 123 }.getOrNull() ?: return@putFunction Undefined.instance 124 125 scriptableObject("JavaClassWrapper") { 126 val newInstance: (Array<out Any?>?) -> Any? = { args -> 127 val constructor = clazz.declaredConstructors.find { 128 (args ?: emptyArray()).isSameParameters(it.parameterTypes) 129 }?.also { it.isAccessible = true } ?: throw IllegalArgumentException("Constructor not found with args ${argsToString(args)}") 130 constructor.newInstance(*args ?: emptyArray()) 131 } 132 putFunction("__new__") { newInstance(it) } 133 134 clazz.declaredMethods.filter { Modifier.isStatic(it.modifiers) }.forEach { method -> 135 putFunction(method.name) { args -> 136 val declaredMethod = clazz.declaredMethods.find { 137 it.name == method.name && (args ?: emptyArray()).isSameParameters(it.parameterTypes) 138 }?.also { it.isAccessible = true } ?: throw IllegalArgumentException("Method ${method.name} not found with args ${argsToString(args)}") 139 declaredMethod.invoke(null, *args ?: emptyArray()) 140 } 141 } 142 143 clazz.declaredFields.filter { Modifier.isStatic(it.modifiers) }.forEach { field -> 144 field.isAccessible = true 145 defineProperty(field.name, { field.get(null) }, { value -> field.set(null, value) }, 0) 146 } 147 148 if (get("newInstance") == null) { 149 putFunction("newInstance") { newInstance(it) } 150 } 151 } 152 } 153 154 moduleObject.putFunction("logInfo") { args -> 155 scriptRuntime.logger.info(argsToString(args)) 156 Undefined.instance 157 } 158 159 moduleObject.putFunction("logError") { args -> 160 scriptRuntime.logger.error(argsToString(arrayOf(args?.get(0))), args?.getOrNull(1) as? Throwable ?: Throwable()) 161 Undefined.instance 162 } 163 164 moduleObject.putFunction("setTimeout") { 165 val function = it?.get(0) as? Function ?: return@putFunction Undefined.instance 166 val time = it[1] as? Number ?: 0 167 168 return@putFunction coroutineScope.launch { 169 delay(time.toLong()) 170 contextScope { 171 function.call(this, this@putFunction, this@putFunction, emptyArray()) 172 } 173 } 174 } 175 176 moduleObject.putFunction("setInterval") { 177 val function = it?.get(0) as? Function ?: return@putFunction Undefined.instance 178 val time = it[1] as? Number ?: 0 179 180 return@putFunction coroutineScope.launch { 181 while (true) { 182 delay(time.toLong()) 183 contextScope { 184 function.call(this, this@putFunction, this@putFunction, emptyArray()) 185 } 186 } 187 } 188 } 189 190 arrayOf("clearInterval", "clearTimeout").forEach { 191 moduleObject.putFunction(it) { args -> 192 val job = args?.get(0) as? Job ?: return@putFunction Undefined.instance 193 runCatching { 194 job.cancel() 195 } 196 Undefined.instance 197 } 198 } 199 200 for (toastFunc in listOf("longToast", "shortToast")) { 201 moduleObject.putFunction(toastFunc) { args -> 202 Handler(scriptRuntime.androidContext.mainLooper).post { 203 Toast.makeText( 204 scriptRuntime.androidContext, 205 args?.joinToString(" ") ?: "", 206 if (toastFunc == "longToast") Toast.LENGTH_LONG else Toast.LENGTH_SHORT 207 ).show() 208 } 209 Undefined.instance 210 } 211 } 212 213 block(moduleObject) 214 215 moduleBindings.forEach { (_, instance) -> 216 instance.context = moduleBindingContext 217 218 runCatching { 219 instance.onInit() 220 }.onFailure { 221 scriptRuntime.logger.error("Failed to init binding ${instance.name}", it) 222 } 223 } 224 225 moduleObject.putFunction("require") { args -> 226 val bindingName = args?.get(0).toString() 227 val (namespace, path) = bindingName.takeIf { 228 it.startsWith("@") && it.contains("/") 229 }?.let { 230 it.substring(1).substringBefore("/") to it.substringAfter("/") 231 } ?: (null to "") 232 233 when (namespace) { 234 "modules" -> scriptRuntime.getModuleByName(path)?.moduleObject?.scriptable("module")?.scriptable("exports") 235 else -> moduleBindings[bindingName]?.getObject() 236 } 237 } 238 } 239 240 contextScope(shouldOptimize = scriptRuntime.config().scripting.disableOptimization.getNullable() != true) { 241 evaluateReader(moduleObject, reader, moduleInfo.name, 1, null) 242 } 243 } 244 245 fun unload() { 246 callFunction("module.onUnload") 247 runCatching { 248 coroutineScope.cancel("Module unloaded") 249 } 250 moduleBindings.entries.removeIf { (name, binding) -> 251 runCatching { 252 binding.onDispose() 253 }.onFailure { 254 scriptRuntime.logger.error("Failed to dispose binding $name", it) 255 } 256 true 257 } 258 } 259 260 fun callFunction(name: String, vararg args: Any?) { 261 contextScope { 262 name.split(".").also { split -> 263 val function = split.dropLast(1).fold(moduleObject) { obj, key -> 264 obj.get(key, obj) as? ScriptableObject ?: return@contextScope Unit 265 }.get(split.last(), moduleObject) as? Function ?: return@contextScope Unit 266 267 runCatching { 268 function.call(this, moduleObject, moduleObject, args) 269 }.onFailure { 270 scriptRuntime.logger.error("Error while calling function $name", it) 271 } 272 } 273 } 274 } 275 276 fun registerBindings(vararg bindings: AbstractBinding) { 277 bindings.forEach { 278 moduleBindings[it.name] = it.apply { 279 context = moduleBindingContext 280 } 281 } 282 } 283 284 fun onBridgeConnected(reloaded: Boolean = false) { 285 if (reloaded) { 286 moduleBindings.values.forEach { binding -> 287 runCatching { 288 binding.onBridgeReloaded() 289 }.onFailure { 290 scriptRuntime.logger.error("Failed to call onBridgeConnected for binding ${binding.name}", it) 291 } 292 } 293 } 294 295 callFunction("module.onBridgeConnected", reloaded) 296 } 297 298 @Suppress("UNCHECKED_CAST") 299 fun <T : Any> getBinding(clazz: KClass<T>): T? { 300 return moduleBindings.values.find { clazz.isInstance(it) } as? T 301 } 302 303 private fun argsToString(args: Array<out Any?>?): String { 304 return args?.joinToString(" ") { 305 when (it) { 306 is Wrapper -> it.unwrap().let { value -> 307 if (value is Throwable) value.message + "\n" + value.stackTraceToString() 308 else value.toString() 309 } 310 else -> it.toString() 311 } 312 } ?: "null" 313 } 314 }