package me.eternal.purrfect.core.features.impl.experiments import androidx.compose.foundation.layout.* 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.Alignment 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 kotlinx.coroutines.launch import android.os.Build import me.eternal.purrfect.common.bridge.FileHandleScope import me.eternal.purrfect.common.bridge.toWrapper import me.eternal.purrfect.common.ui.createComposeAlertDialog import me.eternal.purrfect.core.Purrfect import me.eternal.purrfect.core.features.Feature import me.eternal.purrfect.core.features.impl.downloader.MediaDownloader import me.eternal.purrfect.core.util.hook.HookStage import me.eternal.purrfect.core.util.hook.Hooker import me.eternal.purrfect.core.util.hook.hook import me.eternal.purrfect.core.wrapper.impl.valdi.ValdiFunction import me.eternal.purrfect.core.wrapper.impl.valdi.ValdiMarshaller import me.eternal.purrfect.nativelib.NativeLib import kotlin.math.absoluteValue import kotlin.random.Random class ValdiHooks: Feature("ValdiHooks") { private val config by lazy { context.config.experimental.nativeHooks.valdiHooks } private val getImportsFunctionName = Random.nextLong().absoluteValue.toString(16) private var evalFunction: ValdiFunction? = null private val valdiConsole 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("1 + 2") } Text("Valdi 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", "ValdiConsole") result = "Running..." context.coroutineScope.launch { ValdiMarshaller.create()?.use { valdiMarshaller -> valdiMarshaller.pushUntyped(codeContent) valdiMarshaller.pushUntyped(newValdiFunction { if (getSize() < 1) return@newValdiFunction false val output = getUntyped(0) context.log.verbose("eval: $output", "ValdiConsole") result = if (output is Exception) { "${output.javaClass.simpleName}: ${output.message}" } else { output?.toString() ?: "undefined" } true }) evalFunction?.perform(valdiMarshaller) } ?: run { result = "Failed to create ValdiMarshaller" } } } ) { Text("Run") } Column( modifier = Modifier.verticalScroll(rememberScrollState()) ) { Text(result) } } } } private fun newValdiFunction(block: ValdiMarshaller.() -> Boolean): Any? { val functionClass = Purrfect.classCache.valdiFunction ?: return null return java.lang.reflect.Proxy.newProxyInstance( functionClass.classLoader, arrayOf(functionClass) ) { _, method, args -> if (method.name != "perform") return@newProxyInstance null block(ValdiMarshaller(args?.get(0) ?: return@newProxyInstance false)) } } @Suppress("UNCHECKED_CAST") override fun init() { if (config.globalState != true) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { context.log.warn("ValdiHooks disabled on Android 9 and below") return } if (Purrfect.classCache.valdiFunction == null) { context.log.warn("ComposerFunction/ValdiFunction class not found, ValdiHooks feature disabled") return } val importedFunctions = mutableMapOf() fun valdiFunction(name: String, block: ValdiMarshaller.() -> Unit) { val function = newValdiFunction { block(this) true } if (function != null) { importedFunctions[name] = function } } valdiFunction("getConfig") { pushUntyped(mapOf( "operaDownloadButton" to context.config.downloader.operaDownloadButton.get(), "bypassCameraRollLimit" to config.bypassCameraRollLimit.get(), "showFirstCreatedUsername" to config.showFirstCreatedUsername.get(), "valdiLogs" to config.valdiLogs.get(), "customSelfDestructSnapDelay" to config.customSelfDestructSnapDelay.get(), )) } valdiFunction("showToast") { if (getSize() < 1) return@valdiFunction context.shortToast(getUntyped(0) as? String ?: return@valdiFunction) } valdiFunction("downloadLastOperaMedia") { context.feature(MediaDownloader::class).downloadLastOperaMediaAsync(getUntyped(0) == true) } valdiFunction("getFriendOriginalUsername") { if (getSize() < 1) return@valdiFunction val username = getUntyped(0) as? String ?: return@valdiFunction runCatching { pushUntyped(context.database.getFriendOriginalUsername(username)) }.onFailure { pushUntyped(null) } } valdiFunction("log") { if (getSize() < 2) return@valdiFunction val logLevel = getUntyped(0) as? String ?: return@valdiFunction val message = getUntyped(1) as? String ?: return@valdiFunction val tag = "ValdiLogs" 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) } } valdiFunction("setEvalFunction") { if (getSize() < 1) return@valdiFunction evalFunction = ValdiFunction(getUntyped(0) ?: return@valdiFunction) context.log.verbose("Set eval function: $evalFunction", "ValdiHooks") } fun loadHooks() { if (!NativeLib.initialized) { context.log.error("ValdiHooks cannot be loaded without NativeLib") return } val loaderScript = runCatching { context.fileHandlerManager.getFileHandle(FileHandleScope.VALDI.key, "loader.js").toWrapper().readBytes().toString(Charsets.UTF_8) }.onFailure { context.log.error("Failed to load valdi loader script", it) }.getOrNull() ?: return context.native.setValdiLoader(""" const i = setInterval(() => { try { const _runtimeName = "${if (Purrfect.classCache.nativeBridge.name == "com.snapchat.client.valdi.NativeBridge") "valdi" else "composer"}"; require(_runtimeName + '_core/src/DeviceBridge').getDisplayWidth(); clearInterval(i); (() => { const _getImportsFunctionName = "$getImportsFunctionName"; $loaderScript })(); } catch (e) {} }, 200) """.trimIndent().trim()) } loadHooks() if (config.valdiConsole.get()) { context.inAppOverlay.addCustomComposable { FilledIconButton( onClick = { valdiConsole.show() }, modifier = Modifier.align(Alignment.TopEnd).padding(top = 100.dp, end = 16.dp) ) { Icon(Icons.Default.BugReport, contentDescription = "Debug Console") } } } Purrfect.classCache.nativeBridge.hook("registerNativeModuleFactory", HookStage.BEFORE) { param -> val moduleFactory = param.argNullable(1) ?: return@hook if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString()?.contains("DeviceBridge") != true) return@hook Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam -> val result = methodParam.getResult() as? MutableMap ?: return@ephemeralHookObjectMethod val importsFunction = newValdiFunction { pushUntyped(importedFunctions) true } if (importsFunction != null) { result[getImportsFunctionName] = importsFunction } } } } }