├── README.md ├── examples ├── alert_dialog.js ├── custom_toast.js ├── disable_camera.js ├── friend_feed_context_menu.js ├── loginfix.js ├── messaging.js ├── networking.js ├── simple_ipc.js ├── toast.js ├── toolbox.js └── unsafe_classloader.js ├── index.d.ts └── index.md /README.md: -------------------------------------------------------------------------------- 1 | # Scripting Documentation 2 | Hello! Here is the long awaited Documentation for the Scripting Engine.
3 | Contributions and improvements to this Documentation are gladly accepted via PRs.
4 | In case of any questions, feel free to ask via opening an issue. 5 | 6 | ## Script Header 7 | 8 | Every script written for the Scripting Engine must include a header section at the beginning of the file. The header follows a specific format, as shown below: 9 | ``` 10 | // ==SE_module== 11 | // name: script_name 12 | // displayName: Script Name 13 | // description: Script description 14 | // version: 1.0 15 | // author: Author 16 | // ==/SE_module== 17 | ``` 18 | The following fields are required: `name`, `version`, `author`
19 | Additionally, there are also optional fields available which are: `displayName`, `description`, `minSnapchatVersion`, `minSEVersion`, `permissions` 20 | 21 | ### Field Description 22 | 23 | `name`: Descriptive name for the script. Must be composed of lowercase letters and underscores only.
24 | `displayName`: Display name of the script.
25 | `author`: Name of the script's creator.
26 | `version`: Version number of the script (e.g., 1.0).
27 | `description`: Brief explanation of the script's functionality.
28 | `minSnapchatVersion`: Minimum required Snapchat version code (e.g., 106822).
29 | `minSEVersion`: Minimum required SnapEnhance version code (e.g., ).
30 | `permissions`: Grant permissions to the script (e.g. unsafe-classloader)
31 | `executionSides`: Set a custom execution side (e.g. core, manager) 32 | 33 | ## Examples 34 | To start off, you can find a couple of Example scripts written by us [here](https://github.com/SnapEnhance/docs/tree/main/examples). 35 | 36 | ### `disable_camera.js`
37 | This scripts main function is, as probably recognizable by the name, to disable the camera.
38 | Conveniently enough, Android offers a great way to do this by using [`getCameraDisabled`](https://developer.android.com/reference/android/app/admin/DevicePolicyManager#getCameraDisabled(android.content.ComponentName)). 39 | ```js 40 | var getCameraDisabledMethod = hooker.findMethodWithParameters("android.app.admin.DevicePolicyManager", "getCameraDisabled", ["android.content.ComponentName"]) 41 | 42 | hooker.hook(getCameraDisabledMethod, hooker.stage.BEFORE, param => { 43 | param.result = true 44 | }) 45 | ``` 46 | It does this by hooking `getCameraDisabled` in the `android.app.admin.DevicePolicyManager` class and making Snapchat believe the camera is disabled by always returning true. 47 | 48 | ### `simple_ipc.js` 49 | This script is supposed to demonstrate the use of IPC Events.
50 | It simply emits an IPC Event with specific arguments.
51 | ```js 52 | module.onSnapMainActivityCreate = activity => { 53 | ipc.emit("myEvent", "hello", 255, activity.packageName) 54 | } 55 | ``` 56 | Which then can get received by other scripts or functions using listeners. 57 | ```js 58 | module.onSnapEnhanceLoad = context => { 59 | ipc.on("myEvent", (args) => { 60 | longToast("received event: " + args) 61 | }) 62 | } 63 | ``` 64 | 65 | ### `toast.js` 66 | This script demonstrates the use of Toast messages, these can be useful to debug or transmit information to the user. 67 | ```js 68 | module.onSnapMainActivityCreate = context => { 69 | longToast("Snapchat opened!") 70 | } 71 | 72 | module.onUnload = () => { 73 | shortToast("Script unloaded!") 74 | } 75 | ``` 76 | Please note that in most cases a context is required. 77 | -------------------------------------------------------------------------------- /examples/alert_dialog.js: -------------------------------------------------------------------------------- 1 | // ==SE_module== 2 | // name: alert_dialog 3 | // displayName: Alert Dialog test 4 | // version: 1.0 5 | // author: John Doe 6 | // ==/SE_module== 7 | 8 | var interfaces = require('java-interfaces') 9 | var im = require('interface-manager') 10 | 11 | function getMyUserId(context) { 12 | var database = context.openOrCreateDatabase("arroyo.db", 0, null) 13 | var cursor = database.rawQuery("SELECT value FROM required_values WHERE key = 'USERID'", null) 14 | var userId = null 15 | 16 | try { 17 | if (cursor.moveToFirst()) { 18 | userId = cursor.getString(0) 19 | } 20 | } finally { 21 | cursor.close() 22 | database.close() 23 | } 24 | 25 | return userId 26 | } 27 | 28 | 29 | module.onSnapMainActivityCreate = activity => { 30 | var myUserId = getMyUserId(activity) 31 | 32 | activity.runOnUiThread(interfaces.runnable(() => { 33 | var myDialog = im.createAlertDialog(activity, (builder, dialog) => { 34 | builder.text("My User Id is : " + myUserId) 35 | }) 36 | 37 | myDialog.show() 38 | })) 39 | } 40 | -------------------------------------------------------------------------------- /examples/custom_toast.js: -------------------------------------------------------------------------------- 1 | // ==SE_module== 2 | // name: custom_toast 3 | // displayName: Custom Toast 4 | // description: A Script that shows a custom toast message on launching Snapchat. 5 | // version: 1.0 6 | // author: Jimothy & Jacob Thomas 7 | // ==/SE_module== 8 | 9 | var networking = require("networking"); 10 | var messaging = require("messaging"); 11 | var config = require("config"); 12 | var im = require("interface-manager"); 13 | var ipc = require("ipc"); 14 | var javaInterfaces = require("java-interfaces"); 15 | var hooker = require("hooker"); 16 | var events = require("events"); 17 | 18 | var settingsContext = { 19 | events: [], 20 | }; 21 | 22 | var defaultPrompt = "Message"; 23 | function createManagerToolBoxUI() { 24 | settingsContext.events.push({ 25 | start: function (builder) { 26 | builder.row(function (builder) { 27 | builder.textInput("Message", config.get("customPrompt", defaultPrompt), function (value) { 28 | config.set("customPrompt", value, true); 29 | }) .maxLines(8) 30 | .singleLine(false); 31 | }); 32 | }, 33 | }); 34 | } 35 | 36 | module.onSnapMainActivityCreate = activity => { 37 | const customPrompt = String(config.get("customPrompt")) || "Message"; 38 | shortToast(customPrompt); 39 | } 40 | function createInterface() { 41 | createManagerToolBoxUI(); 42 | } 43 | function start(_) { 44 | createInterface(); 45 | } 46 | start(); 47 | im.create("settings" /* EnumUI.SETTINGS */, function (builder, args) { 48 | settingsContext.events.forEach(function (event) { 49 | event.start(builder, args); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/disable_camera.js: -------------------------------------------------------------------------------- 1 | // ==SE_module== 2 | // name: camera_disabler 3 | // description: Camera Disabler 4 | // version: 1.0 5 | // author: SnapEnhance 6 | // ==/SE_module== 7 | 8 | var hooker = require("hooker") 9 | var im = require("interface-manager") 10 | var config = require("config") 11 | 12 | module.onSnapEnhanceLoad = context => { 13 | im.create("settings", builder => { 14 | builder.row(builder => { 15 | builder.text("Disable camera") 16 | builder.switch(config.getBoolean("disableCamera", false), value => { 17 | config.setBoolean("disableCamera", value, true) 18 | }) 19 | }).spacedBy(10).fillMaxWidth() 20 | }) 21 | } 22 | 23 | module.onSnapMainActivityCreate = activity => { 24 | var getCameraDisabledMethod = hooker.findMethodWithParameters("android.app.admin.DevicePolicyManager", "getCameraDisabled", ["android.content.ComponentName"]) 25 | 26 | hooker.hook(getCameraDisabledMethod, hooker.stage.BEFORE, param => { 27 | var shouldDisableCamera = config.getBoolean("disableCamera") == true 28 | logInfo("getCameraDisabled called! shouldDisableCamera=" + shouldDisableCamera) 29 | if (shouldDisableCamera) { 30 | param.result = true 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /examples/friend_feed_context_menu.js: -------------------------------------------------------------------------------- 1 | // ==SE_module== 2 | // name: friend_feed_context_menu 3 | // displayName: Friend Feed Contex Menu Test 4 | // version: 1.0 5 | // author: John Doe 6 | // ==/SE_module== 7 | 8 | var im = require("interface-manager") 9 | var config = require('config') 10 | 11 | module.onSnapMainActivityCreate = () => { 12 | im.create("friendFeedContextMenu", (builder, args) => { 13 | builder.text("conversationId: " + args["conversationId"]) 14 | builder.text("userId: " + args["userId"]) 15 | 16 | builder.row(builder => { 17 | builder.text("My Switch") 18 | builder.switch(config.getBoolean("myBoolean", false), value => { 19 | config.setBoolean("myBoolean", value, true) 20 | }) 21 | }).arrangement("spaceBetween").fillMaxWidth().padding(4) 22 | }) 23 | } -------------------------------------------------------------------------------- /examples/loginfix.js: -------------------------------------------------------------------------------- 1 | // ==SE_module== 2 | // name: login_fix 3 | // displayName: Login Fix 4 | // description: Fix login for devices that flags on COF 5 | // version: 1.0 6 | // author: rhunk 7 | // ==/SE_module== 8 | 9 | module.onSnapApplicationLoad = context => { 10 | const hooker = require('hooker') 11 | const config = require('config') 12 | 13 | function t(context) { 14 | const classLoader = context.getClassLoader() 15 | 16 | const baseDexClassLoaderClass = findClass("dalvik.system.BaseDexClassLoader") 17 | const pathListField = baseDexClassLoaderClass.getDeclaredField("pathList") 18 | pathListField.setAccessible(true) 19 | 20 | const dexPathList = pathListField.get(classLoader) 21 | const dexElements = getField(dexPathList, "dexElements"); 22 | 23 | for (let i = 0; i < 1; i++) { 24 | const element = dexElements[i]; 25 | const dexFile = getField(element, "dexFile"); 26 | const entries = getField(dexFile.entries(), "mNameList") 27 | 28 | for (let entry of entries) { 29 | try { 30 | let c = classLoader.loadClass(entry) 31 | if (c == null || getField(c, "superClass").getName() != "java.lang.Object") continue 32 | let methods = c.getMethods() 33 | 34 | for (let method of methods) { 35 | if (method.getParameterCount() < 6 || method.getReturnType().getName() != "io.reactivex.rxjava3.core.Single") continue 36 | 37 | let params = method.getParameterTypes() 38 | if (params[0].getName() != "java.lang.String") continue 39 | if (!params[1].isEnum()) continue 40 | if (params[2].getName() != "java.util.List") continue 41 | 42 | return { 43 | class: c.getName(), 44 | method: method.getName() 45 | } 46 | } 47 | } catch (e) { 48 | logInfo("Error: " + e) 49 | } 50 | } 51 | } 52 | } 53 | 54 | let versionCode = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode 55 | 56 | if (config.get("versionCode") != versionCode) { 57 | config.set("loginMethod", null, true) 58 | config.set("versionCode", versionCode, true) 59 | } 60 | 61 | hooker.hookAllMethods(findClass("com.snap.identity.loginsignup.ui.LoginSignupActivity"), "onCreate", hooker.stage.BEFORE, () => { 62 | let loginMethod = null 63 | try { 64 | loginMethod = JSON.parse(config.get("loginMethod")) 65 | } catch (e) { 66 | logInfo("Error: " + e) 67 | } 68 | 69 | if (loginMethod == null) { 70 | loginMethod = t(context) 71 | } 72 | 73 | config.set("loginMethod", JSON.stringify(loginMethod), true) 74 | 75 | if (loginMethod == null) { 76 | shortToast("Could not find login method!") 77 | return 78 | } 79 | 80 | hooker.hookAllMethods(loginMethod.class, loginMethod.method, hooker.stage.BEFORE, methodParams => { 81 | methodParams.setArg(5, "") 82 | shortToast("Bypassed login check!" + loginMethod.method.toString()) 83 | }) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /examples/messaging.js: -------------------------------------------------------------------------------- 1 | // ==SE_module== 2 | // name: messaging_test 3 | // displayName: Messaging Test 4 | // version: 1.0 5 | // author: SnapEnhance 6 | // ==/SE_module== 7 | 8 | var im = require('interface-manager') 9 | var messaging = require('messaging') 10 | 11 | module.onSnapApplicationLoad = context => { 12 | im.create("conversationToolbox", (builder, args) => { 13 | if (!messaging.isPresent()) { 14 | builder.text("Messaging isn't loaded!") 15 | return 16 | } 17 | 18 | var text1 = builder.text("Fetching conversation...") 19 | 20 | messaging.fetchConversationWithMessages(args["conversationId"], (error, messageList) => { 21 | if (error != null) { 22 | text1.setAttribute("label", "Failed to fetch conversation: " + error) 23 | return 24 | } 25 | 26 | messageList.forEach(message => { 27 | logInfo("message : " + message.serialize()) 28 | }) 29 | 30 | text1.setAttribute("label", "This conversation has " + messageList.size() + " recent messages") 31 | }) 32 | 33 | builder.button("Send Hello World", () => { 34 | messaging.sendChatMessage(args["conversationId"], "Hello World!", (error) => { 35 | if (error != null) { 36 | longToast("Failed to send message: " + error) 37 | return 38 | } 39 | 40 | shortToast("Message sent!") 41 | }) 42 | }) 43 | }) 44 | } -------------------------------------------------------------------------------- /examples/networking.js: -------------------------------------------------------------------------------- 1 | // ==SE_module== 2 | // name: networking_test 3 | // displayName: Networking Test 4 | // version: 1.0 5 | // author: SnapEnhance 6 | // ==/SE_module== 7 | 8 | var networking = require('networking') 9 | var interfaces = require('java-interfaces') 10 | 11 | module.onSnapMainActivityCreate = activity => { 12 | var targetUrl = "https://example.com" 13 | 14 | // async request 15 | 16 | networking.enqueue(networking.newRequest().url(targetUrl), (error, response) => { 17 | if (error != null) { 18 | logInfo("Failed to make request: " + error) 19 | return 20 | } 21 | logInfo("ContentType : " + response.getHeader("Content-Type")) 22 | logInfo("Response: " + response.bodyAsString) 23 | }) 24 | 25 | // sync request in background thread 26 | 27 | type("java.lang.Thread").newInstance( 28 | interfaces.runnable(() => { 29 | var response = networking.execute(networking.newRequest().url(targetUrl)) 30 | logInfo("Response: " + response.bodyAsString) 31 | }) 32 | ).start() 33 | 34 | // websocket 35 | 36 | networking.newWebSocket(networking.newRequest().url("wss://echo.websocket.org"), { 37 | onOpen: (ws, response) => { 38 | logInfo("WebSocket open: " + response.statusMessage) 39 | ws.send("Hello") 40 | }, 41 | onMessageText: (ws, text) => { 42 | logInfo("message: " + text) 43 | }, 44 | onMessageBytes: (ws, bytes) => { 45 | logInfo("message bytes: " + bytes) 46 | }, 47 | onClosing: (ws, code, reason) => { 48 | logInfo("Closing: " + code + " " + reason) 49 | }, 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /examples/simple_ipc.js: -------------------------------------------------------------------------------- 1 | // ==SE_module== 2 | // name: ipc_test 3 | // description: IPC Test 4 | // version: 1.0 5 | // author: SnapEnhance 6 | // ==/SE_module== 7 | 8 | var ipc = require("ipc") 9 | 10 | module.onSnapEnhanceLoad = context => { 11 | ipc.on("myEvent", (args) => { 12 | longToast("received event: " + args) 13 | }) 14 | } 15 | 16 | module.onSnapMainActivityCreate = activity => { 17 | ipc.emit("myEvent", "hello", 255, activity.packageName) 18 | } 19 | -------------------------------------------------------------------------------- /examples/toast.js: -------------------------------------------------------------------------------- 1 | // ==SE_module== 2 | // name: toasts 3 | // displayName: Toasts 4 | // description: Shows various toast messages 5 | // version: 1.0 6 | // author: John Doe 7 | // ==/SE_module== 8 | 9 | module.onSnapMainActivityCreate = activity => { 10 | shortToast("Snapchat opened!") 11 | } 12 | 13 | module.onSnapApplicationLoad = context => { 14 | shortToast("Snapchat loaded!") 15 | } 16 | 17 | module.onSnapEnhanceLoad = context => { 18 | shortToast("SnapEnhance loaded!") 19 | } 20 | 21 | module.onUnload = () => { 22 | shortToast("Script unloaded!") 23 | } 24 | -------------------------------------------------------------------------------- /examples/toolbox.js: -------------------------------------------------------------------------------- 1 | // ==SE_module== 2 | // name: toolbox_test 3 | // displayName: Toolbox Test 4 | // version: 1.0 5 | // author: SnapEnhance 6 | // ==/SE_module== 7 | 8 | var im = require('interface-manager') 9 | 10 | module.onSnapApplicationLoad = context => { 11 | im.create("conversationToolbox", (builder, args) => { 12 | builder.text("Conversation id: " + args["conversationId"]) 13 | builder.button("Dismiss Dialog", () => { 14 | args["alertDialog"].dismiss() 15 | }) 16 | }) 17 | } -------------------------------------------------------------------------------- /examples/unsafe_classloader.js: -------------------------------------------------------------------------------- 1 | // ==SE_module== 2 | // name: unsafe_classloader 3 | // displayName: Unsafe classloader 4 | // version: 1.0 5 | // author: John Doe 6 | // permissions: unsafe-classloader 7 | // ==/SE_module== 8 | 9 | var interfaces = require('java-interfaces') 10 | 11 | module.onSnapMainActivityCreate = () => { 12 | type("java.lang.Thread").newInstance( 13 | interfaces.runnable(() => { 14 | try { 15 | var okHttpClient = type("okhttp3.OkHttpClient$Builder", true).newInstance() 16 | .followRedirects(false) 17 | .build() 18 | 19 | var response = okHttpClient.newCall(type("okhttp3.Request$Builder", true).newInstance().url("https://github.com/").build()).execute() 20 | logInfo("response: " + response.body().string()) 21 | } catch (e) { 22 | logError(e) 23 | } 24 | }) 25 | ).start() 26 | } -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | interface Class { 2 | getName(): string; 3 | } 4 | 5 | interface JavaType { 6 | /** @deprecated */ 7 | newInstance(...args: any[]): any; 8 | __new__(...args: any[]): any; 9 | } 10 | 11 | interface SEWrapper { 12 | instanceNonNull(): any; 13 | isPresent(): boolean; 14 | toString(): string; 15 | 16 | getEnumValue(fieldName: string, defaultValue: any): any; 17 | setEnumValue(fieldName: string, value: any /* java.lang.Enum */): void; 18 | } 19 | 20 | interface JSConsole { 21 | log(...data: any): void; 22 | warn(...data: any): void; 23 | error(...data: any): void; 24 | debug(...data: any): void; 25 | info(...data: any): void; 26 | trace(...data: any): void; 27 | verbose(...data: any): void; 28 | } 29 | 30 | declare const console: JSConsole; 31 | 32 | // Get the current execution side at runtime 33 | // - core: Snapchat side 34 | // - manager: SnapEnhance side 35 | declare const currentSide: "core" | "manager"; 36 | 37 | declare namespace module { 38 | interface ModuleInfo { 39 | readonly name: string; 40 | readonly displayName: string; 41 | readonly version: string; 42 | readonly description: string | undefined; 43 | readonly author: string | undefined; 44 | readonly minSnapchatVersion: number | undefined; 45 | readonly minSEVersion: number | undefined; 46 | readonly grantedPermissions: string[]; 47 | } 48 | 49 | /* 50 | Allow the module to export functions/variables to be used by other modules like in Node.js (can be accessed using require('@modules/moduleName').variableName) 51 | */ 52 | let exports: any | undefined; 53 | 54 | const info: ModuleInfo; 55 | 56 | // -- SnapEnhance side -- 57 | 58 | // Called when the SnapEnhance app is started in the background/foreground 59 | let onSnapEnhanceLoad: ((context: any) => void) | undefined; 60 | 61 | 62 | // -- Snapchat side -- 63 | 64 | // Called when the Snapchat app is started in the background/foreground 65 | let onSnapApplicationLoad: ((context: any) => void) | undefined; 66 | 67 | // Called once when the main activity is created 68 | let onSnapMainActivityCreate: ((activity: any) => void) | undefined; 69 | 70 | // Called when the bridge connection between SnapEnhance and Snapchat is connected or reconnected 71 | let onBridgeConnected: ((isReloaded: boolean) => void) | undefined; 72 | 73 | // Called when the module is unloaded by the user 74 | let onUnload: (() => void) | undefined; 75 | } 76 | 77 | declare function logInfo(message: any); 78 | declare function logError(message: any, throwable?: any); 79 | 80 | declare function shortToast(...messages: string[]); 81 | declare function longToast(...messages: string[]); 82 | 83 | // the useModClassLoader parameter requires the "unsafe-classloader" permission when set to true 84 | declare function type(className: string, useModClassLoader?: boolean): JavaType | undefined; 85 | declare function findClass(className: string, useModClassLoader?: boolean): Class | undefined; 86 | declare function setField(instance: any, fieldName: string, value: any | undefined): void; 87 | declare function getField(instance: any, fieldName: string): any | undefined; 88 | 89 | // This function makes the current thread sleep for the specified duration in milliseconds 90 | declare function sleep(durationMs: number); 91 | 92 | declare module "hooker" { 93 | enum stage { 94 | BEFORE = "before", 95 | AFTER = "after" 96 | } 97 | 98 | interface ScriptHookCallback { 99 | result: any; 100 | readonly thisObject: any; 101 | readonly method: any; 102 | readonly args: any[]; 103 | 104 | cancel(): void; 105 | arg(index: number): any; 106 | setArg(index: number, value: any): void; 107 | invokeOriginal(): void; 108 | invokeOriginal(args: any[]): void; 109 | toString(): string; 110 | } 111 | 112 | type HookCallback = (scriptHookCallback: ScriptHookCallback) => void; 113 | type HookUnhook = () => void; 114 | 115 | function findMethod(clazz: Class, methodName: string): any | undefined; 116 | function findMethodWithParameters(clazz: Class, methodName: string, types: string[]): any | undefined; 117 | function findMethod(className: string, methodName: string): any | undefined; 118 | function findMethodWithParameters(className: string, methodName: string, types: string[]): any | undefined; 119 | function findConstructor(clazz: Class, types: string[]): any | undefined; 120 | function findConstructorParameters(className: string, types: string[]): any | undefined; 121 | 122 | function hook(method: any, stage: stage, callback: HookCallback): HookUnhook; 123 | function hookAllMethods(clazz: Class, methodName: string, stage: stage, callback: HookCallback): HookUnhook; 124 | function hookAllConstructors(clazz: Class, stage: stage, callback: HookCallback): HookUnhook; 125 | function hookAllMethods(className: string, methodName: string, stage: stage, callback: HookCallback): HookUnhook | undefined; 126 | function hookAllConstructors(className: string, stage: stage, callback: HookCallback): HookUnhook | undefined; 127 | } 128 | 129 | declare module "config" { 130 | function get(key: string): string | undefined; 131 | function get(key: string, defaultValue: any): string; 132 | 133 | function getInteger(key: string): number | undefined; 134 | function getInteger(key: string, defaultValue: number): number; 135 | 136 | function getDouble(key: string): number | undefined; 137 | function getDouble(key: string, defaultValue: number): number; 138 | 139 | function getBoolean(key: string): boolean | undefined; 140 | function getBoolean(key: string, defaultValue: boolean): boolean; 141 | 142 | function getLong(key: string): number | undefined; 143 | function getLong(key: string, defaultValue: number): number | undefined; 144 | 145 | function getFloat(key: string): number | undefined; 146 | function getFloat(key: string, defaultValue: number): number | undefined; 147 | 148 | function getByte(key: string): number | undefined; 149 | function getByte(key: string, defaultValue: number): number | undefined; 150 | 151 | function getShort(key: string): number | undefined; 152 | function getShort(key: string, defaultValue: number): number | undefined; 153 | 154 | function set(key: string, value: any): void; 155 | function set(key: string, value: any, save: boolean): void; 156 | 157 | function setInteger(key: string, value: number): void; 158 | function setInteger(key: string, value: number, save: boolean): void; 159 | 160 | function setDouble(key: string, value: number): void; 161 | function setDouble(key: string, value: number, save: boolean): void; 162 | 163 | function setBoolean(key: string, value: boolean): void; 164 | function setBoolean(key: string, value: boolean, save: boolean): void; 165 | 166 | function setLong(key: string, value: number): void; 167 | function setLong(key: string, value: number, save: boolean): void; 168 | 169 | function setFloat(key: string, value: number): void; 170 | function setFloat(key: string, value: number, save: boolean): void; 171 | 172 | function setByte(key: string, value: number): void; 173 | function setByte(key: string, value: number, save: boolean): void; 174 | 175 | function setShort(key: string, value: number): void; 176 | function setShort(key: string, value: number, save: boolean): void; 177 | 178 | function save(): void; 179 | function load(): void; 180 | function deleteConfig(): void; 181 | } 182 | 183 | declare module "interface-manager" { 184 | type enumUi = "settings" | "friendFeedContextMenu" | "conversationToolbox" 185 | 186 | interface AlertDialog { 187 | show(): void; 188 | dismiss(): void; 189 | } 190 | 191 | type BuilderCallback = (builder: InterfaceBuilder, args: Record) => void; 192 | type AlertDialogCallback = (builder: InterfaceBuilder, alertDialog: AlertDialog) => void; 193 | 194 | interface Node { 195 | setAttribute(key: string, value: any | undefined): void 196 | fillMaxWidth(): Node 197 | fillMaxHeight(): Node 198 | label(text: string): Node 199 | padding(padding: number): Node 200 | fontSize(size: number): Node 201 | color(color: number): Node 202 | visibility(visibility: "visible" | "invisible" | "gone"): Node 203 | } 204 | 205 | interface RowColumnNode extends Node { 206 | arrangement(arrangement: "start" | "end" | "top" | "bottom" | "center" | "spaceBetween" | "spaceAround" | "spaceEvenly"): RowColumnNode 207 | alignment(alignment: "start" | "end" | "top" | "bottom" | "centerVertically" | "centerHorizontally"): RowColumnNode 208 | spacedBy(spacing: number): RowColumnNode 209 | } 210 | 211 | interface TextInputNode extends Node { 212 | value(value: string): TextInputNode 213 | placeholder(placeholder: string): TextInputNode 214 | callback(callback: ((value: string) => void)): TextInputNode 215 | readonly(state: boolean): TextInputNode 216 | singleLine(state: boolean): TextInputNode 217 | maxLines(lines: number): TextInputNode 218 | } 219 | 220 | interface InterfaceBuilder { 221 | onDispose(callback: (() => void)): void; 222 | onLaunched(callback: (() => void)): void; 223 | onLaunched(key: any, callback: (() => void)); 224 | row(callback: BuilderCallback): RowColumnNode; 225 | column(callback: BuilderCallback): RowColumnNode; 226 | text(label: string): Node; 227 | switch(state: boolean | undefined, onChange: ((state: boolean) => void)): Node; 228 | button(label: string, onClick: (() => void)): Node; 229 | slider(min: number, max: number, step: number, value: number, onChange: ((value: number) => void)): Node; 230 | list(label: string, items: string[], onClick: ((index: number) => void)): Node; 231 | textInput(placeholder: string, value: string, onChange: ((value: string) => void)): TextInputNode; 232 | } 233 | 234 | function create(name: enumUi, callback: BuilderCallback): void; 235 | function createAlertDialog(activity: any, callback: AlertDialogCallback): AlertDialog; 236 | function createAlertDialog(activity: any, builder: ((alertDialogBuilder: any) => void), callback: AlertDialogCallback): AlertDialog; 237 | } 238 | 239 | declare module "ipc" { 240 | type Listener = (args: any[]) => void; 241 | 242 | /* Note: listeners are automatically re-registered after a bridge reload */ 243 | function on(channel: string, listener: Listener): void; 244 | function onBroadcast(channel: string, eventName: string, listener: Listener): void; 245 | 246 | /* all those functions return the number of listeners that are called */ 247 | function emit(eventName: string): number; 248 | function emit(eventName: string, ...args: any[]): number; 249 | function broadcast(channel: string, eventName: string): number; 250 | function broadcast(channel: string, eventName: string, ...args: any[]): number; 251 | 252 | function isBridgeAlive(): boolean; 253 | } 254 | 255 | declare module "java-interfaces" { 256 | function runnable(callback: (() => void)): any; 257 | function newProxy(javaClass: Class, callback: ((proxy: any, method: any, args: any[]) => any)): any; 258 | function thread(callback: (() => void)): any; 259 | } 260 | 261 | declare module "messaging" { 262 | interface SnapUUID extends SEWrapper { 263 | toBytes(): any; // byte[] 264 | toUUID(): any; // java.util.UUID 265 | } 266 | 267 | interface MessageContent extends SEWrapper { 268 | content: any; // byte[] 269 | contentType: any; 270 | } 271 | 272 | interface UserIdToReaction { 273 | userId: SnapUUID; 274 | reactionId: number; 275 | } 276 | 277 | interface MessageMetadata extends SEWrapper { 278 | createdAt: number, 279 | readAt: number, 280 | playableSnapState: any; 281 | savedBy: SnapUUID[]; 282 | openedBy: SnapUUID[]; 283 | seenBy: SnapUUID[]; 284 | reactions: UserIdToReaction[]; 285 | isSaveable: boolean; 286 | } 287 | 288 | interface MessageDescriptor extends SEWrapper { 289 | messageId: number; 290 | conversationId: SnapUUID; 291 | } 292 | 293 | interface Message extends SEWrapper { 294 | orderKey: number; 295 | senderId: SnapUUID; 296 | messageContent: MessageContent; 297 | messageMetadata: MessageMetadata; 298 | messageDescriptor: MessageDescriptor; 299 | messageState: any 300 | 301 | serialize(): string | undefined; 302 | } 303 | 304 | type ResultCallback = (error: any | undefined) => void; 305 | type MessageResultCallback = (error: any | undefined, message?: Message) => void; 306 | type MessageListResultCallback = (error: any | undefined, messages: Message[]) => void; 307 | 308 | interface ConversationUserIdPair { 309 | readonly conversationId: string; 310 | readonly userId: string; 311 | } 312 | 313 | type MessageUpdate = "read" | "release" | "save" | "unsave" | "erase" | "screenshot" | "screen_record" | "replay" | "reaction" | "remove_reaction" | "revoke_transcription" | "allow_transcription" | "erase_saved_story_media"; 314 | 315 | function onConversationManagerReady(callback: () => void): void; 316 | 317 | function isPresent(): boolean; 318 | function newSnapUUID(uuid: string): SnapUUID; 319 | 320 | function updateMessage(conversationId: string, messageId: number, action: MessageUpdate, callback: ResultCallback): void; 321 | function fetchConversationWithMessagesPaginated(conversationId: string, lastMessageId: number, amount: number, callback: MessageListResultCallback): void; 322 | function fetchConversationWithMessages(conversationId: string, callback: MessageListResultCallback): void; 323 | function fetchMessageByServerId(conversationId: string, serverId: number, callback: MessageResultCallback): void; 324 | function fetchMessagesByServerIds(conversationId: string, serverIds: number[], callback: MessageListResultCallback): void; 325 | function displayedMessages(conversationId: string, lastMessageId: number, callback: ResultCallback): void; 326 | function fetchMessage(conversationId: string, messageId: number, callback: MessageResultCallback): void; 327 | function clearConversation(conversationId: string, callback: ResultCallback): void; 328 | function getOneOnOneConversationIds(userIds: string[], callback: (error?: any, result?: ConversationUserIdPair[]) => void): void; 329 | function sendChatMessage(conversationId: string, message: string, callback: ResultCallback): void; 330 | 331 | interface BitmojiInfo extends SEWrapper { 332 | avatarId?: string 333 | backgroundId?: string 334 | sceneId?: string 335 | selfieId?: string 336 | } 337 | 338 | interface Snapchatter extends SEWrapper { 339 | readonly bitmojiInfo?: BitmojiInfo 340 | displayName?: string 341 | userId: SnapUUID 342 | username: string 343 | } 344 | 345 | function fetchSnapchatterInfos(userIds: string[]): Snapchatter[]; 346 | } 347 | 348 | 349 | 350 | declare module "networking" { 351 | interface RequestBuilder { 352 | url(url: string): RequestBuilder; 353 | addHeader(name: string, value: string): RequestBuilder; 354 | removeHeader(name: string): RequestBuilder; 355 | method(method: "POST" | "GET" | "PUT" | "HEAD" | "DELETE" | "PATCH", body: string | any | undefined /* byte[] | java.io.InputStream | null */): RequestBuilder; 356 | } 357 | 358 | interface Response { 359 | readonly statusCode: number; 360 | readonly statusMessage: string; 361 | readonly headers: Record; 362 | readonly bodyAsString: string; 363 | readonly bodyAsStream: any; // java.io.InputStream 364 | readonly bodyAsByteArray: any; // byte[] 365 | readonly contentLength: number; 366 | getHeader(name: string): string | undefined; 367 | close(): void; 368 | } 369 | 370 | interface Websocket { 371 | cancel(): void; 372 | close(code: number, reason: string): void; 373 | queueSize(): number; 374 | send(bytes: any): void; // byte[] | string 375 | } 376 | 377 | interface WebsocketListener { 378 | onOpen(websocket: Websocket, response: Response): void; 379 | onClosed(websocket: Websocket, code: number, reason: string): void; 380 | onClosing(websocket: Websocket, code: number, reason: string): void; 381 | onFailure(websocket: Websocket, throwable: any, response: Response | undefined): void; 382 | onMessageBytes(websocket: Websocket, bytes: any): void; // byte[] 383 | onMessageText(websocket: Websocket, text: string): void; 384 | } 385 | 386 | function getUrl(url: string, callback: (error: string | undefined, response: string) => void): void; 387 | function getUrlAsStream(url: string, callback: (error: string | undefined, response: any) => void): void; // java.io.InputStream 388 | function newRequest(): RequestBuilder; 389 | function enqueue(requestBuilder: RequestBuilder, callback: (error: string | undefined, response: Response | undefined) => void): void; 390 | function execute(requestBuilder: RequestBuilder): Response; 391 | function newWebSocket(requestBuilder: RequestBuilder, listener: WebsocketListener): void; 392 | } 393 | 394 | declare module "events" { 395 | interface Event { 396 | canceled: boolean; 397 | } 398 | 399 | interface ConversationUpdateEvent extends Event { 400 | readonly conversationId: string; 401 | readonly conversation: any; // com.snapchat.client.messaging.Conversation 402 | readonly messages: Message[]; 403 | } 404 | 405 | interface BuildMessageEvent extends Event { 406 | readonly message: Message; 407 | } 408 | 409 | interface BindViewEvent extends Event { 410 | readonly model: any 411 | readonly view: any; // android.view.View 412 | } 413 | 414 | interface OnSnapInteractionEvent extends Event { 415 | readonly interactionType: any; // enum 416 | readonly conversationId: string; 417 | readonly messageId: number; // long 418 | } 419 | 420 | interface SendMessageWithContentEvent extends Event { 421 | readonly destinations: any; 422 | readonly messageContent: MessageContent; 423 | } 424 | 425 | interface AddViewEvent extends Event { 426 | readonly parent: any; // android.view.ViewGroup 427 | view: any; // android.view.View 428 | } 429 | 430 | function onConversationUpdated(callback: (event: ConversationUpdateEvent) => void): void; 431 | function onMessageBuild(callback: (event: BuildMessageEvent) => void): void; 432 | function onViewBind(callback: (event: BindViewEvent) => void): void; 433 | function onSnapInteraction(callback: (event: OnSnapInteractionEvent) => void): void; 434 | function onPreMessageSend(callback: (event: SendMessageWithContentEvent) => void): void; 435 | function onAddView(callback: (event: AddViewEvent) => void): void; 436 | } 437 | 438 | 439 | declare module "protobuf" { 440 | interface Wire { 441 | getId(): number; 442 | getType(): any; 443 | getValue(): any; 444 | 445 | toReader(): ProtoReader 446 | } 447 | 448 | interface ProtoReader { 449 | getBuffer(): any; // byte[] 450 | 451 | followPath(ids: number[], excludeLast: boolean, reader: (reader: ProtoReader) => void): ProtoReader | null; 452 | containsPath(ids: number[]): boolean; 453 | 454 | forEach(block: (index: number, wire: Wire) => void): void; 455 | forEach(ids: number[], reader: (reader: ProtoReader) => void): void; 456 | 457 | eachBuffer(ids: number[], reader: (reader: ProtoReader) => void): void; 458 | eachBuffer(block: (index: number, byteArray: any) => void): void; 459 | contains(id: number): boolean; 460 | 461 | getWire(id: number): Wire | null; 462 | getRawValue(id: number): any; 463 | 464 | getByteArray(id: number): any; 465 | getByteArray(ids: number[]): any; 466 | 467 | getString(id: number): string; 468 | getString(ids: number[]): string; 469 | 470 | getVarInt(id: number): number; 471 | getVarInt(ids: number[]): number; 472 | 473 | getCount(id: number): number; 474 | 475 | getFixed64(id: number): number; 476 | getFixed64(ids: number[]): number; 477 | getFixed32(id: number): number; 478 | } 479 | 480 | interface ProtoWriter { 481 | addBuffer(id: number, byteArray: any): void; 482 | addVarInt(id: number, value: number): void; 483 | addString(id: number, value: string): void; 484 | addFixed32(id: number, value: number): void; 485 | addFixed64(id: number, value: number): void; 486 | 487 | from(id: number, block: (writer: ProtoWriter) => void): void; 488 | from(ids: number[], block: (writer: ProtoWriter) => void): void; 489 | 490 | addWire(wire: Wire): void; 491 | 492 | toByteArray(): any; // byte[] 493 | } 494 | 495 | interface EditorContext { 496 | clear(): void; 497 | 498 | addWire(wire: Wire): void; 499 | addVarInt(id: number, value: number): void; 500 | addBuffer(id: number, byteArray: any): void; 501 | add(id: number, block: (writer: ProtoWriter) => void): void; 502 | addString(id: number, value: string): void; 503 | addFixed64(id: number, value: number): void; 504 | addFixed32(id: number, value: number): void; 505 | 506 | firstOrNull(id: number): Wire | null; 507 | getOrNull(id: number): Wire[] | null; 508 | get(id: number): Wire[]; 509 | remove(id: number): void; 510 | remove(id: number, index: number): void; 511 | 512 | edit(id: number, block: (context: EditorContext) => void): void; 513 | editEach(id: number, block: (context: EditorContext) => void): void; 514 | } 515 | 516 | interface ProtoEditor { 517 | edit(ids: number[], block: (context: EditorContext) => void): void; 518 | 519 | toByteArray(): any; // byte[] 520 | } 521 | 522 | interface GrpcWriter { 523 | addHeader(key: string, value: string): void; 524 | toByteArray(): any; // byte[] 525 | } 526 | 527 | interface GrpcReader { 528 | getHeaders(): Record; 529 | getMessages(): ProtoReader[]; 530 | 531 | read(block: (reader: ProtoReader) => void): void; 532 | } 533 | 534 | function reader(data: any/* byte[] | java.io.InputStream | String */): ProtoReader; 535 | function writer(): ProtoWriter; 536 | function editor(data: any/* byte[] | java.io.InputStream | String */): ProtoEditor; 537 | 538 | function grpcWriter(...data: any[]/* byte[] | java.io.InputStream | String */): GrpcWriter; 539 | function grpcReader(data: any/* byte[] | java.io.InputStream | String */): GrpcReader; 540 | } 541 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | ## Script header 2 | 3 | ``` 4 | // ==SE_module== 5 | // name: Your module name 6 | // description: Your module description 7 | // version: 1.0 8 | // author: You 9 | // ==/SE_module== 10 | ``` 11 | 12 | ## Global scope 13 | - **logInfo** (*messages*: ObjectArray) 14 | ```js 15 | logInfo("hello", 14, false) 16 | ``` 17 | - **type** (className: String) gives an interface for static method calls, static fields and instantiation 18 | ```js 19 | var logObject = type("android.util.Log").newInstance() 20 | logInfo(logObject) // prints "android.util.Log@000000" 21 | type("java.lang.System").out.println("Hello!") // prints "Hello!" 22 | ``` 23 | - **findClass** (className: String) returns a java.lang.Class for a given class name 24 | ```js 25 | logInfo(findClass("java.lang.System")) // prints "class java.lang.System" 26 | ``` 27 | - **longToast**/**shortToast**(message: String) show a toast on the screen 28 | ```js 29 | longToast("Hello!") 30 | ``` 31 | 32 | 33 | ## Execution sides 34 | 35 | ```js 36 | module.onManagerLoad = () => { 37 | // run code when manager context is started 38 | } 39 | 40 | module.onSnapActivity = () => { 41 | // run code when the main snapchat activity is created 42 | } 43 | 44 | ``` 45 | 46 | ## IPC 47 | #### Pass event through any execution side 48 | 49 | ```js 50 | module.onManagerLoad = () => { 51 | // declare event in manager side 52 | ipc.on("hello", args => { 53 | logInfo("Got event from snapchat app! arguments: ", args) 54 | }) 55 | } 56 | 57 | module.onSnapActivity = () => { 58 | // send event to the manager from the snapchat app 59 | ipc.emit("hello", 1337) 60 | } 61 | 62 | ``` 63 | 64 | #### Pass events between other installed scripts 65 | 66 | - *script1.js* 67 | 68 | ```js 69 | module.onManagerLoad = () => { 70 | ipc.onBroadcast("mychannel", "eventA", (args) => { 71 | logInfo("eventA called from script1.js => " + args) 72 | }) 73 | } 74 | ``` 75 | 76 | - *script2.js* 77 | ```js 78 | module.onManagerLoad = () => { 79 | ipc.onBroadcast("mychannel", "eventA", (args) => { 80 | logInfo("eventA called from script2.js => " + args) 81 | ipc.broadcast("mychannel", "eventC", "hello") 82 | }) 83 | } 84 | ``` 85 | 86 | - *script3.js* 87 | 88 | ```js 89 | module.onSnapActivity = () => { 90 | ipc.onBroadcast("mychannel", "eventC", (args) => { 91 | logInfo("eventC called from script3.js => " + args) 92 | }) 93 | // channel, eventName, data 94 | ipc.broadcast("mychannel", "eventA", "hi") 95 | } 96 | ``` 97 | 98 | result (android logcat) 99 | 100 | ``` 101 | eventA called from script1.js => hi 102 | eventA called from script2.js => hi 103 | eventC called from script3.js => hello 104 | ``` 105 | 106 | ## Hooks 107 | 108 | ### types 109 | - enum HookStage is a string value of "before" or "after" 110 | 111 | - class HookCallback : 112 | - properties 113 | - getter/setter result: Object 114 | - getter thisObject: Object 115 | - getter method: JavaMember 116 | - getter args: ObjectArray 117 | - functions 118 | - cancel() 119 | - arg(index: Integer) 120 | - setArg(index: Integer, value: Object) 121 | - invokeOriginal() 122 | - invokeOriginal(args: ObjectArray) 123 | 124 | - function HookUnhook unhooks hooked members 125 | 126 | ### public interface 127 | - Find members 128 | - findMethod (class: JavaClass, methodName: String) => Member 129 | - findMethod (className: String, methodName: String) => Member 130 | - findMethodWithParameters (class: JavaClass, methodName: String, types: StringArray) => Member 131 | - findMethodWithParameters (className: String, methodName: String, types: StringArray) => Member 132 | - findConstructor (class: JavaClass, types: StringArray) => Member 133 | - findConstructor (className: String, types: StringArray) => Member 134 | 135 | - Hook members 136 | - hook (member: JavaMember, stage: HookStage, callback: HookCallback) => HookUnhook 137 | - hookAllMethods (class: JavaClass, methodName: String, stage: HookStage, callback: HookCallback) => HookUnhook 138 | - hookAllMethods (className: String, methodName: String, stage: HookStage, callback: HookCallback) => HookUnhook 139 | - hookAllConstructors (class: JavaClass, stage: HookStage, callback: HookCallback) => HookUnhook 140 | - hookAllConstructors (className: String, stage: HookStage, callback: HookCallback) => HookUnhook 141 | 142 | ### Example 143 | ```js 144 | module.onSnapActivity = () => { 145 | hooker.hookAllConstructors("com.snapchat.client.messaging.Message", "before", callback => { 146 | logInfo("Message constructor " + callback.thisObject) 147 | }) 148 | 149 | val hookerUnhook = hooker.hook( 150 | hooker.findMethod("com.snapchat.client.network_api.NetworkApi$CppProxy", "submit"), 151 | "before", 152 | (callback) => { 153 | logInfo("NetworkApi submit " + callback.arg(0)) 154 | } 155 | ) 156 | 157 | // unhook when needed using this call 158 | hookerUnhook(); 159 | } 160 | ``` 161 | 162 | ## Interface builder 163 | Allows to build dynamic interface in the manager 164 | Create a menu: 165 | ```js 166 | im.create("settings", builder => { 167 | builder.text("Hello world!") 168 | } 169 | ``` 170 | 171 | ### Node 172 | - setAttribute(key: String, value: Object) -> Node : set a custom attribute of the node 173 | - padding(value: Integer) -> Node : set the padding of the node 174 | - color(value: Integer) -> Node : set the color of the node 175 | - fontSize(value: Integer) -> Node : set the font size of the node 176 | - label(value: String) -> Node : set the label of the node 177 | 178 | ### enum Arrangement 179 | start 180 | end 181 | top 182 | bottom 183 | center 184 | spaceBetween 185 | spaceAround 186 | spaceEvenly 187 | 188 | ### enum Alignment 189 | start 190 | end 191 | top 192 | bottom 193 | centerVertically 194 | centerHorizontally 195 | 196 | ### RowColumnNode 197 | - arrangement(value: Arrangement) -> RowColumnNode : set the arrangement of the node 198 | - alignment(value: Alignment) -> RowColumnNode : set the alignment of the node 199 | - spacedBy(value: Integer) -> RowColumnNode : set the spacing of the child nodes 200 | 201 | 202 | ### Builder functions: 203 | - row(builderCallback) -> RowColumnNode: create a row 204 | - column(builderCallback) -> RowColumnNode: create a column 205 | - text(content: String) -> Node : create a text view 206 | - switch(state: Boolean?, callback: (Boolean) -> Void) -> Node : create a switch node 207 | - button(label: String, callback: () -> Void) : create a button 208 | - slider(min: Int, max: Int, step: Int, value: Int, callback: (Integer) -> Void): create a integer slider 209 | - onLaunched(callback) : Trigger callback when the menu appear on the screen 210 | 211 | Example: 212 | ```js 213 | im.create("settings", builder => { 214 | builder.text("My Title").padding(10).color(0xff000000).fontSize(20) 215 | 216 | builder.row(builder => { 217 | var textView = builder.text("myValue : unknown") 218 | builder.slider(0, 100, 50, 1, value => { 219 | // set the textview value 220 | textView.label("myValue : " + value) 221 | }) 222 | }).spacedBy(10) 223 | 224 | builder.row(builder => { 225 | var mySwitchText = builder.text("mySwitch: unknown") 226 | builder.switch(false, value => { 227 | mySwitchText.label("mySwitch: " + switchValue) 228 | }) 229 | }).arrangement("spaceBetween").alignment("centerVertically") 230 | 231 | builder.onLaunched(() => { 232 | longToast("menu shown!") 233 | }) 234 | } 235 | ``` 236 | --------------------------------------------------------------------------------