mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-12 05:07:46 +02:00
feat(scripting): module system
This commit is contained in:
@ -5,6 +5,8 @@ enum class LogChannel(
|
||||
val shortName: String
|
||||
) {
|
||||
CORE("SnapEnhanceCore", "core"),
|
||||
COMMON("SnapEnhanceCommon", "common"),
|
||||
SCRIPTING("Scripting", "scripting"),
|
||||
NATIVE("SnapEnhanceNative", "native"),
|
||||
MANAGER("SnapEnhanceManager", "manager"),
|
||||
XPOSED("LSPosed-Bridge", "xposed");
|
||||
|
@ -2,6 +2,8 @@ package me.rhunk.snapenhance.common.scripting
|
||||
|
||||
import android.os.Handler
|
||||
import android.widget.Toast
|
||||
import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding
|
||||
import me.rhunk.snapenhance.common.scripting.bindings.BindingsContext
|
||||
import me.rhunk.snapenhance.common.scripting.ktx.contextScope
|
||||
import me.rhunk.snapenhance.common.scripting.ktx.putFunction
|
||||
import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject
|
||||
@ -12,15 +14,23 @@ import org.mozilla.javascript.ScriptableObject
|
||||
import org.mozilla.javascript.Undefined
|
||||
import org.mozilla.javascript.Wrapper
|
||||
import java.lang.reflect.Modifier
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class JSModule(
|
||||
val scriptRuntime: ScriptRuntime,
|
||||
val moduleInfo: ModuleInfo,
|
||||
val content: String,
|
||||
) {
|
||||
val extras = mutableMapOf<String, Any>()
|
||||
private val moduleBindings = mutableMapOf<String, AbstractBinding>()
|
||||
private lateinit var moduleObject: ScriptableObject
|
||||
|
||||
private val moduleBindingContext by lazy {
|
||||
BindingsContext(
|
||||
moduleInfo = moduleInfo,
|
||||
runtime = scriptRuntime
|
||||
)
|
||||
}
|
||||
|
||||
fun load(block: ScriptableObject.() -> Unit) {
|
||||
contextScope {
|
||||
val classLoader = scriptRuntime.androidContext.classLoader
|
||||
@ -33,7 +43,7 @@ class JSModule(
|
||||
putConst("author", this, moduleInfo.author)
|
||||
putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion)
|
||||
putConst("minSEVersion", this, moduleInfo.minSEVersion)
|
||||
putConst("grantPermissions", this, moduleInfo.grantPermissions)
|
||||
putConst("grantedPermissions", this, moduleInfo.grantedPermissions)
|
||||
})
|
||||
})
|
||||
|
||||
@ -62,12 +72,16 @@ class JSModule(
|
||||
|
||||
moduleObject.putFunction("findClass") {
|
||||
val className = it?.get(0).toString()
|
||||
classLoader.loadClass(className)
|
||||
runCatching {
|
||||
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 clazz = classLoader.loadClass(className)
|
||||
val clazz = runCatching { classLoader.loadClass(className) }.getOrNull() ?: return@putFunction Undefined.instance
|
||||
|
||||
scriptableObject("JavaClassWrapper") {
|
||||
putFunction("newInstance") newInstance@{ args ->
|
||||
@ -95,12 +109,12 @@ class JSModule(
|
||||
}
|
||||
|
||||
moduleObject.putFunction("logInfo") { args ->
|
||||
scriptRuntime.logger.info(args?.joinToString(" ") {
|
||||
when (it) {
|
||||
is Wrapper -> it.unwrap().toString()
|
||||
else -> it.toString()
|
||||
}
|
||||
} ?: "null")
|
||||
scriptRuntime.logger.info(argsToString(args))
|
||||
Undefined.instance
|
||||
}
|
||||
|
||||
moduleObject.putFunction("logError") { args ->
|
||||
scriptRuntime.logger.error(argsToString(arrayOf(args?.get(0))), args?.get(1) as? Throwable ?: Throwable())
|
||||
Undefined.instance
|
||||
}
|
||||
|
||||
@ -116,16 +130,38 @@ class JSModule(
|
||||
Undefined.instance
|
||||
}
|
||||
}
|
||||
|
||||
block(moduleObject)
|
||||
extras.forEach { (key, value) ->
|
||||
moduleObject.putConst(key, moduleObject, value)
|
||||
|
||||
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()
|
||||
moduleBindings[bindingName]?.getObject()
|
||||
}
|
||||
|
||||
evaluateString(moduleObject, content, moduleInfo.name, 1, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun unload() {
|
||||
callFunction("module.onUnload")
|
||||
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?) {
|
||||
@ -143,4 +179,25 @@ class JSModule(
|
||||
}
|
||||
}
|
||||
}
|
||||
fun registerBindings(vararg bindings: AbstractBinding) {
|
||||
bindings.forEach {
|
||||
moduleBindings[it.name] = it.apply {
|
||||
context = moduleBindingContext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> getBinding(clazz: KClass<T>): T? {
|
||||
return moduleBindings.values.find { clazz.isInstance(it) } as? T
|
||||
}
|
||||
|
||||
private fun argsToString(args: Array<out Any?>?): String {
|
||||
return args?.joinToString(" ") {
|
||||
when (it) {
|
||||
is Wrapper -> it.unwrap().toString()
|
||||
else -> it.toString()
|
||||
}
|
||||
} ?: "null"
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package me.rhunk.snapenhance.common.scripting
|
||||
|
||||
import android.content.Context
|
||||
import me.rhunk.snapenhance.bridge.scripting.IScripting
|
||||
import me.rhunk.snapenhance.common.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
|
||||
import org.mozilla.javascript.ScriptableObject
|
||||
@ -10,9 +11,13 @@ import java.io.InputStream
|
||||
|
||||
open class ScriptRuntime(
|
||||
val androidContext: Context,
|
||||
val logger: AbstractLogger,
|
||||
logger: AbstractLogger,
|
||||
) {
|
||||
val logger = ScriptingLogger(logger)
|
||||
|
||||
lateinit var scripting: IScripting
|
||||
var buildModuleObject: ScriptableObject.(JSModule) -> Unit = {}
|
||||
|
||||
private val modules = mutableMapOf<String, JSModule>()
|
||||
|
||||
fun eachModule(f: JSModule.() -> Unit) {
|
||||
@ -55,7 +60,7 @@ open class ScriptRuntime(
|
||||
author = properties["author"],
|
||||
minSnapchatVersion = properties["minSnapchatVersion"]?.toLong(),
|
||||
minSEVersion = properties["minSEVersion"]?.toLong(),
|
||||
grantPermissions = properties["permissions"]?.split(",")?.map { it.trim() },
|
||||
grantedPermissions = properties["permissions"]?.split(",")?.map { it.trim() } ?: emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
@ -63,19 +68,15 @@ open class ScriptRuntime(
|
||||
return readModuleInfo(inputStream.bufferedReader())
|
||||
}
|
||||
|
||||
fun reload(path: String, content: String) {
|
||||
unload(path)
|
||||
load(path, content)
|
||||
}
|
||||
|
||||
private fun unload(path: String) {
|
||||
val module = modules[path] ?: return
|
||||
fun unload(scriptPath: String) {
|
||||
val module = modules[scriptPath] ?: return
|
||||
logger.info("Unloading module $scriptPath")
|
||||
module.unload()
|
||||
modules.remove(path)
|
||||
modules.remove(scriptPath)
|
||||
}
|
||||
|
||||
fun load(path: String, content: String): JSModule? {
|
||||
logger.info("Loading module $path")
|
||||
fun load(scriptPath: String, content: String): JSModule? {
|
||||
logger.info("Loading module $scriptPath")
|
||||
return runCatching {
|
||||
JSModule(
|
||||
scriptRuntime = this,
|
||||
@ -85,10 +86,10 @@ open class ScriptRuntime(
|
||||
load {
|
||||
buildModuleObject(this, this@apply)
|
||||
}
|
||||
modules[path] = this
|
||||
modules[scriptPath] = this
|
||||
}
|
||||
}.onFailure {
|
||||
logger.error("Failed to load module $path", it)
|
||||
logger.error("Failed to load module $scriptPath", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package me.rhunk.snapenhance.common.scripting
|
||||
|
||||
import me.rhunk.snapenhance.common.logger.AbstractLogger
|
||||
import me.rhunk.snapenhance.common.logger.LogChannel
|
||||
|
||||
class ScriptingLogger(
|
||||
private val logger: AbstractLogger
|
||||
) {
|
||||
companion object {
|
||||
private val TAG = LogChannel.SCRIPTING.channel
|
||||
}
|
||||
|
||||
fun debug(message: Any?, tag: String = TAG) {
|
||||
logger.debug(message, tag)
|
||||
}
|
||||
|
||||
fun error(message: Any?, tag: String = TAG) {
|
||||
logger.error(message, tag)
|
||||
}
|
||||
|
||||
fun error(message: Any?, throwable: Throwable, tag: String = TAG) {
|
||||
logger.error(message, throwable, tag)
|
||||
}
|
||||
|
||||
fun info(message: Any?, tag: String = TAG) {
|
||||
logger.info(message, tag)
|
||||
}
|
||||
|
||||
fun verbose(message: Any?, tag: String = TAG) {
|
||||
logger.verbose(message, tag)
|
||||
}
|
||||
|
||||
fun warn(message: Any?, tag: String = TAG) {
|
||||
logger.warn(message, tag)
|
||||
}
|
||||
|
||||
fun assert(message: Any?, tag: String = TAG) {
|
||||
logger.assert(message, tag)
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package me.rhunk.snapenhance.common.scripting.bindings
|
||||
|
||||
abstract class AbstractBinding(
|
||||
val name: String,
|
||||
val side: BindingSide
|
||||
) {
|
||||
lateinit var context: BindingsContext
|
||||
|
||||
open fun onInit() {}
|
||||
|
||||
open fun onDispose() {}
|
||||
|
||||
abstract fun getObject(): Any
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package me.rhunk.snapenhance.common.scripting.bindings
|
||||
|
||||
enum class BindingSide(
|
||||
val key: String
|
||||
) {
|
||||
COMMON("common"),
|
||||
CORE("core"),
|
||||
MANAGER("manager");
|
||||
|
||||
companion object {
|
||||
fun fromKey(key: String): BindingSide {
|
||||
return entries.firstOrNull { it.key == key } ?: COMMON
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package me.rhunk.snapenhance.common.scripting.bindings
|
||||
|
||||
import me.rhunk.snapenhance.common.scripting.ScriptRuntime
|
||||
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
|
||||
|
||||
class BindingsContext(
|
||||
val moduleInfo: ModuleInfo,
|
||||
val runtime: ScriptRuntime
|
||||
)
|
@ -1,5 +1,7 @@
|
||||
package me.rhunk.snapenhance.common.scripting.impl
|
||||
|
||||
import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding
|
||||
import me.rhunk.snapenhance.common.scripting.bindings.BindingSide
|
||||
import org.mozilla.javascript.annotations.JSFunction
|
||||
|
||||
|
||||
@ -18,7 +20,8 @@ enum class ConfigTransactionType(
|
||||
}
|
||||
|
||||
|
||||
abstract class ConfigInterface {
|
||||
@Suppress("unused")
|
||||
abstract class ConfigInterface : AbstractBinding("config", BindingSide.COMMON) {
|
||||
@JSFunction fun get(key: String): String? = get(key, null)
|
||||
@JSFunction abstract fun get(key: String, defaultValue: Any?): String?
|
||||
|
||||
@ -70,5 +73,7 @@ abstract class ConfigInterface {
|
||||
|
||||
@JSFunction abstract fun save()
|
||||
@JSFunction abstract fun load()
|
||||
@JSFunction abstract fun delete()
|
||||
@JSFunction abstract fun deleteConfig()
|
||||
|
||||
override fun getObject() = this
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
package me.rhunk.snapenhance.common.scripting.impl
|
||||
|
||||
typealias Listener = (Array<out String?>) -> Unit
|
||||
import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding
|
||||
import me.rhunk.snapenhance.common.scripting.bindings.BindingSide
|
||||
|
||||
abstract class IPCInterface {
|
||||
typealias Listener = (List<String?>) -> Unit
|
||||
|
||||
abstract class IPCInterface : AbstractBinding("ipc", BindingSide.COMMON) {
|
||||
abstract fun on(eventName: String, listener: Listener)
|
||||
|
||||
abstract fun onBroadcast(channel: String, eventName: String, listener: Listener)
|
||||
@ -13,5 +16,8 @@ abstract class IPCInterface {
|
||||
@Suppress("unused")
|
||||
fun emit(eventName: String) = emit(eventName, *emptyArray())
|
||||
@Suppress("unused")
|
||||
fun emit(channel: String, eventName: String) = broadcast(channel, eventName)
|
||||
fun broadcast(channel: String, eventName: String) =
|
||||
broadcast(channel, eventName, *emptyArray())
|
||||
|
||||
override fun getObject() = this
|
||||
}
|
@ -7,5 +7,5 @@ data class ModuleInfo(
|
||||
val author: String? = null,
|
||||
val minSnapchatVersion: Long? = null,
|
||||
val minSEVersion: Long? = null,
|
||||
val grantPermissions: List<String>? = null,
|
||||
val grantedPermissions: List<String>,
|
||||
)
|
Reference in New Issue
Block a user