feat: scripting base
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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_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": {
|
||||||
|
|||||||
@@ -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)!!
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" }
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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]
|
||||||
|
|||||||
1
scripting/.gitignore
vendored
1
scripting/.gitignore
vendored
@@ -1 +0,0 @@
|
|||||||
build/
|
|
||||||
@@ -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"))
|
|
||||||
}
|
|
||||||
@@ -21,4 +21,3 @@ include(":core")
|
|||||||
include(":app")
|
include(":app")
|
||||||
include(":mapper")
|
include(":mapper")
|
||||||
include(":native")
|
include(":native")
|
||||||
include(":scripting")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user