commit dd8590d274d9496ef35b8dd1bdfd46561af53a23
parent 807ce1b6e7c4aadaf779cf87db2218f38b3ffcea
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Fri, 24 May 2024 02:06:01 +0200

feat(composer_hooks): ts modules

Diffstat:
M.github/workflows/debug.yml | 9+++++++++
M.github/workflows/release.yml | 9+++++++++
Acomposer/.gitignore | 2++
Acomposer/build.gradle.kts | 34++++++++++++++++++++++++++++++++++
Acomposer/rollup.config.js | 8++++++++
Acomposer/src/main/ts/composer.ts | 4++++
Acomposer/src/main/ts/imports.ts | 21+++++++++++++++++++++
Acomposer/src/main/ts/main.ts | 36++++++++++++++++++++++++++++++++++++
Acomposer/src/main/ts/modules/bypassCameraRollSelectionLimit.ts | 20++++++++++++++++++++
Acomposer/src/main/ts/modules/firstCreatedUsername.ts | 29+++++++++++++++++++++++++++++
Acomposer/src/main/ts/modules/operaDownloadButton.ts | 36++++++++++++++++++++++++++++++++++++
Acomposer/src/main/ts/types.ts | 44++++++++++++++++++++++++++++++++++++++++++++
Acomposer/src/main/ts/utils.ts | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acomposer/tsconfig.json | 9+++++++++
Acomposer/types/index.d.ts | 2++
Mcore/build.gradle.kts | 1+
Dcore/src/main/assets/composer/loader.js | 138-------------------------------------------------------------------------------
Mcore/src/main/kotlin/me/rhunk/snapenhance/core/features/impl/experiments/ComposerHooks.kt | 150++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msettings.gradle.kts | 1+
19 files changed, 395 insertions(+), 215 deletions(-)

diff --git a/.github/workflows/debug.yml b/.github/workflows/debug.yml @@ -26,6 +26,9 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Setup NPM Dependencies + run: npm install typescript -g + - name: Build run: ./gradlew assembleArmv8Debug @@ -70,6 +73,9 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Setup NPM Dependencies + run: npm install typescript -g + - name: Build run: ./gradlew assembleArmv7Debug @@ -114,6 +120,9 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Setup NPM Dependencies + run: npm install typescript -g + - name: Build run: ./gradlew assembleAllDebug diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml @@ -30,6 +30,9 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Setup NPM Dependencies + run: npm install typescript -g + - name: Build run: ./gradlew assembleArmv8Release @@ -73,6 +76,9 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Setup NPM Dependencies + run: npm install typescript -g + - name: Build run: ./gradlew assembleArmv7Release @@ -116,6 +122,9 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew + - name: Setup NPM Dependencies + run: npm install typescript -g + - name: Build run: ./gradlew assembleAllRelease diff --git a/composer/.gitignore b/composer/.gitignore @@ -0,0 +1 @@ +/build+ \ No newline at end of file diff --git a/composer/build.gradle.kts b/composer/build.gradle.kts @@ -0,0 +1,34 @@ +plugins { + alias(libs.plugins.androidLibrary) + alias(libs.plugins.kotlinAndroid) +} + +android { + namespace = rootProject.ext["applicationId"].toString() + ".composer" + compileSdk = 34 + + sourceSets { + getByName("main") { + assets.srcDirs("build/assets") + } + } +} + +task("compileTypeScript") { + doLast { + project.exec { + commandLine("npx", "--yes", "tsc", "--project", "tsconfig.json") + } + project.exec { + commandLine("npx", "--yes", "rollup", "--config", "rollup.config.js", "--bundleConfigAsCjs") + } + project.copy { + from("build/loader.js") + into("build/assets/composer") + } + } +} + +tasks.named("preBuild").configure { + dependsOn("compileTypeScript") +} diff --git a/composer/rollup.config.js b/composer/rollup.config.js @@ -0,0 +1,7 @@ +export default { + input: "./build/typescript/main.js", + output: { + file: "./build/loader.js", + format: "iife", + } +};+ \ No newline at end of file diff --git a/composer/src/main/ts/composer.ts b/composer/src/main/ts/composer.ts @@ -0,0 +1,4 @@ +export const jsx = require('composer_core/src/JSX').jsx; +export const assetCatalog = require("composer_core/src/AssetCatalog") +export const style = require("composer_core/src/Style"); +export const colors = require("coreui/src/styles/semanticColors"); diff --git a/composer/src/main/ts/imports.ts b/composer/src/main/ts/imports.ts @@ -0,0 +1,21 @@ +import { Config, FriendInfo } from "./types"; + +declare var _getImportsFunctionName: string; +const remoteImports = require('composer_core/src/DeviceBridge')[_getImportsFunctionName](); + +function callRemoteFunction(method: string, ...args: any[]): any | null { + return remoteImports[method](...args); +} + + +export const log = (logLevel: string, message: string) => callRemoteFunction("log", logLevel, message); + +export const getConfig = () => callRemoteFunction("getConfig") as Config; + +export const downloadLastOperaMedia = (isLongPress: boolean) => callRemoteFunction("downloadLastOperaMedia", isLongPress); + +export function getFriendInfoByUsername(username: string): FriendInfo | null { + const friendInfo = callRemoteFunction("getFriendInfoByUsername", username); + if (!friendInfo) return null; + return JSON.parse(friendInfo); +} diff --git a/composer/src/main/ts/main.ts b/composer/src/main/ts/main.ts @@ -0,0 +1,36 @@ +import { getConfig, log } from "./imports"; +import { Module } from "./types"; + +import operaDownloadButton from "./modules/operaDownloadButton"; +import firstCreatedUsername from "./modules/firstCreatedUsername"; +import bypassCameraRollSelectionLimit from "./modules/bypassCameraRollSelectionLimit"; + + +try { + const config = getConfig(); + + if (config.composerLogs) { + ["log", "error", "warn", "info", "debug"].forEach(method => { + console[method] = (...args: any) => log(method, Array.from(args).join(" ")); + }) + } + + const modules: Module[] = [ + operaDownloadButton, + firstCreatedUsername, + bypassCameraRollSelectionLimit + ]; + + modules.forEach(module => { + if (!module.enabled(config)) return + try { + module.init(); + } catch (e) { + console.error(`failed to initialize module ${module.name}`, e, e.stack); + } + }); + + console.log("composer modules loaded!"); +} catch (e) { + log("error", "Failed to load composer modules\n" + e + "\n" + e.stack) +} diff --git a/composer/src/main/ts/modules/bypassCameraRollSelectionLimit.ts b/composer/src/main/ts/modules/bypassCameraRollSelectionLimit.ts @@ -0,0 +1,19 @@ +import { defineModule } from "../types"; +import { interceptComponent } from "../utils"; + +export default defineModule({ + name: "Bypass Camera Roll Selection Limit", + enabled: config => config.bypassCameraRollLimit, + init() { + interceptComponent( + 'memories_ui/src/clickhandlers/MultiSelectClickHandler', + 'MultiSelectClickHandler', + { + "<init>": (args: any[], superCall: () => void) => { + args[1].selectionLimit = 9999999; + superCall(); + } + } + ) + } +});+ \ No newline at end of file diff --git a/composer/src/main/ts/modules/firstCreatedUsername.ts b/composer/src/main/ts/modules/firstCreatedUsername.ts @@ -0,0 +1,28 @@ +import { defineModule } from "../types"; +import { getFriendInfoByUsername } from "../imports"; +import { interceptComponent } from "../utils"; + +export default defineModule({ + name: "Show First Created Username", + enabled: config => config.showFirstCreatedUsername, + init() { + interceptComponent( + 'common_profile/src/identity/ProfileIdentityView', + 'ProfileIdentityView', + { + onRender: (component: any, _args: any[], render: () => void) => { + if (component.viewModel) { + let userInfo = getFriendInfoByUsername(component.viewModel.username); + if (userInfo) { + let firstCreatedUsername = userInfo.username.split("|")[0]; + if (firstCreatedUsername != component.viewModel.username) { + component.viewModel.username += " (" + firstCreatedUsername + ")"; + } + } + } + render(); + } + } + ) + } +});+ \ No newline at end of file diff --git a/composer/src/main/ts/modules/operaDownloadButton.ts b/composer/src/main/ts/modules/operaDownloadButton.ts @@ -0,0 +1,35 @@ +import { assetCatalog, jsx, style } from "../composer" +import { defineModule } from "../types" +import { downloadLastOperaMedia } from "../imports" +import { interceptComponent } from "../utils" + + +export default defineModule({ + name: "Opera Download Button", + enabled: config => config.operaDownloadButton, + init() { + const downloadIcon = assetCatalog.loadCatalog("share_sheet/res").downloadIcon + interceptComponent( + 'context_chrome_header/src/ChromeHeaderRenderer', + 'ChromeHeaderRenderer', + { + onRenderBaseHeader: (_component: any, _args: any[], render: () => void) => { + render() + jsx.beginRender(jsx.makeNodePrototype("image")) + jsx.setAttributeStyle("style", new style.Style({ + height: 32, + marginTop: 4, + marginLeft: 8, + marginRight: 12, + objectFit: "contain", + tint: "white" + })) + jsx.setAttribute("src", downloadIcon) + jsx.setAttributeFunction("onTap", () => downloadLastOperaMedia(false)) + jsx.setAttributeFunction("onLongPress", () => downloadLastOperaMedia(true)) + jsx.endRender() + } + } + ) + } +})+ \ No newline at end of file diff --git a/composer/src/main/ts/types.ts b/composer/src/main/ts/types.ts @@ -0,0 +1,44 @@ +export interface Config { + readonly operaDownloadButton: boolean + readonly bypassCameraRollLimit: boolean + readonly showFirstCreatedUsername: boolean + readonly composerLogs: boolean +} + +export interface FriendInfo { + readonly id: number + readonly lastModifiedTimestamp: number + readonly username: string + readonly userId: string + readonly displayName: string + readonly bitmojiAvatarId: string + readonly bitmojiSelfieId: string + readonly bitmojiSceneId: string + readonly bitmojiBackgroundId: string + readonly friendmojis: string + readonly friendmojiCategories: string + readonly snapScore: number + readonly birthday: number + readonly addedTimestamp: number + readonly reverseAddedTimestamp: number + readonly serverDisplayName: string + readonly streakLength: number + readonly streakExpirationTimestamp: number + readonly reverseBestFriendRanking: number + readonly isPinnedBestFriend: number + readonly plusBadgeVisibility: number + readonly usernameForSorting: string + readonly friendLinkType: number + readonly postViewEmoji: string + readonly businessCategory: number +} + +export interface Module { + readonly name: string + enabled: (config: Config) => boolean + init: () => void +} + +export function defineModule<T extends Module>(module: T & Record<string, any>): T { + return module +} diff --git a/composer/src/main/ts/utils.ts b/composer/src/main/ts/utils.ts @@ -0,0 +1,57 @@ +export function dumpObject(obj: any, indent = 0) { + if (typeof obj !== "object") return console.log(obj); + let prefix = "" + for (let i = 0; i < indent; i++) { + prefix += " "; + } + for (let key of Object.keys(obj)) { + try { + console.log(prefix, key, typeof obj[key], obj[key]); + if (key == "renderer") continue + if (typeof obj[key] === "object" && indent < 10) dumpObject(obj[key], indent + 1); + } catch (e) {} + } +} + +export function proxyProperty(module: any, functionName: string, handler: any) { + if (!module || !module[functionName]) { + console.warn("Function not found", functionName); + return; + } + module[functionName] = new Proxy(module[functionName], { + apply: (a, b, c) => handler(a, b, c), + construct: (a, b, c) => handler(a, b, c) + }); +} + +export function interceptComponent(moduleName: string, className: string, functions: any) { + proxyProperty(require(moduleName), className, (target: any, args: any[], newTarget: any) => { + let initProxy = functions["<init>"] + let component: any; + + if (initProxy) { + initProxy(args, (newArgs: any[]) => { + component = Reflect.construct(target, newArgs || args, newTarget); + }); + } else { + component = Reflect.construct(target, args, newTarget); + } + + for (let funcName of Object.keys(functions)) { + if (funcName == "<init>" || !component[funcName]) continue + proxyProperty(component, funcName, (target: any, thisArg: any, argumentsList: any[]) => { + let result: any; + try { + functions[funcName](component, argumentsList, (newArgs: any[]) => { + result = Reflect.apply(target, thisArg, newArgs || argumentsList); + }); + } catch (e) { + console.error("Error in", funcName, e); + } + return result; + }); + } + + return component; + }) +} diff --git a/composer/tsconfig.json b/composer/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "outDir": "build/typescript", + "target": "ES6", + "typeRoots": ["types/*"] + }, + "include": ["./src/main/ts/**/*", "./types/**/*"] +}+ \ No newline at end of file diff --git a/composer/types/index.d.ts b/composer/types/index.d.ts @@ -0,0 +1 @@ +declare function require(module: string): any;+ \ No newline at end of file diff --git a/core/build.gradle.kts b/core/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { implementation(project(":common")) implementation(project(":mapper")) implementation(project(":native")) + implementation(project(":composer")) implementation(libs.androidx.activity.ktx) implementation(platform(libs.androidx.compose.bom)) diff --git a/core/src/main/assets/composer/loader.js b/core/src/main/assets/composer/loader.js @@ -1,138 +0,0 @@ -const config = callExport("getConfig"); - -if (config.composerLogs) { - ["log", "error", "warn", "info", "debug"].forEach(method => { - console[method] = (...args) => callExport("log", method, Array.from(args).join(" ")); - }) - - console.stacktrace = () => new Error().stack; - console.info("loader.js loaded"); -} - -// Composer imports - -const jsx = require('composer_core/src/JSX').jsx; -const assetCatalog = require("composer_core/src/AssetCatalog") -const style = require("composer_core/src/Style"); -const colors = require("coreui/src/styles/semanticColors"); - -function dumpObject(obj, indent = 0) { - if (typeof obj !== "object") return console.log(obj); - let prefix = "" - for (let i = 0; i < indent; i++) { - prefix += " "; - } - for (let key of Object.keys(obj)) { - try { - console.log(prefix, key, typeof obj[key], obj[key]); - if (key == "renderer") continue - if (typeof obj[key] === "object" && indent < 10) dumpObject(obj[key], indent + 1); - } catch (e) { - } - } -} - -function proxyProperty(module, functionName, handler) { - if (!module || !module[functionName]) { - console.warn("Function not found", functionName); - return; - } - module[functionName] = new Proxy(module[functionName], { - apply: (a, b, c) => handler(a, b, c), - construct: (a, b, c) => handler(a, b, c) - }); -} - -function interceptComponent(moduleName, className, functions) { - proxyProperty(require(moduleName), className, (target, args, newTarget) => { - let initProxy = functions["<init>"] - let component; - - if (initProxy) { - initProxy(args, (newArgs) => { - component = Reflect.construct(target, newArgs || args, newTarget); - }); - } else { - component = Reflect.construct(target, args, newTarget); - } - - for (let funcName of Object.keys(functions)) { - if (funcName == "<init>" || !component[funcName]) continue - proxyProperty(component, funcName, (target, thisArg, argumentsList) => { - let result; - try { - functions[funcName](component, argumentsList, (newArgs) => { - result = Reflect.apply(target, thisArg, newArgs || argumentsList); - }); - } catch (e) { - console.error("Error in", funcName, e); - } - return result; - }); - } - - return component; - }) -} - -if (config.bypassCameraRollLimit) { - interceptComponent( - 'memories_ui/src/clickhandlers/MultiSelectClickHandler', - 'MultiSelectClickHandler', - { - "<init>": (args, superCall) => { - args[1].selectionLimit = 9999999; - superCall(); - } - } - ) -} - -if (config.operaDownloadButton) { - const downloadIcon = assetCatalog.loadCatalog("share_sheet/res").downloadIcon - - interceptComponent( - 'context_chrome_header/src/ChromeHeaderRenderer', - 'ChromeHeaderRenderer', - { - onRenderBaseHeader: (component, args, render) => { - render() - jsx.beginRender(jsx.makeNodePrototype("image")) - jsx.setAttributeStyle("style", new style.Style({ - height: 32, - marginTop: 4, - marginLeft: 8, - marginRight: 12, - objectFit: "contain", - tint: "white" - })) - jsx.setAttribute("src", downloadIcon) - jsx.setAttributeFunction("onTap", () => callExport("downloadLastOperaMedia", false)) - jsx.setAttributeFunction("onLongPress", () => callExport("downloadLastOperaMedia", true)) - jsx.endRender() - } - } - ) -} - -if (config.showFirstCreatedUsername) { - interceptComponent( - 'common_profile/src/identity/ProfileIdentityView', - 'ProfileIdentityView', - { - onRender: (component, _, render) => { - if (component.viewModel) { - let userInfo = callExport("getFriendInfoByUsername", component.viewModel.username); - if (userInfo) { - let userInfoJson = JSON.parse(userInfo); - let firstCreatedUsername = userInfoJson.username.split("|")[0]; - if (firstCreatedUsername != component.viewModel.username) { - component.viewModel.username += " (" + firstCreatedUsername + ")"; - } - } - } - render(); - } - } - ) -} 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 @@ -44,7 +44,7 @@ import kotlin.random.Random class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.INIT_SYNC) { private val config by lazy { context.config.experimental.nativeHooks.composerHooks } - private val exportedFunctionName = Random.nextInt().absoluteValue.toString(16) + private val getImportsFunctionName = Random.nextLong().absoluteValue.toString(16) private val composerConsole by lazy { createComposeAlertDialog(context.mainActivity!!) { @@ -126,7 +126,7 @@ class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.INI } } - private fun newComposerFunction(block: (composerMarshaller: ComposerMarshaller) -> Boolean): Any { + private fun newComposerFunction(block: ComposerMarshaller.() -> Boolean): Any? { val composerFunctionClass = findClass("com.snap.composer.callable.ComposerFunction") return Proxy.newProxyInstance( composerFunctionClass.classLoader, @@ -137,101 +137,97 @@ class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.INI } } - private fun getConfig(): Map<String, Any> { - return mapOf<String, Any>( - "operaDownloadButton" to context.config.downloader.operaDownloadButton.get(), - "bypassCameraRollLimit" to config.bypassCameraRollLimit.get(), - "showFirstCreatedUsername" to config.showFirstCreatedUsername.get(), - "composerConsole" to config.composerConsole.get(), - "composerLogs" to config.composerLogs.get() - ) - } + @Suppress("UNCHECKED_CAST") + override fun init() { + if (config.globalState != true) return - private fun handleExportCall(composerMarshaller: ComposerMarshaller): Boolean { - val argc = composerMarshaller.getSize() - if (argc < 1) return false - val action = composerMarshaller.getUntyped(0) as? String ?: return false + val importedFunctions = mutableMapOf<String, Any?>() - when (action) { - "getConfig" -> composerMarshaller.pushUntyped(getConfig()) - "showToast" -> { - if (argc < 2) return false - context.shortToast(composerMarshaller.getUntyped(1) as? String ?: return false) + fun composerFunction(name: String, block: ComposerMarshaller.() -> Unit) { + importedFunctions[name] = newComposerFunction { + block(this) + true } - "downloadLastOperaMedia" -> context.feature(MediaDownloader::class).downloadLastOperaMediaAsync(composerMarshaller.getUntyped(1) == true) - "getFriendInfoByUsername" -> { - if (argc < 2) return false - val username = composerMarshaller.getUntyped(1) as? String ?: return false - runCatching { - composerMarshaller.pushUntyped(context.database.getFriendInfoByUsername(username)?.let { - context.gson.toJson(it) - }) - }.onFailure { - composerMarshaller.pushUntyped(null) - } - } - "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) - } - } - else -> context.log.warn("Unknown action: $action", "Composer") } - return true - } + composerFunction("getConfig") { + pushUntyped(mapOf<String, Any>( + "operaDownloadButton" to context.config.downloader.operaDownloadButton.get(), + "bypassCameraRollLimit" to config.bypassCameraRollLimit.get(), + "showFirstCreatedUsername" to config.showFirstCreatedUsername.get(), + "composerLogs" to config.composerLogs.get() + )) + } - private fun loadHooks() { - if (!NativeLib.initialized) { - context.log.error("ComposerHooks cannot be loaded without NativeLib") - return + composerFunction("showToast") { + if (getSize() < 1) return@composerFunction + context.shortToast(getUntyped(0) as? String ?: return@composerFunction) } - val loaderScript = context.scriptRuntime.scripting.getScriptContent("composer/loader.js")?.let { pfd -> - ParcelFileDescriptor.AutoCloseInputStream(pfd).use { - it.readBytes().toString(Charsets.UTF_8) + + composerFunction("downloadLastOperaMedia") { + context.feature(MediaDownloader::class).downloadLastOperaMediaAsync(getUntyped(0) == true) + } + + composerFunction("getFriendInfoByUsername") { + if (getSize() < 1) return@composerFunction + val username = getUntyped(0) as? String ?: return@composerFunction + runCatching { + pushUntyped(context.database.getFriendInfoByUsername(username)?.let { + context.gson.toJson(it) + }) + }.onFailure { + pushUntyped(null) } - } ?: run { - context.log.error("Failed to load composer loader script") - return } - context.native.setComposerLoader(""" - (() => { const callExport = require('composer_core/src/DeviceBridge')["$exportedFunctionName"]; try { $loaderScript - } catch (e) { - try { - callExport("log", "error", e.toString() + "\n" + e.stack); - } catch (t) {} + composerFunction("log") { + if (getSize() < 2) return@composerFunction + val logLevel = getUntyped(0) as? String ?: return@composerFunction + val message = getUntyped(1) as? String ?: return@composerFunction + + 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) + } + } + + fun loadHooks() { + if (!NativeLib.initialized) { + context.log.error("ComposerHooks cannot be loaded without NativeLib") + return + } + val loaderScript = context.scriptRuntime.scripting.getScriptContent("composer/loader.js")?.let { pfd -> + ParcelFileDescriptor.AutoCloseInputStream(pfd).use { + it.readBytes().toString(Charsets.UTF_8) } - })(); - """.trimIndent().trim()) + } ?: run { + context.log.error("Failed to load composer loader script") + return + } + context.native.setComposerLoader(""" + (() => { const _getImportsFunctionName = "$getImportsFunctionName"; $loaderScript })(); + """.trimIndent().trim()) - if (config.composerConsole.get()) { - injectConsole() + if (config.composerConsole.get()) { + injectConsole() + } } - } - @Suppress("UNCHECKED_CAST") - override fun init() { - if (config.globalState != true) return findClass("com.snapchat.client.composer.NativeBridge").apply { hook("createViewLoaderManager", HookStage.AFTER) { loadHooks() } 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) + val result = methodParam.getResult() as? MutableMap<String, Any?> ?: return@ephemeralHookObjectMethod + result[getImportsFunctionName] = newComposerFunction { + pushUntyped(importedFunctions) + true } } } diff --git a/settings.gradle.kts b/settings.gradle.kts @@ -20,6 +20,7 @@ dependencyResolutionManagement { rootProject.name = "SnapEnhance" include(":common") include(":core") +include(":composer") include(":app") include(":mapper") include(":native")