feat(scripting): module system

This commit is contained in:
rhunk
2023-12-24 17:29:19 +01:00
parent 7d6978f961
commit 392cd95dac
22 changed files with 315 additions and 129 deletions

View File

@ -118,7 +118,7 @@ class RemoteSideContext(
} }
scriptManager.runtime.eachModule { scriptManager.runtime.eachModule {
callFunction("module.onManagerLoad", androidContext) callFunction("module.onSnapEnhanceLoad", androidContext)
} }
} }

View File

@ -268,7 +268,7 @@ class ModDatabase(
version = cursor.getStringOrNull("version")!!, version = cursor.getStringOrNull("version")!!,
description = cursor.getStringOrNull("description"), description = cursor.getStringOrNull("description"),
author = cursor.getStringOrNull("author"), author = cursor.getStringOrNull("author"),
grantPermissions = null grantedPermissions = emptyList()
) )
) )
} }

View File

@ -11,8 +11,8 @@ import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface
import me.rhunk.snapenhance.common.scripting.impl.ConfigTransactionType import me.rhunk.snapenhance.common.scripting.impl.ConfigTransactionType
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
import me.rhunk.snapenhance.scripting.impl.IPCListeners import me.rhunk.snapenhance.scripting.impl.IPCListeners
import me.rhunk.snapenhance.scripting.impl.RemoteManagerIPC import me.rhunk.snapenhance.scripting.impl.ManagerIPC
import me.rhunk.snapenhance.scripting.impl.RemoteScriptConfig import me.rhunk.snapenhance.scripting.impl.ManagerScriptConfig
import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager import me.rhunk.snapenhance.scripting.impl.ui.InterfaceManager
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
@ -21,7 +21,9 @@ import kotlin.system.exitProcess
class RemoteScriptManager( class RemoteScriptManager(
val context: RemoteSideContext, val context: RemoteSideContext,
) : IScripting.Stub() { ) : IScripting.Stub() {
val runtime = ScriptRuntime(context.androidContext, context.log) val runtime = ScriptRuntime(context.androidContext, context.log).apply {
scripting = this@RemoteScriptManager
}
private var autoReloadListener: AutoReloadListener? = null private var autoReloadListener: AutoReloadListener? = null
private val autoReloadHandler by lazy { private val autoReloadHandler by lazy {
@ -61,11 +63,11 @@ class RemoteScriptManager(
fun init() { fun init() {
runtime.buildModuleObject = { module -> runtime.buildModuleObject = { module ->
module.extras["ipc"] = RemoteManagerIPC(module.moduleInfo, context.log, ipcListeners) module.registerBindings(
module.extras["im"] = InterfaceManager(module.moduleInfo, context.log) ManagerIPC(ipcListeners),
module.extras["config"] = RemoteScriptConfig(this@RemoteScriptManager, module.moduleInfo, context.log).also { InterfaceManager(),
it.load() ManagerScriptConfig(this@RemoteScriptManager)
} )
} }
sync() sync()
@ -74,12 +76,20 @@ class RemoteScriptManager(
} }
} }
fun loadScript(name: String) { fun getModulePath(name: String): String? {
val content = getScriptContent(name) ?: return return cachedModuleInfo.entries.find { it.value.name == name }?.key
}
fun loadScript(path: String) {
val content = getScriptContent(path) ?: return
if (context.config.root.scripting.autoReload.getNullable() != null) { if (context.config.root.scripting.autoReload.getNullable() != null) {
autoReloadHandler.addFile(getScriptsFolder()?.findFile(name) ?: return) autoReloadHandler.addFile(getScriptsFolder()?.findFile(path) ?: return)
} }
runtime.load(name, content) runtime.load(path, content)
}
fun unloadScript(scriptPath: String) {
runtime.unload(scriptPath)
} }
private fun <R> getScriptInputStream(name: String, callback: (InputStream?) -> R): R { private fun <R> getScriptInputStream(name: String, callback: (InputStream?) -> R): R {
@ -140,7 +150,7 @@ class RemoteScriptManager(
value: String?, value: String?,
save: Boolean save: Boolean
): String? { ): String? {
val scriptConfig = runtime.getModuleByName(module ?: return null)?.extras?.get("config") as? ConfigInterface ?: return null.also { val scriptConfig = runtime.getModuleByName(module ?: return null)?.getBinding(ConfigInterface::class) ?: return null.also {
context.log.warn("Failed to get config interface for $module") context.log.warn("Failed to get config interface for $module")
} }
val transactionType = ConfigTransactionType.fromKey(action) val transactionType = ConfigTransactionType.fromKey(action)
@ -154,7 +164,7 @@ class RemoteScriptManager(
ConfigTransactionType.SET -> set(key ?: return@runCatching null, value, save) ConfigTransactionType.SET -> set(key ?: return@runCatching null, value, save)
ConfigTransactionType.SAVE -> save() ConfigTransactionType.SAVE -> save()
ConfigTransactionType.LOAD -> load() ConfigTransactionType.LOAD -> load()
ConfigTransactionType.DELETE -> delete() ConfigTransactionType.DELETE -> deleteConfig()
else -> {} else -> {}
} }
null null

View File

@ -2,17 +2,13 @@ package me.rhunk.snapenhance.scripting.impl
import android.os.DeadObjectException import android.os.DeadObjectException
import me.rhunk.snapenhance.bridge.scripting.IPCListener import me.rhunk.snapenhance.bridge.scripting.IPCListener
import me.rhunk.snapenhance.common.logger.AbstractLogger
import me.rhunk.snapenhance.common.scripting.impl.IPCInterface import me.rhunk.snapenhance.common.scripting.impl.IPCInterface
import me.rhunk.snapenhance.common.scripting.impl.Listener import me.rhunk.snapenhance.common.scripting.impl.Listener
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
typealias IPCListeners = ConcurrentHashMap<String, MutableMap<String, MutableSet<IPCListener>>> // channel, eventName -> listeners typealias IPCListeners = ConcurrentHashMap<String, MutableMap<String, MutableSet<IPCListener>>> // channel, eventName -> listeners
class RemoteManagerIPC( class ManagerIPC(
private val moduleInfo: ModuleInfo,
private val logger: AbstractLogger,
private val ipcListeners: IPCListeners = ConcurrentHashMap(), private val ipcListeners: IPCListeners = ConcurrentHashMap(),
) : IPCInterface() { ) : IPCInterface() {
companion object { companion object {
@ -20,22 +16,22 @@ class RemoteManagerIPC(
} }
override fun on(eventName: String, listener: Listener) { override fun on(eventName: String, listener: Listener) {
onBroadcast(moduleInfo.name, eventName, listener) onBroadcast(context.moduleInfo.name, eventName, listener)
} }
override fun emit(eventName: String, vararg args: String?) { override fun emit(eventName: String, vararg args: String?) {
emit(moduleInfo.name, eventName, *args) emit(context.moduleInfo.name, eventName, *args)
} }
override fun onBroadcast(channel: String, eventName: String, listener: Listener) { override fun onBroadcast(channel: String, eventName: String, listener: Listener) {
ipcListeners.getOrPut(channel) { mutableMapOf() }.getOrPut(eventName) { mutableSetOf() }.add(object: IPCListener.Stub() { ipcListeners.getOrPut(channel) { mutableMapOf() }.getOrPut(eventName) { mutableSetOf() }.add(object: IPCListener.Stub() {
override fun onMessage(args: Array<out String?>) { override fun onMessage(args: Array<out String?>) {
try { try {
listener(args) listener(args.toList())
} catch (doe: DeadObjectException) { } catch (doe: DeadObjectException) {
ipcListeners[channel]?.get(eventName)?.remove(this) ipcListeners[channel]?.get(eventName)?.remove(this)
} catch (t: Throwable) { } catch (t: Throwable) {
logger.error("Failed to receive message for channel: $channel, event: $eventName", t, TAG) context.runtime.logger.error("Failed to receive message for channel: $channel, event: $eventName", t, TAG)
} }
} }
}) })
@ -48,7 +44,7 @@ class RemoteManagerIPC(
} catch (doe: DeadObjectException) { } catch (doe: DeadObjectException) {
ipcListeners[channel]?.get(eventName)?.remove(it) ipcListeners[channel]?.get(eventName)?.remove(it)
} catch (t: Throwable) { } catch (t: Throwable) {
logger.error("Failed to send message for channel: $channel, event: $eventName", t, TAG) context.runtime.logger.error("Failed to send message for channel: $channel, event: $eventName", t, TAG)
} }
} }
} }

View File

@ -1,18 +1,14 @@
package me.rhunk.snapenhance.scripting.impl package me.rhunk.snapenhance.scripting.impl
import com.google.gson.JsonObject import com.google.gson.JsonObject
import me.rhunk.snapenhance.common.logger.AbstractLogger
import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
import me.rhunk.snapenhance.scripting.RemoteScriptManager import me.rhunk.snapenhance.scripting.RemoteScriptManager
import java.io.File import java.io.File
class RemoteScriptConfig( class ManagerScriptConfig(
private val remoteScriptManager: RemoteScriptManager, private val remoteScriptManager: RemoteScriptManager
moduleInfo: ModuleInfo,
private val logger: AbstractLogger,
) : ConfigInterface() { ) : ConfigInterface() {
private val configFile = File(remoteScriptManager.getModuleDataFolder(moduleInfo.name), "config.json") private val configFile by lazy { File(remoteScriptManager.getModuleDataFolder(context.moduleInfo.name), "config.json") }
private var config = JsonObject() private var config = JsonObject()
override fun get(key: String, defaultValue: Any?): String? { override fun get(key: String, defaultValue: Any?): String? {
@ -46,12 +42,16 @@ class RemoteScriptConfig(
} }
config = remoteScriptManager.context.gson.fromJson(configFile.readText(), JsonObject::class.java) config = remoteScriptManager.context.gson.fromJson(configFile.readText(), JsonObject::class.java)
}.onFailure { }.onFailure {
logger.error("Failed to load config file", it) context.runtime.logger.error("Failed to load config file", it)
save() save()
} }
} }
override fun delete() { override fun deleteConfig() {
configFile.delete() configFile.delete()
} }
override fun onInit() {
load()
}
} }

View File

@ -1,13 +1,13 @@
package me.rhunk.snapenhance.scripting.impl.ui package me.rhunk.snapenhance.scripting.impl.ui
import me.rhunk.snapenhance.common.logger.AbstractLogger import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.common.scripting.bindings.BindingSide
import me.rhunk.snapenhance.common.scripting.ktx.contextScope
import me.rhunk.snapenhance.scripting.impl.ui.components.Node import me.rhunk.snapenhance.scripting.impl.ui.components.Node
import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType import me.rhunk.snapenhance.scripting.impl.ui.components.NodeType
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionNode
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType import me.rhunk.snapenhance.scripting.impl.ui.components.impl.ActionType
import me.rhunk.snapenhance.scripting.impl.ui.components.impl.RowColumnNode import me.rhunk.snapenhance.scripting.impl.ui.components.impl.RowColumnNode
import org.mozilla.javascript.Context
import org.mozilla.javascript.Function import org.mozilla.javascript.Function
import org.mozilla.javascript.annotations.JSFunction import org.mozilla.javascript.annotations.JSFunction
@ -73,27 +73,31 @@ class InterfaceBuilder {
class InterfaceManager( class InterfaceManager : AbstractBinding("interface-manager", BindingSide.MANAGER) {
private val moduleInfo: ModuleInfo,
private val logger: AbstractLogger
) {
private val interfaces = mutableMapOf<String, () -> InterfaceBuilder?>() private val interfaces = mutableMapOf<String, () -> InterfaceBuilder?>()
fun buildInterface(name: String): InterfaceBuilder? { fun buildInterface(name: String): InterfaceBuilder? {
return interfaces[name]?.invoke() return interfaces[name]?.invoke()
} }
override fun onDispose() {
interfaces.clear()
}
@Suppress("unused")
@JSFunction fun create(name: String, callback: Function) { @JSFunction fun create(name: String, callback: Function) {
interfaces[name] = { interfaces[name] = {
val interfaceBuilder = InterfaceBuilder() val interfaceBuilder = InterfaceBuilder()
runCatching { runCatching {
Context.enter() contextScope {
callback.call(Context.getCurrentContext(), callback, callback, arrayOf(interfaceBuilder)) callback.call(this, callback, callback, arrayOf(interfaceBuilder))
Context.exit() }
interfaceBuilder interfaceBuilder
}.onFailure { }.onFailure {
logger.error("Failed to create interface $name for ${moduleInfo.name}", it) context.runtime.logger.error("Failed to create interface $name for ${context.moduleInfo.name}", it)
}.getOrNull() }.getOrNull()
} }
} }
override fun getObject() = this
} }

View File

@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.LibraryBooks
import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.* import androidx.compose.material3.*
@ -14,6 +15,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -70,12 +72,27 @@ class ScriptsSection : Section() {
} }
Switch( Switch(
checked = enabled, checked = enabled,
onCheckedChange = { onCheckedChange = { isChecked ->
context.modDatabase.setScriptEnabled(script.name, it) context.modDatabase.setScriptEnabled(script.name, isChecked)
if (it) { enabled = isChecked
context.scriptManager.loadScript(script.name) runCatching {
val modulePath = context.scriptManager.getModulePath(script.name)!!
context.scriptManager.unloadScript(modulePath)
if (isChecked) {
context.scriptManager.loadScript(modulePath)
context.scriptManager.runtime.getModuleByName(script.name)
?.callFunction("module.onSnapEnhanceLoad")
context.shortToast("Loaded script ${script.name}")
} else {
context.shortToast("Unloaded script ${script.name}")
}
}.onFailure { throwable ->
enabled = !isChecked
("Failed to ${if (isChecked) "enable" else "disable"} script").let {
context.log.error(it, throwable)
context.shortToast(it)
}
} }
enabled = it
} }
) )
} }
@ -130,7 +147,7 @@ class ScriptsSection : Section() {
val settingsInterface = remember { val settingsInterface = remember {
val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null val module = context.scriptManager.runtime.getModuleByName(script.name) ?: return@remember null
runCatching { runCatching {
(module.extras["im"] as? InterfaceManager)?.buildInterface("settings") (module.getBinding(InterfaceManager::class))?.buildInterface("settings")
}.onFailure { }.onFailure {
settingsError = it settingsError = it
}.getOrNull() }.getOrNull()
@ -228,4 +245,18 @@ class ScriptsSection : Section() {
) )
} }
} }
@Composable
override fun TopBarActions(rowScope: RowScope) {
rowScope.apply {
IconButton(onClick = {
context.androidContext.startActivity(Intent(Intent.ACTION_VIEW).apply {
data = "https://github.com/SnapEnhance/docs".toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK
})
}) {
Icon(imageVector = Icons.Default.LibraryBooks, contentDescription = "Documentation")
}
}
}
} }

View File

@ -5,6 +5,8 @@ enum class LogChannel(
val shortName: String val shortName: String
) { ) {
CORE("SnapEnhanceCore", "core"), CORE("SnapEnhanceCore", "core"),
COMMON("SnapEnhanceCommon", "common"),
SCRIPTING("Scripting", "scripting"),
NATIVE("SnapEnhanceNative", "native"), NATIVE("SnapEnhanceNative", "native"),
MANAGER("SnapEnhanceManager", "manager"), MANAGER("SnapEnhanceManager", "manager"),
XPOSED("LSPosed-Bridge", "xposed"); XPOSED("LSPosed-Bridge", "xposed");

View File

@ -2,6 +2,8 @@ package me.rhunk.snapenhance.common.scripting
import android.os.Handler import android.os.Handler
import android.widget.Toast 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.contextScope
import me.rhunk.snapenhance.common.scripting.ktx.putFunction import me.rhunk.snapenhance.common.scripting.ktx.putFunction
import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject 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.Undefined
import org.mozilla.javascript.Wrapper import org.mozilla.javascript.Wrapper
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import kotlin.reflect.KClass
class JSModule( class JSModule(
val scriptRuntime: ScriptRuntime, val scriptRuntime: ScriptRuntime,
val moduleInfo: ModuleInfo, val moduleInfo: ModuleInfo,
val content: String, val content: String,
) { ) {
val extras = mutableMapOf<String, Any>() private val moduleBindings = mutableMapOf<String, AbstractBinding>()
private lateinit var moduleObject: ScriptableObject private lateinit var moduleObject: ScriptableObject
private val moduleBindingContext by lazy {
BindingsContext(
moduleInfo = moduleInfo,
runtime = scriptRuntime
)
}
fun load(block: ScriptableObject.() -> Unit) { fun load(block: ScriptableObject.() -> Unit) {
contextScope { contextScope {
val classLoader = scriptRuntime.androidContext.classLoader val classLoader = scriptRuntime.androidContext.classLoader
@ -33,7 +43,7 @@ class JSModule(
putConst("author", this, moduleInfo.author) putConst("author", this, moduleInfo.author)
putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion) putConst("minSnapchatVersion", this, moduleInfo.minSnapchatVersion)
putConst("minSEVersion", this, moduleInfo.minSEVersion) putConst("minSEVersion", this, moduleInfo.minSEVersion)
putConst("grantPermissions", this, moduleInfo.grantPermissions) putConst("grantedPermissions", this, moduleInfo.grantedPermissions)
}) })
}) })
@ -62,12 +72,16 @@ class JSModule(
moduleObject.putFunction("findClass") { moduleObject.putFunction("findClass") {
val className = it?.get(0).toString() 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 -> moduleObject.putFunction("type") { args ->
val className = args?.get(0).toString() val className = args?.get(0).toString()
val clazz = classLoader.loadClass(className) val clazz = runCatching { classLoader.loadClass(className) }.getOrNull() ?: return@putFunction Undefined.instance
scriptableObject("JavaClassWrapper") { scriptableObject("JavaClassWrapper") {
putFunction("newInstance") newInstance@{ args -> putFunction("newInstance") newInstance@{ args ->
@ -95,12 +109,12 @@ class JSModule(
} }
moduleObject.putFunction("logInfo") { args -> moduleObject.putFunction("logInfo") { args ->
scriptRuntime.logger.info(args?.joinToString(" ") { scriptRuntime.logger.info(argsToString(args))
when (it) { Undefined.instance
is Wrapper -> it.unwrap().toString() }
else -> it.toString()
} moduleObject.putFunction("logError") { args ->
} ?: "null") scriptRuntime.logger.error(argsToString(arrayOf(args?.get(0))), args?.get(1) as? Throwable ?: Throwable())
Undefined.instance Undefined.instance
} }
@ -116,16 +130,38 @@ class JSModule(
Undefined.instance Undefined.instance
} }
} }
block(moduleObject) 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) evaluateString(moduleObject, content, moduleInfo.name, 1, null)
} }
} }
fun unload() { fun unload() {
callFunction("module.onUnload") 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?) { 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"
}
} }

View File

@ -1,6 +1,7 @@
package me.rhunk.snapenhance.common.scripting package me.rhunk.snapenhance.common.scripting
import android.content.Context import android.content.Context
import me.rhunk.snapenhance.bridge.scripting.IScripting
import me.rhunk.snapenhance.common.logger.AbstractLogger import me.rhunk.snapenhance.common.logger.AbstractLogger
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
import org.mozilla.javascript.ScriptableObject import org.mozilla.javascript.ScriptableObject
@ -10,9 +11,13 @@ import java.io.InputStream
open class ScriptRuntime( open class ScriptRuntime(
val androidContext: Context, val androidContext: Context,
val logger: AbstractLogger, logger: AbstractLogger,
) { ) {
val logger = ScriptingLogger(logger)
lateinit var scripting: IScripting
var buildModuleObject: ScriptableObject.(JSModule) -> Unit = {} var buildModuleObject: ScriptableObject.(JSModule) -> Unit = {}
private val modules = mutableMapOf<String, JSModule>() private val modules = mutableMapOf<String, JSModule>()
fun eachModule(f: JSModule.() -> Unit) { fun eachModule(f: JSModule.() -> Unit) {
@ -55,7 +60,7 @@ open class ScriptRuntime(
author = properties["author"], author = properties["author"],
minSnapchatVersion = properties["minSnapchatVersion"]?.toLong(), minSnapchatVersion = properties["minSnapchatVersion"]?.toLong(),
minSEVersion = properties["minSEVersion"]?.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()) return readModuleInfo(inputStream.bufferedReader())
} }
fun reload(path: String, content: String) { fun unload(scriptPath: String) {
unload(path) val module = modules[scriptPath] ?: return
load(path, content) logger.info("Unloading module $scriptPath")
}
private fun unload(path: String) {
val module = modules[path] ?: return
module.unload() module.unload()
modules.remove(path) modules.remove(scriptPath)
} }
fun load(path: String, content: String): JSModule? { fun load(scriptPath: String, content: String): JSModule? {
logger.info("Loading module $path") logger.info("Loading module $scriptPath")
return runCatching { return runCatching {
JSModule( JSModule(
scriptRuntime = this, scriptRuntime = this,
@ -85,10 +86,10 @@ open class ScriptRuntime(
load { load {
buildModuleObject(this, this@apply) buildModuleObject(this, this@apply)
} }
modules[path] = this modules[scriptPath] = this
} }
}.onFailure { }.onFailure {
logger.error("Failed to load module $path", it) logger.error("Failed to load module $scriptPath", it)
}.getOrNull() }.getOrNull()
} }
} }

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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
)

View File

@ -1,5 +1,7 @@
package me.rhunk.snapenhance.common.scripting.impl 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 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 fun get(key: String): String? = get(key, null)
@JSFunction abstract fun get(key: String, defaultValue: Any?): String? @JSFunction abstract fun get(key: String, defaultValue: Any?): String?
@ -70,5 +73,7 @@ abstract class ConfigInterface {
@JSFunction abstract fun save() @JSFunction abstract fun save()
@JSFunction abstract fun load() @JSFunction abstract fun load()
@JSFunction abstract fun delete() @JSFunction abstract fun deleteConfig()
override fun getObject() = this
} }

View File

@ -1,8 +1,11 @@
package me.rhunk.snapenhance.common.scripting.impl 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 on(eventName: String, listener: Listener)
abstract fun onBroadcast(channel: String, eventName: String, listener: Listener) abstract fun onBroadcast(channel: String, eventName: String, listener: Listener)
@ -13,5 +16,8 @@ abstract class IPCInterface {
@Suppress("unused") @Suppress("unused")
fun emit(eventName: String) = emit(eventName, *emptyArray()) fun emit(eventName: String) = emit(eventName, *emptyArray())
@Suppress("unused") @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
} }

View File

@ -7,5 +7,5 @@ data class ModuleInfo(
val author: String? = null, val author: String? = null,
val minSnapchatVersion: Long? = null, val minSnapchatVersion: Long? = null,
val minSEVersion: Long? = null, val minSEVersion: Long? = null,
val grantPermissions: List<String>? = null, val grantedPermissions: List<String>,
) )

View File

@ -17,8 +17,8 @@ import me.rhunk.snapenhance.common.data.MessagingGroupInfo
import me.rhunk.snapenhance.core.bridge.BridgeClient import me.rhunk.snapenhance.core.bridge.BridgeClient
import me.rhunk.snapenhance.core.bridge.loadFromBridge import me.rhunk.snapenhance.core.bridge.loadFromBridge
import me.rhunk.snapenhance.core.data.SnapClassCache import me.rhunk.snapenhance.core.data.SnapClassCache
import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent
import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent import me.rhunk.snapenhance.core.event.events.impl.NativeUnaryCallEvent
import me.rhunk.snapenhance.core.event.events.impl.SnapWidgetBroadcastReceiveEvent
import me.rhunk.snapenhance.core.util.LSPatchUpdater import me.rhunk.snapenhance.core.util.LSPatchUpdater
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook import me.rhunk.snapenhance.core.util.hook.hook
@ -142,7 +142,7 @@ class SnapEnhance {
bridgeClient.registerMessagingBridge(messagingBridge) bridgeClient.registerMessagingBridge(messagingBridge)
features.init() features.init()
scriptRuntime.connect(bridgeClient.getScriptingInterface()) scriptRuntime.connect(bridgeClient.getScriptingInterface())
scriptRuntime.eachModule { callFunction("module.onBeforeApplicationLoad", androidContext) } scriptRuntime.eachModule { callFunction("module.onSnapApplicationLoad", androidContext) }
syncRemote() syncRemote()
} }
} }
@ -151,7 +151,7 @@ class SnapEnhance {
measureTimeMillis { measureTimeMillis {
with(appContext) { with(appContext) {
features.onActivityCreate() features.onActivityCreate()
scriptRuntime.eachModule { callFunction("module.onSnapActivity", mainActivity!!) } scriptRuntime.eachModule { callFunction("module.onSnapMainActivityCreate", mainActivity!!) }
} }
}.also { time -> }.also { time ->
appContext.log.verbose("onActivityCreate took $time") appContext.log.verbose("onActivityCreate took $time")

View File

@ -7,22 +7,21 @@ import me.rhunk.snapenhance.common.scripting.ScriptRuntime
import me.rhunk.snapenhance.core.ModContext import me.rhunk.snapenhance.core.ModContext
import me.rhunk.snapenhance.core.scripting.impl.CoreIPC import me.rhunk.snapenhance.core.scripting.impl.CoreIPC
import me.rhunk.snapenhance.core.scripting.impl.CoreScriptConfig import me.rhunk.snapenhance.core.scripting.impl.CoreScriptConfig
import me.rhunk.snapenhance.core.scripting.impl.ScriptHooker import me.rhunk.snapenhance.core.scripting.impl.CoreScriptHooker
class CoreScriptRuntime( class CoreScriptRuntime(
private val modContext: ModContext, private val modContext: ModContext,
logger: AbstractLogger, logger: AbstractLogger,
): ScriptRuntime(modContext.androidContext, logger) { ): ScriptRuntime(modContext.androidContext, logger) {
private val scriptHookers = mutableListOf<ScriptHooker>()
fun connect(scriptingInterface: IScripting) { fun connect(scriptingInterface: IScripting) {
scripting = scriptingInterface
scriptingInterface.apply { scriptingInterface.apply {
buildModuleObject = { module -> buildModuleObject = { module ->
module.extras["ipc"] = CoreIPC(this@apply, module.moduleInfo) module.registerBindings(
module.extras["hooker"] = ScriptHooker(module.moduleInfo, logger, androidContext.classLoader).also { CoreScriptConfig(),
scriptHookers.add(it) CoreIPC(),
} CoreScriptHooker(),
module.extras["config"] = CoreScriptConfig(this@apply, module.moduleInfo) )
} }
enabledScripts.forEach { path -> enabledScripts.forEach { path ->

View File

@ -1,32 +1,27 @@
package me.rhunk.snapenhance.core.scripting.impl package me.rhunk.snapenhance.core.scripting.impl
import me.rhunk.snapenhance.bridge.scripting.IPCListener import me.rhunk.snapenhance.bridge.scripting.IPCListener
import me.rhunk.snapenhance.bridge.scripting.IScripting
import me.rhunk.snapenhance.common.scripting.impl.IPCInterface import me.rhunk.snapenhance.common.scripting.impl.IPCInterface
import me.rhunk.snapenhance.common.scripting.impl.Listener import me.rhunk.snapenhance.common.scripting.impl.Listener
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
class CoreIPC( class CoreIPC : IPCInterface() {
private val scripting: IScripting,
private val moduleInfo: ModuleInfo
) : IPCInterface() {
override fun onBroadcast(channel: String, eventName: String, listener: Listener) { override fun onBroadcast(channel: String, eventName: String, listener: Listener) {
scripting.registerIPCListener(channel, eventName, object: IPCListener.Stub() { context.runtime.scripting.registerIPCListener(channel, eventName, object: IPCListener.Stub() {
override fun onMessage(args: Array<out String?>) { override fun onMessage(args: Array<out String?>) {
listener(args) listener(args.toList())
} }
}) })
} }
override fun on(eventName: String, listener: Listener) { override fun on(eventName: String, listener: Listener) {
onBroadcast(moduleInfo.name, eventName, listener) onBroadcast(context.moduleInfo.name, eventName, listener)
} }
override fun emit(eventName: String, vararg args: String?) { override fun emit(eventName: String, vararg args: String?) {
broadcast(moduleInfo.name, eventName, *args) broadcast(context.moduleInfo.name, eventName, *args)
} }
override fun broadcast(channel: String, eventName: String, vararg args: String?) { override fun broadcast(channel: String, eventName: String, vararg args: String?) {
scripting.sendIPCMessage(channel, eventName, args) context.runtime.scripting.sendIPCMessage(channel, eventName, args)
} }
} }

View File

@ -1,31 +1,26 @@
package me.rhunk.snapenhance.core.scripting.impl package me.rhunk.snapenhance.core.scripting.impl
import me.rhunk.snapenhance.bridge.scripting.IScripting
import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface import me.rhunk.snapenhance.common.scripting.impl.ConfigInterface
import me.rhunk.snapenhance.common.scripting.impl.ConfigTransactionType import me.rhunk.snapenhance.common.scripting.impl.ConfigTransactionType
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
class CoreScriptConfig( class CoreScriptConfig: ConfigInterface() {
private val scripting: IScripting,
private val moduleInfo: ModuleInfo
): ConfigInterface() {
override fun get(key: String, defaultValue: Any?): String? { override fun get(key: String, defaultValue: Any?): String? {
return scripting.configTransaction(moduleInfo.name, ConfigTransactionType.GET.key, key, defaultValue.toString(), false) return context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.GET.key, key, defaultValue.toString(), false)
} }
override fun set(key: String, value: Any?, save: Boolean) { override fun set(key: String, value: Any?, save: Boolean) {
scripting.configTransaction(moduleInfo.name, ConfigTransactionType.SET.key, key, value.toString(), save) context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.SET.key, key, value.toString(), save)
} }
override fun save() { override fun save() {
scripting.configTransaction(moduleInfo.name, ConfigTransactionType.SAVE.key, null, null, false) context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.SAVE.key, null, null, false)
} }
override fun load() { override fun load() {
scripting.configTransaction(moduleInfo.name, ConfigTransactionType.LOAD.key, null, null, false) context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.LOAD.key, null, null, false)
} }
override fun delete() { override fun deleteConfig() {
scripting.configTransaction(moduleInfo.name, ConfigTransactionType.DELETE.key, null, null, false) context.runtime.scripting.configTransaction(context.moduleInfo.name, ConfigTransactionType.DELETE.key, null, null, false)
} }
} }

View File

@ -1,8 +1,9 @@
package me.rhunk.snapenhance.core.scripting.impl package me.rhunk.snapenhance.core.scripting.impl
import me.rhunk.snapenhance.common.logger.AbstractLogger import me.rhunk.snapenhance.common.scripting.bindings.AbstractBinding
import me.rhunk.snapenhance.common.scripting.bindings.BindingSide
import me.rhunk.snapenhance.common.scripting.ktx.scriptableObject
import me.rhunk.snapenhance.common.scripting.toPrimitiveValue import me.rhunk.snapenhance.common.scripting.toPrimitiveValue
import me.rhunk.snapenhance.common.scripting.type.ModuleInfo
import me.rhunk.snapenhance.core.util.hook.HookAdapter import me.rhunk.snapenhance.core.util.hook.HookAdapter
import me.rhunk.snapenhance.core.util.hook.HookStage import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.Hooker import me.rhunk.snapenhance.core.util.hook.Hooker
@ -71,21 +72,20 @@ class ScriptHookCallback(
typealias HookCallback = (ScriptHookCallback) -> Unit typealias HookCallback = (ScriptHookCallback) -> Unit
typealias HookUnhook = () -> Unit typealias HookUnhook = () -> Unit
@Suppress("unused", "MemberVisibilityCanBePrivate") @Suppress("unused")
class ScriptHooker( class CoreScriptHooker: AbstractBinding("hooker", BindingSide.CORE) {
private val moduleInfo: ModuleInfo,
private val logger: AbstractLogger,
private val classLoader: ClassLoader
) {
private val hooks = mutableListOf<HookUnhook>() private val hooks = mutableListOf<HookUnhook>()
// -- search for class members val stage = scriptableObject {
putConst("BEFORE", this, "before")
putConst("AFTER", this, "after")
}
private fun findClassSafe(className: String): Class<*>? { private fun findClassSafe(className: String): Class<*>? {
return runCatching { return runCatching {
classLoader.loadClass(className) context.runtime.androidContext.classLoader.loadClass(className)
}.onFailure { }.onFailure {
logger.warn("Failed to load class $className") context.runtime.logger.warn("Failed to load class $className")
}.getOrNull() }.getOrNull()
} }
@ -158,4 +158,11 @@ class ScriptHooker(
fun hookAllConstructors(className: String, stage: String, callback: HookCallback) fun hookAllConstructors(className: String, stage: String, callback: HookCallback)
= findClassSafe(className)?.let { hookAllConstructors(it, stage, callback) } = findClassSafe(className)?.let { hookAllConstructors(it, stage, callback) }
override fun onDispose() {
hooks.forEach { it() }
hooks.clear()
}
override fun getObject() = this
} }