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,7 +3,9 @@ package me.rhunk.snapenhance
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log import android.util.Log
import com.google.gson.GsonBuilder 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.File
import java.io.OutputStream import java.io.OutputStream
import java.io.RandomAccessFile import java.io.RandomAccessFile
@@ -100,7 +102,7 @@ class LogReader(
class LogManager( class LogManager(
private val remoteSideContext: RemoteSideContext private val remoteSideContext: RemoteSideContext
) { ): AbstractLogger(LogChannel.MANAGER) {
companion object { companion object {
private const val TAG = "SnapEnhanceManager" private const val TAG = "SnapEnhanceManager"
private val LOG_LIFETIME = 24.hours private val LOG_LIFETIME = 24.hours
@@ -171,32 +173,32 @@ class LogManager(
lineAddListener = { line -> it.incrementLineCount(); onAddLine(line) } 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) internalLog(tag, LogLevel.DEBUG, message)
} }
fun error(message: Any?, tag: String = TAG) { override fun error(message: Any?, tag: String) {
internalLog(tag, LogLevel.ERROR, message) 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, message)
internalLog(tag, LogLevel.ERROR, throwable.stackTraceToString()) 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) internalLog(tag, LogLevel.INFO, message)
} }
fun verbose(message: Any?, tag: String = TAG) { override fun verbose(message: Any?, tag: String) {
internalLog(tag, LogLevel.VERBOSE, message) internalLog(tag, LogLevel.VERBOSE, message)
} }
fun warn(message: Any?, tag: String = TAG) { override fun warn(message: Any?, tag: String) {
internalLog(tag, LogLevel.WARN, message) internalLog(tag, LogLevel.WARN, message)
} }
fun assert(message: Any?, tag: String = TAG) { override fun assert(message: Any?, tag: String) {
internalLog(tag, LogLevel.ASSERT, message) internalLog(tag, LogLevel.ASSERT, message)
} }

View File

@@ -24,6 +24,7 @@ import me.rhunk.snapenhance.core.config.ModConfig
import me.rhunk.snapenhance.download.DownloadTaskManager import me.rhunk.snapenhance.download.DownloadTaskManager
import me.rhunk.snapenhance.messaging.ModDatabase import me.rhunk.snapenhance.messaging.ModDatabase
import me.rhunk.snapenhance.messaging.StreaksReminder 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.MainActivity
import me.rhunk.snapenhance.ui.manager.data.InstallationSummary import me.rhunk.snapenhance.ui.manager.data.InstallationSummary
import me.rhunk.snapenhance.ui.manager.data.ModInfo import me.rhunk.snapenhance.ui.manager.data.ModInfo
@@ -54,6 +55,7 @@ class RemoteSideContext(
val modDatabase = ModDatabase(this) val modDatabase = ModDatabase(this)
val streaksReminder = StreaksReminder(this) val streaksReminder = StreaksReminder(this)
val log = LogManager(this) val log = LogManager(this)
val scriptManager = RemoteScriptManager(this)
//used to load bitmoji selfies and download previews //used to load bitmoji selfies and download previews
val imageLoader by lazy { val imageLoader by lazy {
@@ -75,6 +77,7 @@ class RemoteSideContext(
val coroutineScope = CoroutineScope(Dispatchers.IO) val coroutineScope = CoroutineScope(Dispatchers.IO)
fun reload() { fun reload() {
log.verbose("Loading RemoteSideContext")
runCatching { runCatching {
config.loadFromContext(androidContext) config.loadFromContext(androidContext)
translation.apply { translation.apply {
@@ -88,6 +91,7 @@ class RemoteSideContext(
downloadTaskManager.init(androidContext) downloadTaskManager.init(androidContext)
modDatabase.init() modDatabase.init()
streaksReminder.init() streaksReminder.init()
scriptManager.init()
}.onFailure { }.onFailure {
log.error("Failed to load RemoteSideContext", it) log.error("Failed to load RemoteSideContext", it)
} }

View File

@@ -3,7 +3,6 @@ package me.rhunk.snapenhance.bridge
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import me.rhunk.snapenhance.core.LogLevel
import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.RemoteSideContext
import me.rhunk.snapenhance.SharedContextHolder import me.rhunk.snapenhance.SharedContextHolder
import me.rhunk.snapenhance.core.bridge.types.BridgeFileType 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.LocaleWrapper
import me.rhunk.snapenhance.core.bridge.wrapper.MessageLoggerWrapper import me.rhunk.snapenhance.core.bridge.wrapper.MessageLoggerWrapper
import me.rhunk.snapenhance.core.database.objects.FriendInfo 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.MessagingFriendInfo
import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo import me.rhunk.snapenhance.core.messaging.MessagingGroupInfo
import me.rhunk.snapenhance.download.DownloadProcessor
import me.rhunk.snapenhance.core.util.SerializableDataObject import me.rhunk.snapenhance.core.util.SerializableDataObject
import me.rhunk.snapenhance.download.DownloadProcessor
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class BridgeService : Service() { class BridgeService : Service() {
@@ -168,5 +168,7 @@ class BridgeService : Service() {
groups.map { SerializableDataObject.fromJson<MessagingGroupInfo>(it) } groups.map { SerializableDataObject.fromJson<MessagingGroupInfo>(it) }
) )
} }
override fun getScriptingInterface() = remoteSideContext.scriptManager
} }
} }

View File

@@ -4,7 +4,7 @@ import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.FFmpegSession import com.arthenica.ffmpegkit.FFmpegSession
import com.arthenica.ffmpegkit.Level import com.arthenica.ffmpegkit.Level
import kotlinx.coroutines.suspendCancellableCoroutine 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.LogManager
import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.Logger
import me.rhunk.snapenhance.core.config.impl.DownloaderConfig import me.rhunk.snapenhance.core.config.impl.DownloaderConfig

View File

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

View File

@@ -2,8 +2,8 @@ package me.rhunk.snapenhance.ui.manager
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons 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.Download
import androidx.compose.material.icons.filled.Extension
import androidx.compose.material.icons.filled.Group import androidx.compose.material.icons.filled.Group
import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Stars import androidx.compose.material.icons.filled.Stars
@@ -13,10 +13,10 @@ import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import me.rhunk.snapenhance.RemoteSideContext 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.NotImplemented
import me.rhunk.snapenhance.ui.manager.sections.downloads.DownloadsSection 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.features.FeaturesSection
import me.rhunk.snapenhance.ui.manager.sections.home.HomeSection
import me.rhunk.snapenhance.ui.manager.sections.social.SocialSection import me.rhunk.snapenhance.ui.manager.sections.social.SocialSection
import kotlin.reflect.KClass import kotlin.reflect.KClass
@@ -45,9 +45,9 @@ enum class EnumSection(
icon = Icons.Filled.Group, icon = Icons.Filled.Group,
section = SocialSection::class section = SocialSection::class
), ),
PLUGINS( SCRIPTS(
route = "plugins", route = "scripts",
icon = Icons.Filled.Extension icon = Icons.Filled.DataObject
); );
companion object { companion object {

View File

@@ -53,8 +53,8 @@ import androidx.compose.ui.window.Dialog
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.rhunk.snapenhance.Constants import me.rhunk.snapenhance.Constants
import me.rhunk.snapenhance.core.LogChannels import me.rhunk.snapenhance.core.logger.LogChannel
import me.rhunk.snapenhance.core.LogLevel import me.rhunk.snapenhance.core.logger.LogLevel
import me.rhunk.snapenhance.LogReader import me.rhunk.snapenhance.LogReader
import me.rhunk.snapenhance.RemoteSideContext import me.rhunk.snapenhance.RemoteSideContext
import me.rhunk.snapenhance.action.EnumAction import me.rhunk.snapenhance.action.EnumAction
@@ -169,7 +169,7 @@ class HomeSubSection(
) )
Text( Text(
text = LogChannels.fromChannel(line.tag)?.shortName ?: line.tag, text = LogChannel.fromChannel(line.tag)?.shortName ?: line.tag,
modifier = Modifier.padding(start = 4.dp), modifier = Modifier.padding(start = 4.dp),
fontWeight = FontWeight.Light, fontWeight = FontWeight.Light,
fontSize = 10.sp, fontSize = 10.sp,

View File

@@ -39,6 +39,7 @@ dependencies {
implementation(libs.gson) implementation(libs.gson)
implementation(libs.okhttp) implementation(libs.okhttp)
implementation(libs.androidx.documentfile) implementation(libs.androidx.documentfile)
implementation(libs.rhino)
implementation(project(":mapper")) implementation(project(":mapper"))
implementation(project(":native")) implementation(project(":native"))

View File

@@ -3,6 +3,7 @@ package me.rhunk.snapenhance.bridge;
import java.util.List; import java.util.List;
import me.rhunk.snapenhance.bridge.DownloadCallback; import me.rhunk.snapenhance.bridge.DownloadCallback;
import me.rhunk.snapenhance.bridge.SyncCallback; import me.rhunk.snapenhance.bridge.SyncCallback;
import me.rhunk.snapenhance.bridge.scripting.IScripting;
interface BridgeInterface { interface BridgeInterface {
/** /**
@@ -85,4 +86,6 @@ interface BridgeInterface {
* @param friends list of friends (MessagingFriendInfo as json string) * @param friends list of friends (MessagingFriendInfo as json string)
*/ */
oneway void passGroupsAndFriends(in List<String> groups, in List<String> friends); 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_debug": "Debug",
"home_logs": "Logs", "home_logs": "Logs",
"social": "Social", "social": "Social",
"plugins": "Plugins" "scripts": "Scripts"
}, },
"sections": { "sections": {
"home": { "home": {
@@ -521,6 +521,20 @@
"description": "Enables unreleased/beta Snapchat Plus features\nMight not work on older Snapchat versions" "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": { "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.manager.impl.FeatureManager
import me.rhunk.snapenhance.nativelib.NativeConfig import me.rhunk.snapenhance.nativelib.NativeConfig
import me.rhunk.snapenhance.nativelib.NativeLib import me.rhunk.snapenhance.nativelib.NativeLib
import me.rhunk.snapenhance.scripting.ScriptRuntime
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.reflect.KClass import kotlin.reflect.KClass
@@ -59,6 +60,7 @@ class ModContext {
val messageSender = MessageSender(this) val messageSender = MessageSender(this)
val classCache get() = SnapEnhance.classCache val classCache get() = SnapEnhance.classCache
val resources: Resources get() = androidContext.resources val resources: Resources get() = androidContext.resources
val scriptRuntime by lazy { ScriptRuntime(log) }
fun <T : Feature> feature(featureClass: KClass<T>): T { fun <T : Feature> feature(featureClass: KClass<T>): T {
return features.get(featureClass)!! return features.get(featureClass)!!

View File

@@ -7,6 +7,7 @@ import android.content.pm.PackageManager
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import me.rhunk.snapenhance.bridge.SyncCallback import me.rhunk.snapenhance.bridge.SyncCallback
import me.rhunk.snapenhance.bridge.scripting.ReloadListener
import me.rhunk.snapenhance.core.BuildConfig import me.rhunk.snapenhance.core.BuildConfig
import me.rhunk.snapenhance.core.Logger import me.rhunk.snapenhance.core.Logger
import me.rhunk.snapenhance.core.bridge.BridgeClient import me.rhunk.snapenhance.core.bridge.BridgeClient
@@ -113,6 +114,23 @@ class SnapEnhance {
if (!mappings.isMappingsLoaded()) return if (!mappings.isMappingsLoaded()) return
features.init() features.init()
syncRemote() 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 -> }.also { time ->
appContext.log.verbose("init took $time") appContext.log.verbose("init took $time")

View File

@@ -4,54 +4,17 @@ import android.annotation.SuppressLint
import android.util.Log import android.util.Log
import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedBridge
import me.rhunk.snapenhance.core.bridge.BridgeClient 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.HookStage
import me.rhunk.snapenhance.hook.hook import me.rhunk.snapenhance.hook.hook
import me.rhunk.snapenhance.core.logger.LogLevel
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 }
}
}
}
@SuppressLint("PrivateApi") @SuppressLint("PrivateApi")
class Logger( class Logger(
private val bridgeClient: BridgeClient private val bridgeClient: BridgeClient
) { ): AbstractLogger(LogChannel.CORE) {
companion object { companion object {
private const val TAG = "SnapEnhanceCore" 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, message)
internalLog(tag, LogLevel.ERROR, throwable.stackTraceToString()) 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) fun setRule(targetUuid: String, type: MessagingRuleType, state: Boolean)
= service.setRule(targetUuid, type.key, state) = service.setRule(targetUuid, type.key, state)
fun getScriptingInterface() = service.getScriptingInterface()
} }

View File

@@ -14,4 +14,5 @@ class RootConfig : ConfigContainer() {
val experimental = container("experimental", Experimental()) { val experimental = container("experimental", Experimental()) {
icon = "Science"; addNotices(FeatureNotice.UNSTABLE) 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,
)

View File

@@ -18,6 +18,7 @@ dexlib2 = "2.5.2"
androidx-documentfile = "1.1.0-alpha01" androidx-documentfile = "1.1.0-alpha01"
activity-ktx = "1.7.2" activity-ktx = "1.7.2"
material3 = "1.1.1" material3 = "1.1.1"
rhino = "1.7.14"
[libraries] [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-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "androidx-documentfile" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity-ktx" } androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity-ktx" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
[plugins] [plugins]

View File

@@ -1 +0,0 @@
build/

View File

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

View File

@@ -21,4 +21,3 @@ include(":core")
include(":app") include(":app")
include(":mapper") include(":mapper")
include(":native") include(":native")
include(":scripting")