commit 53359170f6b9cb6d37f31eab835563db4355f4ff parent 82658d3ad9580aa5b2376e37762308977da12b19 Author: rhunk <101876869+rhunk@users.noreply.github.com> Date: Sat, 16 Sep 2023 17:15:11 +0200 feat: scripting base Diffstat:
28 files changed, 363 insertions(+), 90 deletions(-)
diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/LogManager.kt @@ -3,7 +3,9 @@ package me.rhunk.snapenhance import android.content.SharedPreferences import android.util.Log import com.google.gson.GsonBuilder -import me.rhunk.snapenhance.core.LogLevel +import me.rhunk.snapenhance.core.logger.AbstractLogger +import me.rhunk.snapenhance.core.logger.LogChannel +import me.rhunk.snapenhance.core.logger.LogLevel import java.io.File import java.io.OutputStream import java.io.RandomAccessFile @@ -100,7 +102,7 @@ class LogReader( class LogManager( private val remoteSideContext: RemoteSideContext -) { +): AbstractLogger(LogChannel.MANAGER) { companion object { private const val TAG = "SnapEnhanceManager" private val LOG_LIFETIME = 24.hours @@ -171,32 +173,32 @@ class LogManager( lineAddListener = { line -> it.incrementLineCount(); onAddLine(line) } } - fun debug(message: Any?, tag: String = TAG) { + override fun debug(message: Any?, tag: String) { internalLog(tag, LogLevel.DEBUG, message) } - fun error(message: Any?, tag: String = TAG) { + 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) { + override fun info(message: Any?, tag: String) { internalLog(tag, LogLevel.INFO, message) } - fun verbose(message: Any?, tag: String = TAG) { + override fun verbose(message: Any?, tag: String) { internalLog(tag, LogLevel.VERBOSE, message) } - fun warn(message: Any?, tag: String = TAG) { + override fun warn(message: Any?, tag: String) { internalLog(tag, LogLevel.WARN, message) } - fun assert(message: Any?, tag: String = TAG) { + override fun assert(message: Any?, tag: String) { internalLog(tag, LogLevel.ASSERT, message) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt b/app/src/main/kotlin/me/rhunk/snapenhance/RemoteSideContext.kt @@ -24,6 +24,7 @@ import me.rhunk.snapenhance.core.config.ModConfig import me.rhunk.snapenhance.download.DownloadTaskManager import me.rhunk.snapenhance.messaging.ModDatabase import me.rhunk.snapenhance.messaging.StreaksReminder +import me.rhunk.snapenhance.scripting.RemoteScriptManager import me.rhunk.snapenhance.ui.manager.MainActivity import me.rhunk.snapenhance.ui.manager.data.InstallationSummary import me.rhunk.snapenhance.ui.manager.data.ModInfo @@ -54,6 +55,7 @@ class RemoteSideContext( val modDatabase = ModDatabase(this) val streaksReminder = StreaksReminder(this) val log = LogManager(this) + val scriptManager = RemoteScriptManager(this) //used to load bitmoji selfies and download previews val imageLoader by lazy { @@ -75,6 +77,7 @@ class RemoteSideContext( val coroutineScope = CoroutineScope(Dispatchers.IO) fun reload() { + log.verbose("Loading RemoteSideContext") runCatching { config.loadFromContext(androidContext) translation.apply { @@ -88,6 +91,7 @@ class RemoteSideContext( downloadTaskManager.init(androidContext) modDatabase.init() streaksReminder.init() + scriptManager.init() }.onFailure { log.error("Failed to load RemoteSideContext", it) } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt @@ -3,7 +3,6 @@ package me.rhunk.snapenhance.bridge import android.app.Service import android.content.Intent import android.os.IBinder -import me.rhunk.snapenhance.core.LogLevel import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.core.bridge.types.BridgeFileType @@ -11,10 +10,11 @@ import me.rhunk.snapenhance.core.bridge.types.FileActionType import me.rhunk.snapenhance.core.bridge.wrapper.LocaleWrapper import me.rhunk.snapenhance.core.bridge.wrapper.MessageLoggerWrapper import me.rhunk.snapenhance.core.database.objects.FriendInfo +import me.rhunk.snapenhance.core.logger.LogLevel import me.rhunk.snapenhance.core.messaging.MessagingFriendInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo -import me.rhunk.snapenhance.download.DownloadProcessor import me.rhunk.snapenhance.core.util.SerializableDataObject +import me.rhunk.snapenhance.download.DownloadProcessor import kotlin.system.measureTimeMillis class BridgeService : Service() { @@ -168,5 +168,7 @@ class BridgeService : Service() { groups.map { SerializableDataObject.fromJson<MessagingGroupInfo>(it) } ) } + + override fun getScriptingInterface() = remoteSideContext.scriptManager } } diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt b/app/src/main/kotlin/me/rhunk/snapenhance/download/FFMpegProcessor.kt @@ -4,7 +4,7 @@ import com.arthenica.ffmpegkit.FFmpegKit import com.arthenica.ffmpegkit.FFmpegSession import com.arthenica.ffmpegkit.Level import kotlinx.coroutines.suspendCancellableCoroutine -import me.rhunk.snapenhance.core.LogLevel +import me.rhunk.snapenhance.core.logger.LogLevel import me.rhunk.snapenhance.LogManager import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.config.impl.DownloaderConfig diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/scripting/RemoteScriptManager.kt @@ -0,0 +1,43 @@ +package me.rhunk.snapenhance.scripting + +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import me.rhunk.snapenhance.RemoteSideContext +import me.rhunk.snapenhance.bridge.scripting.IScripting +import me.rhunk.snapenhance.bridge.scripting.ReloadListener + +class RemoteScriptManager( + private val context: RemoteSideContext, +) : IScripting.Stub() { + private val scriptRuntime = ScriptRuntime(context.log) + + private fun getScriptFolder() + = DocumentFile.fromTreeUri(context.androidContext, Uri.parse(context.config.root.scripting.moduleFolder.get())) + private fun hasHotReload() = context.config.root.scripting.hotReload.get() + + private val reloadListeners = mutableListOf<ReloadListener>() + + fun init() { + enabledScriptPaths.forEach { path -> + val content = getScriptContent(path) + scriptRuntime.load(path, content) + } + } + + override fun getEnabledScriptPaths(): List<String> { + val folder = getScriptFolder() ?: return emptyList() + return folder.listFiles().filter { it.name?.endsWith(".js") ?: false }.map { it.name!! } + } + + override fun getScriptContent(path: String): String { + val folder = getScriptFolder() ?: return "" + val file = folder.findFile(path) ?: return "" + return context.androidContext.contentResolver.openInputStream(file.uri)?.use { + it.readBytes().toString(Charsets.UTF_8) + } ?: "" + } + + override fun registerReloadListener(listener: ReloadListener) { + reloadListeners.add(listener) + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/Section.kt @@ -2,8 +2,8 @@ package me.rhunk.snapenhance.ui.manager import androidx.compose.foundation.layout.RowScope import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DataObject import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.Extension import androidx.compose.material.icons.filled.Group import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Stars @@ -13,10 +13,10 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import me.rhunk.snapenhance.RemoteSideContext -import me.rhunk.snapenhance.ui.manager.sections.home.HomeSection import me.rhunk.snapenhance.ui.manager.sections.NotImplemented import me.rhunk.snapenhance.ui.manager.sections.downloads.DownloadsSection import me.rhunk.snapenhance.ui.manager.sections.features.FeaturesSection +import me.rhunk.snapenhance.ui.manager.sections.home.HomeSection import me.rhunk.snapenhance.ui.manager.sections.social.SocialSection import kotlin.reflect.KClass @@ -45,9 +45,9 @@ enum class EnumSection( icon = Icons.Filled.Group, section = SocialSection::class ), - PLUGINS( - route = "plugins", - icon = Icons.Filled.Extension + SCRIPTS( + route = "scripts", + icon = Icons.Filled.DataObject ); companion object { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/manager/sections/home/HomeSubSection.kt @@ -53,8 +53,8 @@ import androidx.compose.ui.window.Dialog import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.core.LogChannels -import me.rhunk.snapenhance.core.LogLevel +import me.rhunk.snapenhance.core.logger.LogChannel +import me.rhunk.snapenhance.core.logger.LogLevel import me.rhunk.snapenhance.LogReader import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.action.EnumAction @@ -169,7 +169,7 @@ class HomeSubSection( ) Text( - text = LogChannels.fromChannel(line.tag)?.shortName ?: line.tag, + text = LogChannel.fromChannel(line.tag)?.shortName ?: line.tag, modifier = Modifier.padding(start = 4.dp), fontWeight = FontWeight.Light, fontSize = 10.sp, diff --git a/core/build.gradle.kts b/core/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(libs.gson) implementation(libs.okhttp) implementation(libs.androidx.documentfile) + implementation(libs.rhino) implementation(project(":mapper")) implementation(project(":native")) diff --git a/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl @@ -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(); } \ No newline at end of file diff --git a/core/src/main/aidl/me/rhunk/snapenhance/bridge/scripting/IScripting.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/scripting/IScripting.aidl @@ -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); +}+ \ No newline at end of file diff --git a/core/src/main/aidl/me/rhunk/snapenhance/bridge/scripting/ReloadListener.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/scripting/ReloadListener.aidl @@ -0,0 +1,5 @@ +package me.rhunk.snapenhance.bridge.scripting; + +oneway interface ReloadListener { + void reloadScript(String path, String content); +} diff --git a/core/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json @@ -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": { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt @@ -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)!! diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -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") diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/Logger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/Logger.kt @@ -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) } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/bridge/BridgeClient.kt @@ -148,4 +148,6 @@ class BridgeClient( fun setRule(targetUuid: String, type: MessagingRuleType, state: Boolean) = service.setRule(targetUuid, type.key, state) + + fun getScriptingInterface() = service.getScriptingInterface() } diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/RootConfig.kt @@ -14,4 +14,5 @@ class RootConfig : ConfigContainer() { val experimental = container("experimental", Experimental()) { icon = "Science"; addNotices(FeatureNotice.UNSTABLE) } + val scripting = container("scripting", Scripting()) { icon = "DataObject" } } \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Scripting.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/config/impl/Scripting.kt @@ -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) +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/logger/AbstractLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/logger/AbstractLogger.kt @@ -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) {} +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/logger/LogChannel.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/logger/LogChannel.kt @@ -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 } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/logger/LogLevel.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/logger/LogLevel.kt @@ -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 } + } + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/scripting/JSModule.kt b/core/src/main/kotlin/me/rhunk/snapenhance/scripting/JSModule.kt @@ -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() + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/scripting/ScriptRuntime.kt b/core/src/main/kotlin/me/rhunk/snapenhance/scripting/ScriptRuntime.kt @@ -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() + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/scripting/type/ModuleInfo.kt b/core/src/main/kotlin/me/rhunk/snapenhance/scripting/type/ModuleInfo.kt @@ -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, +)+ \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ dexlib2 = "2.5.2" androidx-documentfile = "1.1.0-alpha01" activity-ktx = "1.7.2" material3 = "1.1.1" +rhino = "1.7.14" [libraries] @@ -39,6 +40,7 @@ dexlib2 = { group = "org.smali", name = "dexlib2", version.ref = "dexlib2" } androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "androidx-documentfile" } androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity-ktx" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +rhino = { module = "org.mozilla:rhino", version.ref = "rhino" } [plugins] diff --git a/scripting/.gitignore b/scripting/.gitignore @@ -1 +0,0 @@ -build/- \ No newline at end of file diff --git a/scripting/build.gradle.kts b/scripting/build.gradle.kts @@ -1,17 +0,0 @@ -plugins { - alias(libs.plugins.androidLibrary) - alias(libs.plugins.kotlinAndroid) -} - -android { - namespace = rootProject.ext["applicationId"].toString() + ".scripting" - compileSdk = 34 - - kotlinOptions { - jvmTarget = "1.8" - } -} - -dependencies { - implementation(project(":core")) -}- \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts @@ -21,4 +21,3 @@ include(":core") include(":app") include(":mapper") include(":native") -include(":scripting")