mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-12 05:07:46 +02:00
feat: scripting base
This commit is contained in:
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package me.rhunk.snapenhance.bridge.scripting;
|
||||
|
||||
oneway interface ReloadListener {
|
||||
void reloadScript(String path, String content);
|
||||
}
|
@ -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": {
|
||||
|
@ -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)!!
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
}
|
@ -148,4 +148,6 @@ class BridgeClient(
|
||||
|
||||
fun setRule(targetUuid: String, type: MessagingRuleType, state: Boolean)
|
||||
= service.setRule(targetUuid, type.key, state)
|
||||
|
||||
fun getScriptingInterface() = service.getScriptingInterface()
|
||||
}
|
||||
|
@ -14,4 +14,5 @@ class RootConfig : ConfigContainer() {
|
||||
val experimental = container("experimental", Experimental()) {
|
||||
icon = "Science"; addNotices(FeatureNotice.UNSTABLE)
|
||||
}
|
||||
val scripting = container("scripting", Scripting()) { icon = "DataObject" }
|
||||
}
|
@ -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)
|
||||
}
|
@ -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) {}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
Reference in New Issue
Block a user