feat(experimental): composer hooks

This commit is contained in:
rhunk
2024-04-21 23:25:45 +02:00
parent c8195c5250
commit 17ad43ee92
12 changed files with 467 additions and 12 deletions

View File

@@ -0,0 +1,44 @@
const deviceBridge = require('composer_core/src/DeviceBridge');
if (LOADER_CONFIG.logPrefix) {
function internalLog(logLevel, args) {
deviceBridge.copyToClipBoard(LOADER_CONFIG.logPrefix + "|" + logLevel + "|" + Array.from(args).join(" "));
}
console.log = function() {
internalLog("info", arguments);
}
console.error = function() {
internalLog("error", arguments);
}
console.warn = function() {
internalLog("warn", arguments);
}
console.info = function() {
internalLog("info", arguments);
}
console.debug = function() {
internalLog("debug", arguments);
}
console.stacktrace = function() {
return new Error().stack;
}
}
if (LOADER_CONFIG.bypassCameraRollLimit) {
((module) => {
module.MultiSelectClickHandler = new Proxy(module.MultiSelectClickHandler, {
construct: function(target, args) {
args[1].selectionLimit = 9999999;
return new target(...args);
},
});
})(require('memories_ui/src/clickhandlers/MultiSelectClickHandler'))
}
console.info("loader.js loaded");

View File

@@ -156,6 +156,7 @@ class ModContext(
disableBitmoji = config.experimental.nativeHooks.disableBitmoji.get(),
disableMetrics = config.global.disableMetrics.get(),
hookAssetOpen = config.experimental.disableComposerModules.get().isNotEmpty(),
composerHooks = config.experimental.nativeHooks.composerHooks.globalState == true
)
)
}

View File

@@ -4,11 +4,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import me.rhunk.snapenhance.core.ModContext
import me.rhunk.snapenhance.core.features.impl.COFOverride
import me.rhunk.snapenhance.core.features.impl.ConfigurationOverride
import me.rhunk.snapenhance.core.features.impl.MixerStories
import me.rhunk.snapenhance.core.features.impl.OperaViewerParamsOverride
import me.rhunk.snapenhance.core.features.impl.ScopeSync
import me.rhunk.snapenhance.core.features.impl.*
import me.rhunk.snapenhance.core.features.impl.downloader.MediaDownloader
import me.rhunk.snapenhance.core.features.impl.downloader.ProfilePictureDownloader
import me.rhunk.snapenhance.core.features.impl.experiments.*
@@ -16,7 +12,6 @@ import me.rhunk.snapenhance.core.features.impl.global.*
import me.rhunk.snapenhance.core.features.impl.messaging.*
import me.rhunk.snapenhance.core.features.impl.spying.HalfSwipeNotifier
import me.rhunk.snapenhance.core.features.impl.spying.MessageLogger
import me.rhunk.snapenhance.core.features.impl.FriendMutationObserver
import me.rhunk.snapenhance.core.features.impl.spying.StealthMode
import me.rhunk.snapenhance.core.features.impl.tweaks.*
import me.rhunk.snapenhance.core.features.impl.ui.*
@@ -131,6 +126,7 @@ class FeatureManager(
HideActiveMusic(),
AutoOpenSnaps(),
CustomStreaksExpirationFormat(),
ComposerHooks(),
)
initializeFeatures()

View File

@@ -0,0 +1,189 @@
package me.rhunk.snapenhance.core.features.impl.experiments
import android.content.ClipData
import android.content.ClipboardManager
import android.widget.FrameLayout
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material3.*
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.gson.JsonObject
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.common.ui.AppMaterialTheme
import me.rhunk.snapenhance.common.ui.createComposeAlertDialog
import me.rhunk.snapenhance.common.ui.createComposeView
import me.rhunk.snapenhance.core.features.Feature
import me.rhunk.snapenhance.core.features.FeatureLoadParams
import me.rhunk.snapenhance.core.util.hook.HookStage
import me.rhunk.snapenhance.core.util.hook.hook
import me.rhunk.snapenhance.nativelib.NativeLib
import kotlin.random.Random
import kotlin.random.nextInt
class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.ACTIVITY_CREATE_SYNC) {
private val config by lazy { context.config.experimental.nativeHooks.composerHooks }
private val composerConsole by lazy {
createComposeAlertDialog(context.mainActivity!!) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
var result by remember { mutableStateOf("") }
var codeContent by remember { mutableStateOf("") }
Text("Composer Console", fontSize = 18.sp, fontWeight = FontWeight.Bold)
TextField(
modifier = Modifier.fillMaxWidth(),
textStyle = TextStyle.Default.copy(fontSize = 12.sp),
value = codeContent,
placeholder = { Text("Enter your JS code here:") },
onValueChange = {
codeContent = it
}
)
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
context.log.verbose("input: $codeContent", "ComposerConsole")
result = "Running..."
context.coroutineScope.launch {
result = (context.native.composerEval("""
(() => {
try {
$codeContent
} catch (e) {
return e.toString()
}
})()
""".trimIndent()) ?: "(no result)").also {
context.log.verbose("result: $it", "ComposerConsole")
}
}
}
) {
Text("Run")
}
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Text(result)
}
}
}
}
private fun injectConsole() {
val root = context.mainActivity!!.findViewById<FrameLayout>(android.R.id.content)
root.post {
root.addView(createComposeView(root.context) {
AppMaterialTheme {
FilledIconButton(
onClick = {
composerConsole.show()
},
modifier = Modifier.padding(top = 100.dp, end = 16.dp)
) {
Icon(Icons.Default.BugReport, contentDescription = "Debug Console")
}
}
}.apply {
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT).apply {
gravity = android.view.Gravity.TOP or android.view.Gravity.END
}
})
}
}
private fun loadHooks() {
val loaderConfig = JsonObject()
if (config.composerLogs.get()) {
val logPrefix = Random.nextInt(100000..999999).toString()
val logTag = "ComposerLogs"
ClipboardManager::class.java.hook("setPrimaryClip", HookStage.BEFORE) { param ->
val clipData = param.arg<ClipData>(0).takeIf { it.itemCount == 1 } ?: return@hook
val logText = clipData.getItemAt(0).text ?: return@hook
if (!logText.startsWith("$logPrefix|")) return@hook
val logContainer = logText.removePrefix("$logPrefix|").toString()
val logType = logContainer.substringBefore("|")
val content = logContainer.substringAfter("|")
when (logType) {
"verbose" -> context.log.verbose(content, logTag)
"info" -> context.log.info(content, logTag)
"debug" -> context.log.debug(content, logTag)
"warn" -> context.log.warn(content, logTag)
"error" -> context.log.error(content, logTag)
else -> context.log.info(logContainer, logTag)
}
param.setResult(null)
}
loaderConfig.addProperty("logPrefix", logPrefix)
}
if (config.bypassCameraRollLimit.get()) {
loaderConfig.addProperty("bypassCameraRollLimit", true)
}
val loaderScript = context.scriptRuntime.scripting.getScriptContent("composer/loader.js") ?: run {
context.log.error("Failed to load composer loader script")
return
}
val hookResult = context.native.composerEval("""
(() => { try { const LOADER_CONFIG = $loaderConfig; $loaderScript
} catch (e) {
return e.toString() + "\n" + e.stack;
}
return "success";
})()
""".trimIndent().trim())
if (hookResult != "success") {
context.shortToast(("Composer loader failed : $hookResult").also {
context.log.error(it)
})
}
if (config.composerConsole.get()) {
injectConsole()
}
}
override fun onActivityCreate() {
if (!NativeLib.initialized || config.globalState != true) return
var composerThreadTask: (() -> Unit)? = null
findClass("com.snap.composer.callable.ComposerFunctionNative").hook("nativePerform", HookStage.BEFORE) {
composerThreadTask?.invoke()
composerThreadTask = null
}
context.coroutineScope.launch {
context.native.waitForComposer()
composerThreadTask = ::loadHooks
}
}
}