feat: scripting base

This commit is contained in:
rhunk
2023-09-16 17:15:11 +02:00
parent 82658d3ad9
commit 53359170f6
28 changed files with 354 additions and 88 deletions

View File

@ -3,6 +3,7 @@ package me.rhunk.snapenhance.bridge;
import java.util.List;
import me.rhunk.snapenhance.bridge.DownloadCallback;
import me.rhunk.snapenhance.bridge.SyncCallback;
import me.rhunk.snapenhance.bridge.scripting.IScripting;
interface BridgeInterface {
/**
@ -85,4 +86,6 @@ interface BridgeInterface {
* @param friends list of friends (MessagingFriendInfo as json string)
*/
oneway void passGroupsAndFriends(in List<String> groups, in List<String> friends);
IScripting getScriptingInterface();
}

View File

@ -0,0 +1,11 @@
package me.rhunk.snapenhance.bridge.scripting;
import me.rhunk.snapenhance.bridge.scripting.ReloadListener;
interface IScripting {
List<String> getEnabledScriptPaths();
String getScriptContent(String path);
void registerReloadListener(ReloadListener listener);
}

View File

@ -0,0 +1,5 @@
package me.rhunk.snapenhance.bridge.scripting;
oneway interface ReloadListener {
void reloadScript(String path, String content);
}

View File

@ -28,7 +28,7 @@
"home_debug": "Debug",
"home_logs": "Logs",
"social": "Social",
"plugins": "Plugins"
"scripts": "Scripts"
},
"sections": {
"home": {
@ -521,6 +521,20 @@
"description": "Enables unreleased/beta Snapchat Plus features\nMight not work on older Snapchat versions"
}
}
},
"scripting": {
"name": "Scripting",
"description": "Run custom scripts to extend SnapEnhance",
"properties": {
"module_folder": {
"name": "Module Folder",
"description": "The folder where the scripts are located"
},
"hot_reload": {
"name": "Hot Reload",
"description": "Automatically reloads scripts when they change"
}
}
}
},
"options": {

View File

@ -25,6 +25,7 @@ import me.rhunk.snapenhance.manager.impl.ActionManager
import me.rhunk.snapenhance.manager.impl.FeatureManager
import me.rhunk.snapenhance.nativelib.NativeConfig
import me.rhunk.snapenhance.nativelib.NativeLib
import me.rhunk.snapenhance.scripting.ScriptRuntime
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.reflect.KClass
@ -59,6 +60,7 @@ class ModContext {
val messageSender = MessageSender(this)
val classCache get() = SnapEnhance.classCache
val resources: Resources get() = androidContext.resources
val scriptRuntime by lazy { ScriptRuntime(log) }
fun <T : Feature> feature(featureClass: KClass<T>): T {
return features.get(featureClass)!!

View File

@ -7,6 +7,7 @@ import android.content.pm.PackageManager
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import me.rhunk.snapenhance.bridge.SyncCallback
import me.rhunk.snapenhance.bridge.scripting.ReloadListener
import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.Logger
import me.rhunk.snapenhance.core.bridge.BridgeClient
@ -113,6 +114,23 @@ class SnapEnhance {
if (!mappings.isMappingsLoaded()) return
features.init()
syncRemote()
bridgeClient.getScriptingInterface().apply {
registerReloadListener(object: ReloadListener.Stub() {
override fun reloadScript(path: String, content: String) {
scriptRuntime.reload(path, content)
}
})
enabledScriptPaths.forEach { path ->
runCatching {
scriptRuntime.load(path, getScriptContent(path))
}.onFailure {
log.error("Failed to load script $path", it)
}
}
}
}
}.also { time ->
appContext.log.verbose("init took $time")

View File

@ -4,54 +4,17 @@ import android.annotation.SuppressLint
import android.util.Log
import de.robv.android.xposed.XposedBridge
import me.rhunk.snapenhance.core.bridge.BridgeClient
import me.rhunk.snapenhance.core.logger.AbstractLogger
import me.rhunk.snapenhance.core.logger.LogChannel
import me.rhunk.snapenhance.hook.HookStage
import me.rhunk.snapenhance.hook.hook
enum class LogLevel(
val letter: String,
val shortName: String,
val priority: Int = Log.INFO
) {
VERBOSE("V", "verbose", Log.VERBOSE),
DEBUG("D", "debug", Log.DEBUG),
INFO("I", "info", Log.INFO),
WARN("W", "warn", Log.WARN),
ERROR("E", "error", Log.ERROR),
ASSERT("A", "assert", Log.ASSERT);
companion object {
fun fromLetter(letter: String): LogLevel? {
return values().find { it.letter == letter }
}
fun fromShortName(shortName: String): LogLevel? {
return values().find { it.shortName == shortName }
}
fun fromPriority(priority: Int): LogLevel? {
return values().find { it.priority == priority }
}
}
}
enum class LogChannels(val channel: String, val shortName: String) {
CORE("SnapEnhanceCore", "core"),
NATIVE("SnapEnhanceNative", "native"),
MANAGER("SnapEnhanceManager", "manager"),
XPOSED("LSPosed-Bridge", "xposed");
companion object {
fun fromChannel(channel: String): LogChannels? {
return values().find { it.channel == channel }
}
}
}
import me.rhunk.snapenhance.core.logger.LogLevel
@SuppressLint("PrivateApi")
class Logger(
private val bridgeClient: BridgeClient
) {
): AbstractLogger(LogChannel.CORE) {
companion object {
private const val TAG = "SnapEnhanceCore"
@ -104,20 +67,20 @@ class Logger(
}
}
fun debug(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.DEBUG, message)
override fun debug(message: Any?, tag: String) = internalLog(tag, LogLevel.DEBUG, message)
fun error(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.ERROR, message)
override fun error(message: Any?, tag: String) = internalLog(tag, LogLevel.ERROR, message)
fun error(message: Any?, throwable: Throwable, tag: String = TAG) {
override fun error(message: Any?, throwable: Throwable, tag: String) {
internalLog(tag, LogLevel.ERROR, message)
internalLog(tag, LogLevel.ERROR, throwable.stackTraceToString())
}
fun info(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.INFO, message)
override fun info(message: Any?, tag: String) = internalLog(tag, LogLevel.INFO, message)
fun verbose(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.VERBOSE, message)
override fun verbose(message: Any?, tag: String) = internalLog(tag, LogLevel.VERBOSE, message)
fun warn(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.WARN, message)
override fun warn(message: Any?, tag: String) = internalLog(tag, LogLevel.WARN, message)
fun assert(message: Any?, tag: String = TAG) = internalLog(tag, LogLevel.ASSERT, message)
override fun assert(message: Any?, tag: String) = internalLog(tag, LogLevel.ASSERT, message)
}

View File

@ -148,4 +148,6 @@ class BridgeClient(
fun setRule(targetUuid: String, type: MessagingRuleType, state: Boolean)
= service.setRule(targetUuid, type.key, state)
fun getScriptingInterface() = service.getScriptingInterface()
}

View File

@ -14,4 +14,5 @@ class RootConfig : ConfigContainer() {
val experimental = container("experimental", Experimental()) {
icon = "Science"; addNotices(FeatureNotice.UNSTABLE)
}
val scripting = container("scripting", Scripting()) { icon = "DataObject" }
}

View File

@ -0,0 +1,9 @@
package me.rhunk.snapenhance.core.config.impl
import me.rhunk.snapenhance.core.config.ConfigContainer
import me.rhunk.snapenhance.core.config.ConfigFlag
class Scripting : ConfigContainer() {
val moduleFolder = string("module_folder", "modules") { addFlags(ConfigFlag.FOLDER) }
val hotReload = boolean("hot_reload", false)
}

View File

@ -0,0 +1,22 @@
package me.rhunk.snapenhance.core.logger
abstract class AbstractLogger(
logChannel: LogChannel,
) {
private val TAG = logChannel.shortName
open fun debug(message: Any?, tag: String = TAG) {}
open fun error(message: Any?, tag: String = TAG) {}
open fun error(message: Any?, throwable: Throwable, tag: String = TAG) {}
open fun info(message: Any?, tag: String = TAG) {}
open fun verbose(message: Any?, tag: String = TAG) {}
open fun warn(message: Any?, tag: String = TAG) {}
open fun assert(message: Any?, tag: String = TAG) {}
}

View File

@ -0,0 +1,17 @@
package me.rhunk.snapenhance.core.logger
enum class LogChannel(
val channel: String,
val shortName: String
) {
CORE("SnapEnhanceCore", "core"),
NATIVE("SnapEnhanceNative", "native"),
MANAGER("SnapEnhanceManager", "manager"),
XPOSED("LSPosed-Bridge", "xposed");
companion object {
fun fromChannel(channel: String): LogChannel? {
return values().find { it.channel == channel }
}
}
}

View File

@ -0,0 +1,30 @@
package me.rhunk.snapenhance.core.logger
import android.util.Log
enum class LogLevel(
val letter: String,
val shortName: String,
val priority: Int = Log.INFO
) {
VERBOSE("V", "verbose", Log.VERBOSE),
DEBUG("D", "debug", Log.DEBUG),
INFO("I", "info", Log.INFO),
WARN("W", "warn", Log.WARN),
ERROR("E", "error", Log.ERROR),
ASSERT("A", "assert", Log.ASSERT);
companion object {
fun fromLetter(letter: String): LogLevel? {
return values().find { it.letter == letter }
}
fun fromShortName(shortName: String): LogLevel? {
return values().find { it.shortName == shortName }
}
fun fromPriority(priority: Int): LogLevel? {
return values().find { it.priority == priority }
}
}
}

View File

@ -0,0 +1,53 @@
package me.rhunk.snapenhance.scripting
import me.rhunk.snapenhance.core.logger.AbstractLogger
import me.rhunk.snapenhance.scripting.type.ModuleInfo
import org.mozilla.javascript.Context
import org.mozilla.javascript.FunctionObject
import org.mozilla.javascript.ScriptableObject
class JSModule(
val moduleInfo: ModuleInfo,
val content: String,
) {
lateinit var logger: AbstractLogger
private lateinit var scope: ScriptableObject
companion object {
@JvmStatic
fun logDebug(message: String) {
println(message)
}
}
fun load() {
val context = Context.enter()
context.optimizationLevel = -1
scope = context.initSafeStandardObjects()
scope.putConst("module", scope, moduleInfo)
scope.putConst("logDebug", scope,
FunctionObject("logDebug", JSModule::class.java.getDeclaredMethod("logDebug", String::class.java), scope)
)
context.evaluateString(scope, content, moduleInfo.name, 1, null)
}
fun unload() {
val context = Context.enter()
context.evaluateString(scope, "if (typeof module.onUnload === 'function') module.onUnload();", "onUnload", 1, null)
Context.exit()
}
fun callOnCoreLoad() {
val context = Context.enter()
context.evaluateString(scope, "if (typeof module.onCoreLoad === 'function') module.onCoreLoad();", "onCoreLoad", 1, null)
Context.exit()
}
fun callOnManagerLoad() {
val context = Context.enter()
context.evaluateString(scope, "if (typeof module.onManagerLoad === 'function') module.onManagerLoad();", "onManagerLoad", 1, null)
Context.exit()
}
}

View File

@ -0,0 +1,70 @@
package me.rhunk.snapenhance.scripting
import me.rhunk.snapenhance.core.logger.AbstractLogger
import me.rhunk.snapenhance.scripting.type.ModuleInfo
import java.io.BufferedReader
import java.io.ByteArrayInputStream
class ScriptRuntime(
private val logger: AbstractLogger,
) {
private val modules = mutableMapOf<String, JSModule>()
private fun readModuleInfo(reader: BufferedReader): ModuleInfo {
val header = reader.readLine()
if (!header.startsWith("// ==SE_module==")) {
throw Exception("Invalid module header")
}
val properties = mutableMapOf<String, String>()
while (true) {
val line = reader.readLine()
if (line.startsWith("// ==/SE_module==")) {
break
}
val split = line.replaceFirst("//", "").split(":")
if (split.size != 2) {
throw Exception("Invalid module property")
}
properties[split[0].trim()] = split[1].trim()
}
return ModuleInfo(
name = properties["name"] ?: throw Exception("Missing module name"),
version = properties["version"] ?: throw Exception("Missing module version"),
description = properties["description"],
author = properties["author"],
minSnapchatVersion = properties["minSnapchatVersion"]?.toLong(),
minSEVersion = properties["minSEVersion"]?.toLong(),
grantPermissions = properties["permissions"]?.split(",")?.map { it.trim() },
)
}
fun reload(path: String, content: String) {
unload(path)
load(path, content)
}
private fun unload(path: String) {
val module = modules[path] ?: return
module.unload()
module.load()
modules.remove(path)
}
fun load(path: String, content: String): JSModule? {
logger.info("Loading module $path")
return runCatching {
JSModule(
moduleInfo = readModuleInfo(ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)).bufferedReader()),
content = content,
).apply {
logger = this@ScriptRuntime.logger
load()
modules[path] = this
}
}.onFailure {
logger.error("Failed to load module $path", it)
}.getOrNull()
}
}

View File

@ -0,0 +1,11 @@
package me.rhunk.snapenhance.scripting.type
data class ModuleInfo(
val name: String,
val version: String,
val description: String? = null,
val author: String? = null,
val minSnapchatVersion: Long? = null,
val minSEVersion: Long? = null,
val grantPermissions: List<String>? = null,
)