commit 2bb055f4642bdcc02c24117be6794ac73c420e90
parent cce64bb246958555c9a37a519f474a892216bc11
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Mon, 22 Apr 2024 18:50:28 +0200

refactor(composer_hooks): module export

Diffstat:
Mcore/src/main/assets/composer/loader.js | 44+++++++++++++-------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Acore/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/composer/ComposerMarshaller.kt | 14++++++++++++++
3 files changed, 108 insertions(+), 65 deletions(-)

diff --git a/core/src/main/assets/composer/loader.js b/core/src/main/assets/composer/loader.js @@ -1,37 +1,22 @@ -const deviceBridge = require('composer_core/src/DeviceBridge'); +const callExport = require('composer_core/src/DeviceBridge')[EXPORTED_FUNCTION_NAME]; -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); - } +if (!callExport) { + return "No export function found"; +} - console.info = function() { - internalLog("info", arguments); - } +const config = callExport("getConfig"); - console.debug = function() { - internalLog("debug", arguments); - } +if (config.composerLogs) { + ["log", "error", "warn", "info", "debug"].forEach(method => { + console[method] = (...args) => callExport("log", method, Array.from(args).join(" ")); + }) - console.stacktrace = function() { - return new Error().stack; - } + console.stacktrace = () => new Error().stack; + console.info("loader.js loaded"); } -if (LOADER_CONFIG.bypassCameraRollLimit) { - ((module) => { +if (config.bypassCameraRollLimit) { + (module => { module.MultiSelectClickHandler = new Proxy(module.MultiSelectClickHandler, { construct: function(target, args, newTarget) { args[1].selectionLimit = 9999999; @@ -40,5 +25,3 @@ if (LOADER_CONFIG.bypassCameraRollLimit) { }); })(require('memories_ui/src/clickhandlers/MultiSelectClickHandler')) } - -console.info("loader.js loaded");- \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt @@ -1,7 +1,5 @@ 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 @@ -12,7 +10,11 @@ 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.material3.Button +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TextField import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -22,7 +24,6 @@ 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 @@ -30,13 +31,17 @@ 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.Hooker import me.rhunk.snapenhance.core.util.hook.hook +import me.rhunk.snapenhance.core.wrapper.impl.composer.ComposerMarshaller import me.rhunk.snapenhance.nativelib.NativeLib +import java.lang.reflect.Proxy +import kotlin.math.absoluteValue 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 exportedFunctionName = Random.nextInt().absoluteValue.toString(16) private val composerConsole by lazy { createComposeAlertDialog(context.mainActivity!!) { @@ -114,46 +119,75 @@ class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.ACT } } - 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) + private fun newComposerFunction(block: (composerMarshaller: ComposerMarshaller) -> Boolean): Any { + val composerFunctionClass = findClass("com.snap.composer.callable.ComposerFunction") + return Proxy.newProxyInstance( + composerFunctionClass.classLoader, + arrayOf(composerFunctionClass) + ) { _, method, args -> + if (method.name != "perform") return@newProxyInstance null + block(ComposerMarshaller(args?.get(0) ?: return@newProxyInstance false)) + } + } + + private fun handleExportCall(composerMarshaller: ComposerMarshaller): Boolean { + val argc = composerMarshaller.getSize() + if (argc < 1) return false + val action = composerMarshaller.getUntyped(0) as? String ?: return false + + when (action) { + "getConfig" -> { + composerMarshaller.pushUntyped( + HashMap<String, Any>().apply { + put("bypassCameraRollLimit", config.bypassCameraRollLimit.get()) + put("composerConsole", config.composerConsole.get()) + put("composerLogs", config.composerLogs.get()) + } + ) + } + "showToast" -> { + if (argc < 2) return false + val message = composerMarshaller.getUntyped(1) as? String ?: return false + context.shortToast(message) + } + "log" -> { + if (argc < 3) return false + val logLevel = composerMarshaller.getUntyped(1) as? String ?: return false + val message = composerMarshaller.getUntyped(2) as? String ?: return false + + val tag = "ComposerLogs" + + when (logLevel) { + "log" -> context.log.verbose(message, tag) + "debug" -> context.log.debug(message, tag) + "info" -> context.log.info(message, tag) + "warn" -> context.log.warn(message, tag) + "error" -> context.log.error(message, tag) + } + } + "eval" -> { + if (argc < 2) return false + val code = composerMarshaller.getUntyped(1) as? String ?: return false + runCatching { + composerMarshaller.pushUntyped(context.native.composerEval(code)) + }.onFailure { + composerMarshaller.pushUntyped(it.toString()) } - param.setResult(null) } - loaderConfig.addProperty("logPrefix", logPrefix) + else -> context.log.warn("Unknown action: $action", "Composer") } - if (config.bypassCameraRollLimit.get()) { - loaderConfig.addProperty("bypassCameraRollLimit", true) - } + return true + } + private fun loadHooks() { 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 + (() => { try { const EXPORTED_FUNCTION_NAME = "$exportedFunctionName"; $loaderScript } catch (e) { return e.toString() + "\n" + e.stack; } @@ -172,8 +206,21 @@ class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.ACT } } + @Suppress("UNCHECKED_CAST") override fun onActivityCreate() { if (!NativeLib.initialized || config.globalState != true) return + + findClass("com.snapchat.client.composer.NativeBridge").hook("registerNativeModuleFactory", HookStage.BEFORE) { param -> + val moduleFactory = param.argNullable<Any>(1) ?: return@hook + if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString() != "DeviceBridge") return@hook + Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam -> + val functions = methodParam.getResult() as? MutableMap<String, Any> ?: return@ephemeralHookObjectMethod + functions[exportedFunctionName] = newComposerFunction { + handleExportCall(it) + } + } + } + var composerThreadTask: (() -> Unit)? = null findClass("com.snap.composer.callable.ComposerFunctionNative").hook("nativePerform", HookStage.BEFORE) { diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/composer/ComposerMarshaller.kt b/core/src/main/kotlin/me/rhunk/snapenhance/core/wrapper/impl/composer/ComposerMarshaller.kt @@ -0,0 +1,13 @@ +package me.rhunk.snapenhance.core.wrapper.impl.composer + +import me.rhunk.snapenhance.core.wrapper.AbstractWrapper + +class ComposerMarshaller(obj: Any): AbstractWrapper(obj) { + private val getUntypedMethod by lazy { instanceNonNull().javaClass.methods.first { it.name == "getUntyped" } } + private val getSizeMethod by lazy { instanceNonNull().javaClass.methods.first { it.name == "getSize" } } + private val pushUntypedMethod by lazy { instanceNonNull().javaClass.methods.first { it.name == "pushUntyped" } } + + fun getUntyped(index: Int): Any? = getUntypedMethod.invoke(instanceNonNull(), index) + fun getSize() = getSizeMethod.invoke(instanceNonNull()) as Int + fun pushUntyped(value: Any?): Any? = pushUntypedMethod.invoke(instanceNonNull(), value) +}+ \ No newline at end of file