├── 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 |
--------------------------------------------------------------------------------