mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-04-29 22:24:35 +02:00
feat(composer_hooks): ts modules
This commit is contained in:
parent
807ce1b6e7
commit
dd8590d274
9
.github/workflows/debug.yml
vendored
9
.github/workflows/debug.yml
vendored
@ -26,6 +26,9 @@ jobs:
|
|||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Setup NPM Dependencies
|
||||||
|
run: npm install typescript -g
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew assembleArmv8Debug
|
run: ./gradlew assembleArmv8Debug
|
||||||
|
|
||||||
@ -70,6 +73,9 @@ jobs:
|
|||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Setup NPM Dependencies
|
||||||
|
run: npm install typescript -g
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew assembleArmv7Debug
|
run: ./gradlew assembleArmv7Debug
|
||||||
|
|
||||||
@ -114,6 +120,9 @@ jobs:
|
|||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Setup NPM Dependencies
|
||||||
|
run: npm install typescript -g
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew assembleAllDebug
|
run: ./gradlew assembleAllDebug
|
||||||
|
|
||||||
|
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
@ -30,6 +30,9 @@ jobs:
|
|||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Setup NPM Dependencies
|
||||||
|
run: npm install typescript -g
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew assembleArmv8Release
|
run: ./gradlew assembleArmv8Release
|
||||||
|
|
||||||
@ -73,6 +76,9 @@ jobs:
|
|||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Setup NPM Dependencies
|
||||||
|
run: npm install typescript -g
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew assembleArmv7Release
|
run: ./gradlew assembleArmv7Release
|
||||||
|
|
||||||
@ -116,6 +122,9 @@ jobs:
|
|||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Setup NPM Dependencies
|
||||||
|
run: npm install typescript -g
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew assembleAllRelease
|
run: ./gradlew assembleAllRelease
|
||||||
|
|
||||||
|
1
composer/.gitignore
vendored
Normal file
1
composer/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
34
composer/build.gradle.kts
Normal file
34
composer/build.gradle.kts
Normal file
@ -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")
|
||||||
|
}
|
7
composer/rollup.config.js
Normal file
7
composer/rollup.config.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
input: "./build/typescript/main.js",
|
||||||
|
output: {
|
||||||
|
file: "./build/loader.js",
|
||||||
|
format: "iife",
|
||||||
|
}
|
||||||
|
};
|
4
composer/src/main/ts/composer.ts
Normal file
4
composer/src/main/ts/composer.ts
Normal file
@ -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");
|
21
composer/src/main/ts/imports.ts
Normal file
21
composer/src/main/ts/imports.ts
Normal file
@ -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);
|
||||||
|
}
|
36
composer/src/main/ts/main.ts
Normal file
36
composer/src/main/ts/main.ts
Normal file
@ -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)
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
28
composer/src/main/ts/modules/firstCreatedUsername.ts
Normal file
28
composer/src/main/ts/modules/firstCreatedUsername.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
35
composer/src/main/ts/modules/operaDownloadButton.ts
Normal file
35
composer/src/main/ts/modules/operaDownloadButton.ts
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
44
composer/src/main/ts/types.ts
Normal file
44
composer/src/main/ts/types.ts
Normal file
@ -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
|
||||||
|
}
|
57
composer/src/main/ts/utils.ts
Normal file
57
composer/src/main/ts/utils.ts
Normal file
@ -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;
|
||||||
|
})
|
||||||
|
}
|
8
composer/tsconfig.json
Normal file
8
composer/tsconfig.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "build/typescript",
|
||||||
|
"target": "ES6",
|
||||||
|
"typeRoots": ["types/*"]
|
||||||
|
},
|
||||||
|
"include": ["./src/main/ts/**/*", "./types/**/*"]
|
||||||
|
}
|
1
composer/types/index.d.ts
vendored
Normal file
1
composer/types/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare function require(module: string): any;
|
@ -41,6 +41,7 @@ dependencies {
|
|||||||
implementation(project(":common"))
|
implementation(project(":common"))
|
||||||
implementation(project(":mapper"))
|
implementation(project(":mapper"))
|
||||||
implementation(project(":native"))
|
implementation(project(":native"))
|
||||||
|
implementation(project(":composer"))
|
||||||
|
|
||||||
implementation(libs.androidx.activity.ktx)
|
implementation(libs.androidx.activity.ktx)
|
||||||
implementation(platform(libs.androidx.compose.bom))
|
implementation(platform(libs.androidx.compose.bom))
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
@ -44,7 +44,7 @@ import kotlin.random.Random
|
|||||||
|
|
||||||
class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.INIT_SYNC) {
|
class ComposerHooks: Feature("ComposerHooks", loadParams = FeatureLoadParams.INIT_SYNC) {
|
||||||
private val config by lazy { context.config.experimental.nativeHooks.composerHooks }
|
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 {
|
private val composerConsole by lazy {
|
||||||
createComposeAlertDialog(context.mainActivity!!) {
|
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")
|
val composerFunctionClass = findClass("com.snap.composer.callable.ComposerFunction")
|
||||||
return Proxy.newProxyInstance(
|
return Proxy.newProxyInstance(
|
||||||
composerFunctionClass.classLoader,
|
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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(getConfig())
|
|
||||||
"showToast" -> {
|
|
||||||
if (argc < 2) return false
|
|
||||||
context.shortToast(composerMarshaller.getUntyped(1) as? String ?: return false)
|
|
||||||
}
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
private 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)
|
|
||||||
}
|
|
||||||
} ?: 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) {}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
""".trimIndent().trim())
|
|
||||||
|
|
||||||
if (config.composerConsole.get()) {
|
|
||||||
injectConsole()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun init() {
|
override fun init() {
|
||||||
if (config.globalState != true) return
|
if (config.globalState != true) return
|
||||||
|
|
||||||
|
val importedFunctions = mutableMapOf<String, Any?>()
|
||||||
|
|
||||||
|
fun composerFunction(name: String, block: ComposerMarshaller.() -> Unit) {
|
||||||
|
importedFunctions[name] = newComposerFunction {
|
||||||
|
block(this)
|
||||||
|
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()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
composerFunction("showToast") {
|
||||||
|
if (getSize() < 1) return@composerFunction
|
||||||
|
context.shortToast(getUntyped(0) as? String ?: return@composerFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
} ?: 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
findClass("com.snapchat.client.composer.NativeBridge").apply {
|
findClass("com.snapchat.client.composer.NativeBridge").apply {
|
||||||
hook("createViewLoaderManager", HookStage.AFTER) { loadHooks() }
|
hook("createViewLoaderManager", HookStage.AFTER) { loadHooks() }
|
||||||
hook("registerNativeModuleFactory", HookStage.BEFORE) { param ->
|
hook("registerNativeModuleFactory", HookStage.BEFORE) { param ->
|
||||||
val moduleFactory = param.argNullable<Any>(1) ?: return@hook
|
val moduleFactory = param.argNullable<Any>(1) ?: return@hook
|
||||||
if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString() != "DeviceBridge") return@hook
|
if (moduleFactory.javaClass.getMethod("getModulePath").invoke(moduleFactory)?.toString() != "DeviceBridge") return@hook
|
||||||
Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam ->
|
Hooker.ephemeralHookObjectMethod(moduleFactory.javaClass, moduleFactory, "loadModule", HookStage.AFTER) { methodParam ->
|
||||||
val functions = methodParam.getResult() as? MutableMap<String, Any> ?: return@ephemeralHookObjectMethod
|
val result = methodParam.getResult() as? MutableMap<String, Any?> ?: return@ephemeralHookObjectMethod
|
||||||
functions[exportedFunctionName] = newComposerFunction {
|
result[getImportsFunctionName] = newComposerFunction {
|
||||||
handleExportCall(it)
|
pushUntyped(importedFunctions)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ dependencyResolutionManagement {
|
|||||||
rootProject.name = "SnapEnhance"
|
rootProject.name = "SnapEnhance"
|
||||||
include(":common")
|
include(":common")
|
||||||
include(":core")
|
include(":core")
|
||||||
|
include(":composer")
|
||||||
include(":app")
|
include(":app")
|
||||||
include(":mapper")
|
include(":mapper")
|
||||||
include(":native")
|
include(":native")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user