├── .gitignore ├── .env.example ├── package.json ├── index.js ├── bypass captcha.md ├── README.md ├── docs.md └── snapbot.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | screenshot.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | USER_NAME = #YOUR_SNAPCHAT_USERNAME 2 | USER_PASSWORD = #YOUR_SNAPCHAT_PASSWORD 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "snapbot", 3 | "version": "1.0.0", 4 | "description": "A bot that automates snapchat ", 5 | "main": "index.js", 6 | "devDependencies": {}, 7 | "type": "module", 8 | "scripts": { 9 | "bot": "node index.js" 10 | }, 11 | "author": "Roger Rodrigues", 12 | "dependencies": { 13 | "dotenv": "^16.4.5", 14 | "puppeteer": "^24.1.1", 15 | "puppeteer-extra": "^3.3.6", 16 | "puppeteer-extra-plugin-stealth": "^2.11.2" 17 | }, 18 | "license": "ISC" 19 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import SnapBot from "./snapbot.js"; 2 | import dotenv from "dotenv"; 3 | dotenv.config(); 4 | 5 | const bot = new SnapBot(); 6 | 7 | let credentials = { 8 | username: process.env.USER_NAME, 9 | password: process.env.USER_PASSWORD, 10 | }; 11 | 12 | async function sendSnap() { 13 | await bot.launchSnapchat({ 14 | headless: false, // makes the browser visible 15 | args: [ 16 | "--start-maximized", 17 | "--force-device-scale-factor=1", 18 | "--allow-file-access-from-files", 19 | "--use-fake-ui-for-media-stream", // Bypass permission prompt 20 | "--enable-media-stream", 21 | ], 22 | // userDataDir: 23 | // "C:\\Users\\your_name\\AppData\\Local\\Google\\Chrome\\User Data\\Default", 24 | }); 25 | 26 | const isLogged = await bot.isLogged(); 27 | 28 | if (!isLogged) { 29 | await bot.login(credentials); 30 | } else { 31 | console.log("Bot is already Logged in"); 32 | } 33 | 34 | await bot.handlePopup(); 35 | await bot.captureSnap({ caption: "Hello world" }); 36 | await bot.screenshot({ path: "screenshot.png" }); 37 | await bot.send("BestFriends"); // or bot.useShortcut([) 38 | await bot.wait(2000); 39 | await bot.closeBrowser(); 40 | } 41 | 42 | sendSnap(); 43 | -------------------------------------------------------------------------------- /bypass captcha.md: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | # How to Bypass CAPTCHA with SnapBot 5 | 6 | If you're running SnapBot and encounter a **CAPTCHA during login**, follow these steps to bypass it: 7 | 8 | --- 9 | 10 | 11 | ### Step-by-Step Guide 12 | 13 | 1. **Run the Bot Once** 14 | Start SnapBot normally. If you see a CAPTCHA, **close the bot** for now. 15 | 16 | 2. **Log into Snapchat Web Manually** 17 | 18 | - Open your **regular browser** (Chrome recommended). 19 | 20 | - Go to [https://www.snapchat.com/web/](https://www.snapchat.com/web/) 21 | 22 | - Try to log in — if you see a CAPTCHA, **solve it manually**. 23 | 24 | 3. **Find Your Chrome Profile Path** 25 | 26 | - Locate your Chrome user data folder. 27 | Example for Windows: 28 | 29 | ``` 30 | C:\Users\YOUR_USERNAME\AppData\Local\Google\Chrome\User Data\Default 31 | ``` 32 | 33 | - Replace `YOUR_USERNAME` with your actual Windows username. 34 | 35 | 4. **Edit Your Code** 36 | In your SnapBot code, modify the `bot.launchSnapchat` call like this: 37 | 38 | ```js 39 | await bot.launchSnapchat({ 40 | headless: false, // Browser will be visible 41 | args: [ 42 | "--start-maximized", 43 | "--force-device-scale-factor=1", 44 | "--allow-file-access-from-files", 45 | "--use-fake-ui-for-media-stream", 46 | "--enable-media-stream", 47 | ], 48 | userDataDir: "C:\\Users\\YOUR_USERNAME\\AppData\\Local\\Google\\Chrome\\User Data\\Default", // Use your real Chrome profile 49 | }); 50 | ``` 51 | 52 | 5. **Run the Bot Again** 53 | Now when you run the bot, it should use the **same profile where the CAPTCHA was already solved**. 54 | 55 | 56 | --- 57 | 58 | ### Done! 59 | 60 | You’ve bypassed the CAPTCHA, and SnapBot should now work without interruptions. 61 | 62 | --- 63 | 64 | > 📌 Special thanks to **marcel** for pointing this out 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > **Thank You for Your Support!** 3 | > 4 | > Snapbot is a hobby Project of mine which I released almost a year ago and its gaining a lot of traction recently. If you find this project useful or are interested in futher development, please consider starring the repository. It helps me know that you're interested and encourages future development. Your support is greatly appreciated! 5 | 6 | > 7 | # SnapBot v2 8 | 9 | SnapBot is a modern, browser-level automation library designed specifically for Snapchat. 10 | Built on top of Puppeteer, SnapBot enables developers to create powerful Snapchat bots without relying on internal APIs or reverse engineering — all through the familiar browser environment. 11 | 12 | Whether you're automating daily snaps, managing multiple accounts, or building complex chat workflows, SnapBot abstracts the hard parts so you can focus on what matters: the logic and creativity behind your automation. 13 | 14 | ## 💼 Use Cases 15 | * Daily content distribution via snaps 16 | * Streak automation for agencies or influencers 17 | * CRM-style customer engagement bots 18 | * AI-integrated messaging workflows 19 | * Custom Snapchat tools for businesses 20 | 21 | ## 🚀 Features 22 | 23 | * **Image Uploads**: Send images directly through the bot. 24 | * **Full Chat Automation**: Automate sending and receiving messages. 25 | * **Snap Shortcuts**: Quickly send snaps to predefined contacts. 26 | * **Smart Notification Blocking**: Prevent unwanted notifications during automation. 27 | * **Advanced Contact Handling**: Manage contacts efficiently within the bot. 28 | * **Multiple Account Support**: Handle multiple Snapchat accounts simultaneously. 29 | * **Custom Captions**: Send snaps with personalized captions. 30 | * **Snapstreak Maintenance**: Ensure your streaks never break again. 31 | 32 | ## 📦 Installation 33 | 34 | 1. **Clone the repository**: 35 | 36 | ```bash 37 | git clone https://github.com/Emmanuel-Rods/SnapBot 38 | ``` 39 | 40 | 41 | 42 | 2. **Navigate to the project directory**: 43 | 44 | ```bash 45 | cd SnapBot 46 | ``` 47 | 48 | 49 | 50 | 3. **Install the necessary dependencies**: 51 | 52 | ```bash 53 | npm install 54 | ``` 55 | 56 | 57 | 58 | 4. **Set up environment variables**: 59 | 60 | ```bash 61 | cp .env.example .env 62 | ``` 63 | 64 | 65 | 66 | Edit the `.env` file and add your Snapchat credentials: 67 | 68 | ```env 69 | USER_NAME= 70 | USER_PASSWORD= 71 | ``` 72 | 73 | 74 | 75 | ## 🛠️ Usage 76 | 77 | To run the bot, use the following command: 78 | 79 | ```bash 80 | npm run bot 81 | ``` 82 | 83 | 84 | 85 | This command will start SnapBot, logging into Snapchat, capturing a snap, and sending it to your specified contacts. 86 | 87 | If you run into a CAPTCHA when using SnapBot, see this guide for a solution: 88 | [Bypass CAPTCHA](https://github.com/Emmanuel-Rods/SnapBot/blob/main/bypass%20captcha.md) 89 | 90 | ## 📚 Available Methods 91 | 92 | SnapBot provides a comprehensive set of methods to interact with Snapchat: 93 | 94 | * `launchSnapchat(config)`: Opens Snapchat in a browser. Set `headless` to `false` in the config to see the browser. 95 | * `login(credentials)`: Logs into Snapchat using the provided credentials. 96 | * `isLogged()`: Checks if the user is currently logged in. 97 | * `captureSnap(options)`: Takes a snap and applies a caption. 98 | * `send(person)`: Sends the snap to the specified recipient(s). 99 | * `closeBrowser()`: Closes the browser session. 100 | * `screenshot(options)`: Saves a screenshot of the current screen state. 101 | * `logout()`: Logs out of the current Snapchat account, allowing you to log in with another account without closing the browser. 102 | * `wait(time)`: Pauses the script for a specified duration (in milliseconds). 103 | * `openFriendRequests()`: Opens the friend requests section. 104 | * `listRecipients()`: Lists all available recipients. 105 | * `sendMessage(options)`: Sends a message with the specified options. 106 | * `saveCookies(username)`: Saves the current session cookies for the given username. 107 | * `useCookies(username)`: Loads saved session cookies for the given username. 108 | * `extractChatData(chatID)`: Extracts chat data for the specified chat ID. 109 | * `userStatus()`: Retrieves the current user's status. 110 | * `blockTypingNotifications(boolean)`: Enables or disables typing notifications based on the boolean value. 111 | * `useShortcut(shortcutsArray)`: Applies predefined shortcuts from the provided array. 112 | 113 | ## 📖 Documentation 114 | 115 | For detailed information on each method and advanced usage, please refer to the [docs.md](https://github.com/Emmanuel-Rods/SnapBot/blob/main/docs.md) file in the repository. 116 | 117 | 118 | ### 🤝 Contributing 119 | 120 | We welcome contributions! 121 | If you have ideas, feature requests, bug reports, or improvements for the documentation: 122 | 123 | * 📬 **Open an issue** on GitHub 124 | * ✉️ **Email suggestions** to [alewsor@gmail.com](mailto:alewsor@gmail.com) 125 | * 📝 **Help improve the README or `docs.md`** — every edit counts! 126 | 127 | Whether it’s code, feedback, or typo fixes — your support makes SnapBot better for everyone. 💛 128 | 129 | ### 🧩 Stability Notice 130 | 131 | SnapBot relies heavily on DOM selectors, which means updates to the Snapchat web interface can occasionally break its functionality. 132 | 133 | I’m actively working on an auto-update system to detect and adapt to these changes — and **your help is welcome**! 134 | 135 | If you're interested in helping build this system, feel free to: 136 | 137 | * 💬 Open an issue 138 | * 🛠️ Submit a pull request 139 | * ✉️ Reach out at [alewsor@gmail.com](mailto:alewsor@gmail.com) 140 | 141 | Together we can make SnapBot more stable and smarter. 🧠✨ 142 | 143 | ### 🚀 Showcase Your Bots 144 | 145 | Built something cool using SnapBot? 146 | We’d love to feature your creations in our showcase! 147 | Share your bots with us by opening an issue or sending an email to [alewsor@gmail.com](mailto:alewsor@gmail.com), and get your project highlighted for the community to see. 148 | 149 | 150 | ### 📝 TODO 151 | * [ ] Add comprehensive JSDoc comments for all methods and functions 152 | * [ ] Implement better error handling and user-friendly error messages 153 | 154 | 155 | > [!WARNING] 156 | > 157 | > This project is intended for educational and research purposes only. The developers and contributors are not responsible for any misuse or damages resulting from the use of this software. Users are solely responsible for ensuring their compliance with all applicable laws and terms of service. 158 | 159 | ## 📬 Contact 160 | 161 | For any queries or contributions, feel free to reach out at: [alewsor@gmail.com](mailto:alewsor@gmail.com) 162 | 163 | -------------------------------------------------------------------------------- /docs.md: -------------------------------------------------------------------------------- 1 | ## 🚀 Getting Started with SnapBot 2 | 3 | **SnapBot** is a modern, browser-level automation **library** designed specifically for Snapchat. 4 | Built on top of **Puppeteer**, SnapBot enables developers to create powerful Snapchat bots without relying on internal APIs or reverse engineering — all through the familiar browser environment. 5 | 6 | Whether you're automating daily snaps, managing multiple accounts, or building complex chat workflows, SnapBot abstracts the hard parts so you can focus on what matters: the logic and creativity behind your automation. 7 | 8 | ### 💼 Use Cases 9 | - Daily content distribution via snaps 10 | - Streak automation for agencies or influencers 11 | - CRM-style customer engagement bots 12 | - AI-integrated messaging workflows 13 | - Custom Snapchat tools for businesses 14 | 15 | ## 🚀 Getting Started (Developer Preview) 16 | 17 | At its core, SnapBot wraps Snapchat's browser interface into clean, async JavaScript functions. Below is an overview of the key methods you'll be using when building automation workflows. 18 | 19 | SnapBot exposes a set of methods that offer fine-grained control over Snapchat Web automation. Here's a list of the available methods: 20 | 21 | ### Available Methods 22 | 23 | - `launchSnapchat(config)` 24 | - `login(credentials)` 25 | - `isLogged()` 26 | - `captureSnap(options)` 27 | - `send(person)` 28 | - `closeBrowser()` 29 | - `screenshot(options)` 30 | - `logout()` 31 | - `wait(time)` 32 | - `openFriendRequests()` 33 | - `listRecipients()` 34 | - `sendMessage(options)` 35 | - `saveCookies(username)` 36 | - `useCookies(username)` 37 | - `extractChatData(chatID)` 38 | - `userStatus()` 39 | - `blockTypingNotifications(boolean)` 40 | - `useShortcut(shortcutsArray)` 41 | 42 | #### Importing and Initializing 43 | To get started, import the `SnapBot` class and initialize a new instance. 44 | ```js 45 | import SnapBot from "./snapbot.js"; 46 | const bot = new SnapBot(); 47 | ``` 48 | 49 | #### `launchSnapchat(options)` 50 | Launches a new Puppeteer browser session configured to work seamlessly with Snapchat’s web app. 51 | 52 | This method must be called before performing any other actions. It sets up the browser, grants media permissions, and navigates to the Snapchat home page. 53 | ##### Example: 54 | ```js 55 | 56 | await bot.launchSnapchat({ 57 | headless: false, 58 | userDataDir: "C:\\Users\\yourname\\AppData\\Local\\Google\\Chrome\\User Data\\Default", 59 | args: [ 60 | "--start-maximized", 61 | "--force-device-scale-factor=1", 62 | "--use-fake-ui-for-media-stream", 63 | "--allow-file-access-from-files", 64 | "--enable-media-stream", 65 | ], 66 | }); 67 | ``` 68 | 69 | **Parameters:** 70 | 71 | This method accepts any Puppeteer launch configuration object. You're free to customize it based on your needs — whether that's enabling debugging, using a persistent session, or tweaking performance settings. 72 | 73 | Common options include: 74 | 75 | - **`headless`** – Set to `false` if you want to see what the bot is doing. 76 | - **`userDataDir`** – Use this to persist login sessions between runs. 77 | - **`args`** – Chromium flags like camera/mic overrides, window size, etc. 78 | 79 | #### `login(credentials)` 80 | Logs into Snapchat using the given credentials. 81 | ##### Example: 82 | ```js 83 | let credentials = { 84 | username: "your_username", 85 | password: "your_password", 86 | } 87 | 88 | await bot.login(credentials) 89 | ``` 90 | 91 | **Parameters:** 92 | `username` (string): The Snapchat username or email. 93 | `password` (string): The corresponding password. 94 | 95 | #### `isLogged()` 96 | Checks whether the bot is currently logged into Snapchat. 97 | ##### Example: 98 | ```js 99 | const logged = await bot.isLogged() //returns a boolean 100 |   if (!logged) { 101 |     await bot.login(credentials); 102 |   } else { 103 |     console.log("bot is already logged"); 104 |   } 105 | ``` 106 | 107 | #### `wait(milliseconds)` 108 | Pauses the execution of the bot for a specified amount of time (in milliseconds). 109 | ##### Example: 110 | ```js 111 | await bot.wait(2000) //waits for 2 seconds 112 | ``` 113 | 114 | **Parameters:** 115 | `milliseconds` (`number`) — The amount of time to wait before continuing execution. Must be specified in milliseconds. 116 | 117 | #### `listRecipients()` 118 | Returns the list of the first ~20 visible chat recipients on the Snapchat web interface with their `Name`and `ChatID`. This limitation exists because Snapchat uses lazy rendering — only the chats currently visible in the viewport are available in the DOM. 119 | 120 | > A future update will include automatic scrolling to fetch the full list dynamically. 121 | 122 | ##### Example: 123 | ```js 124 | const recipients = await bot.listRecipients(); 125 | console.log(recipients); 126 | ``` 127 | 128 | ```js 129 | [ 130 |     { 131 |         "id": "0cc1a845-93d9-50fb-b79c-f64402ddfd58", 132 |         "name": "Roger Rodrigues💤" 133 |     }, 134 |     { 135 |         "id": "77cd80c1-a9c4-58ad-8219-611eaf28cbd6", 136 |         "name": "Shavina Barbosa" 137 |     }, 138 |     { 139 |         "id": "6091f50d-5821-51a7-9c39-462a5702e92f", 140 |         "name": "Rahul M" 141 |     }, 142 |  ] 143 | ``` 144 | 145 | **Returns:** 146 | - `Array` — Each object contains: 147 | - `name` (`string`) — The display name of the user 148 | - `id` (`string`) — The internal chat ID 149 | 150 | > [!IMPORTANT] 151 | > chatIDs: 152 | > `chatID`s are a crucial part of SnapBot's workflow. They uniquely identify each chat and are used extensively in methods like `sendMessage()`, `extractChatData()`, and others. Be sure to store or reference these IDs when interacting with recipients, as they form the backbone of most chat-related operations. 153 | 154 | #### `sendMessage(obj)` 155 | Sends a message to a recipient identified by their `chatID`. 156 | 157 | This method types and sends a message in an open chat. You can pass either a single string or an array of strings (to send multiline messages). After sending, the bot can either stay in the chat or exit based on the `exit` option. 158 | ##### Example: 159 | ```js 160 | await bot.sendMessage({ 161 | chat: chatID, 162 | message: ["Hey!", "This is a multiline message."], 163 | exit: true, // closes the chat after sending 164 | }); 165 | ``` 166 | 167 | **Parameters:** 168 | 169 | | Key | Type | Description | 170 | | --------- | -------------------- | ------------------------------------------------------------------------------------------------------- | 171 | | `chat` | `string` | The unique `chatID` of the recipient (see `listRecipients()` for how to obtain it). | 172 | | `message` | `string \| string[]` | The message to send. You can pass a single line or an array for multiline. | 173 | | `exit` | `boolean` | If `true`, the bot will close the chat window after sending the message. If `false`, it will stay open. | 174 | 175 | #### `extractChatData(chatID)` 176 | Extracts the full visible chat history from the currently open chatbox for the given chat ID. The data is grouped by day, showing both sent and received messages. 177 | ##### Example: 178 | 179 | ```js 180 | const id = "6091f50d-5821-51a7-9c39-462a5702e92f"; 181 | 182 | await bot.sendMessage({ 183 | chat: id, 184 | message: "This message was sent via bot, do not reply", 185 | exit: false, // keep chat open 186 | }); 187 | 188 | const chatHistory = await bot.extractChatData(id); 189 | await bot.wait(1000); 190 | //optionally save the chat locally 191 | fs.writeFileSync(`chats.json`, JSON.stringify(chatHistory, null, 2)); 192 | 193 | ``` 194 | 195 | **Returns:** 196 | An array of objects structured like this: 197 | ```js 198 | [ 199 | { 200 | time: "April 8", 201 | conversation: [ 202 | { from: "Me", text: "This message was sent via bot, do not reply" }, 203 | { from: "Rahul M", text: "That is crazy" } 204 | ] 205 | }, 206 | ] 207 | 208 | ``` 209 | 210 | **Parameters:** 211 | 212 | | Name | Type | Description | 213 | | ------ | -------- | ------------------------------------------------- | 214 | | chatID | `string` | The chat ID of the user (from `listRecipients()`) | 215 | 216 | > [!NOTE] 217 | > The chatbox must be open before calling this method. 218 | > You can use `bot.sendMessage({ chat: id, message: "", exit: false })` to open the chat without sending a message. 219 | 220 | #### `userStatus()` 221 | Fetches the current status of users you've recently interacted with. Returns an array of objects, where each object includes: 222 | - `id`: Unique identifier of the user (chatID). 223 | - `name`: Display name of the user. 224 | - `status`: An object containing: 225 | - `type`: Can be `"Delivered"`, `"Opened"`, or `"Received"` depending on the last interaction. 226 | - `time`: How long ago the interaction happened (e.g., `"2h"`, `"5h"`). 227 | - `streak`: Current Snapstreak as a string (e.g., `"430 🔥"`) or `null` if no streak. 228 | ##### Example: 229 | 230 | ```js 231 | const statusList = await bot.userStatus(); 232 | console.log(statusList); 233 | ``` 234 | 235 | ```js 236 | [ 237 | { 238 | id: "0cc1a845-93d9-50fb-b79c-f64402ddfd58", 239 | name: "Roger Rodrigues💤", 240 | status: { 241 | type: "Received", 242 | time: "2h", 243 | streak: "430 🔥" 244 | } 245 | }, 246 | { 247 | id: "77cd80c1-a9c4-58ad-8219-611eaf28cbd6", 248 | name: "Shavina Barbosa", 249 | status: { 250 | type: "Opened", 251 | time: "5h", 252 | streak: "307 🔥" 253 | } 254 | }, 255 | { 256 | id: "6091f50d-5821-51a7-9c39-462a5702e92f", 257 | name: "Rahul M", 258 | status: { 259 | type: "Delivered", 260 | time: "21h", 261 | streak: null 262 | } 263 | } 264 | ] 265 | ``` 266 | 267 | #### `captureSnap()` 268 | Captures and sends a snap with a caption. The snap is created using a local image file. 269 | 270 | This method simulates taking a snap using an image from your filesystem and allows you to overlay a caption on it. You can also control the vertical position of the caption. 271 | 272 | **Parameters:** 273 | - `path` (string) – The full path to the image file on your system. 274 | - `caption` (string) – The text you want to appear on the snap. 275 | - `position` (number, optional) – Position of the caption relative to the center: 276 | - `0` = exactly center 277 | - negative = moves the caption **up** 278 | - positive = moves the caption **down** 279 | 280 | By default, the caption will appear slightly below the center. 281 | 282 | ##### Example: 283 | 284 | ```js 285 |  await bot.captureSnap({ 286 |     path: "C:\\Users\\itsro\\Downloads\\hello.png", 287 |     caption: "Whats everyone doing?!", 288 |     position: 50, 289 |   }); 290 | ``` 291 | 292 | >[!CAUTION] 293 | >**Missing Caption Bug** 294 | > 295 | >Currently, if you don't provide a caption, the image **will appear in preview** but **won’t be sent properly**. Snapchat reverts to a blank/camera screen when sending without a caption. 296 | 297 | > [!TIP] 298 | > **Workaround:** 299 | > 300 | > To ensure the snap is sent, use a placeholder caption like an underscore `_` and push it out of view with a high `position` value: 301 | > ``` 302 | > { 303 | > path: "your/image/path", 304 | > caption: "_", 305 | > position: 500 306 | > } 307 | > ``` 308 | 309 | 310 | 311 | #### `send(audience)` 312 | Sends the currently captured snap to a selected group of recipients. 313 | 314 | This method allows you to programmatically send your snap to **friends**, **best friends**, or **groups**. Make sure you’ve already captured a snap using `bot.captureSnap()` before calling this. 315 | 316 | ##### Example: 317 | ```js 318 | await bot.captureSnap({ 319 |     path: "C:\\Users\\itsro\\Downloads\\happy.png", 320 |     caption: "You Are My BestFriend!", 321 |   }); 322 | await bot.send("bestfriends"); 323 | ``` 324 | 325 | **Parameters:** 326 | 327 | - `audience` → _(string)_ 328 |     Defines who should receive the snap. Accepted values: `"friends"`, `"bestfriends"`, `"groups"`. 329 | 330 | > [!note] 331 | The `"friends"` audience includes both **regular friends** and **best friends**. There's no need to specify them separately. 332 | 333 | > [!WARNING] 334 | > **Deprecated Soon** 335 | > 336 | > This `"send"` method and its system will be **deprecated soon**, as it's unreliable. 337 | > For example, **groups** can automatically be marked as **best friends** if you interact with them frequently—leading to unpredictable behavior when sending. 338 | 339 | #### `useShortcut(shortcutsArray)` 340 | Utilizes Snapchat's "Shortcuts" feature to send Snaps to predefined groups of friends represented by emojis. This allows for quick and efficient Snap distribution without selecting recipients individually. 341 | 342 | ```js 343 | await bot.useShortcut(["🔥", "👀"]); 344 | ``` 345 | 346 | **Parameters:** 347 | 348 | - `shortcutsArray` (`string[]`): An array of emoji strings, each representing a specific Shortcut group on Snapchat. 349 | 350 | > [!NOTE] 351 | > Shortcuts 352 | > 353 | > In Snapchat, Shortcuts are user-defined groups of friends identified by an emoji. They facilitate sending Snaps to multiple recipients simultaneously. Ensure that the emojis provided correspond to existing Shortcuts in your Snapchat account. 354 | 355 | > [!tip] 356 | > **Mobile Only:** 357 | > 358 | > Currently, Snapchat does **not** support creating Shortcuts via the web interface. 359 | > Users must create and manage their Shortcuts directly from the **Snapchat mobile app**. 360 | > 361 | > However, once created, these Shortcuts **can be used** through the bot with `useShortcut()`. 362 | 363 | 364 | #### `saveCookies(username)` 365 | Saves the current session cookies to a file named after the username. This allows you to persist sessions per account and avoid repeated logins. 366 | 367 | ##### Example: 368 | 369 | ```js 370 | await bot.saveCookies(credentials.username); 371 | // Creates a file: username-cookies.json 372 | ``` 373 | 374 | **Parameters:** 375 | 376 | - `username` _(string)_ — The account's username. Used to name the cookie file. 377 | 378 | #### `useCookies(username)` *(Deprecated / Non-functional)* 379 | 380 | > [!warning] 381 | > Deprecated & Non-functional 382 | > The `useCookies()` method was originally introduced to restore session data using cookies saved via `saveCookies()`. However, due to a fundamental design limitation—cookies being applied _after_ the browser was launched—this method was non-functional and failed to restore any authenticated session. 383 | > 384 | > This functionality has since been replaced by `launchSnapchat(chromeConfig, cookiefile)`, which correctly applies cookies **prior to browser launch**, ensuring proper session restoration. 385 | 386 | 387 | ##### Recommended usage: 388 | 389 | ```js 390 | await bot.launchSnapchat(chromeConfig, "yourUsername"); 391 | //the same thing you set in saveCookies("yourUsername") 392 | ``` 393 | 394 | > [!NOTE] 395 | > Cookie File Naming Convention 396 | > 397 | >The second argument in `bot.launchSnapchat(chromeConfig, "your-username")` acts as a **key** to locate the cookie file. 398 | >For example, setting `"xyz"` will read or create `xyz-cookies.json`. However, if you use the same key (`xyz`) across multiple Snapchat accounts, the cookie file will be overwritten each time — which can lead to incorrect sessions. 399 | > 400 | >**Best practice:** Use the actual Snapchat username as the key: 401 | >``` 402 | >await bot.launchSnapchat(chromeConfig, credentials.username); 403 | >``` 404 | >This ensures the correct cookies are automatically loaded based on the credentials, especially when switching between different accounts. 405 | 406 | 407 | #### `screenshot({ path })` 408 | Takes a screenshot of the current Snapchat screen. This method is a wrapper around Puppeteer's native `page.screenshot()`, so all configuration options supported by Puppeteer can be passed here. 409 | 410 | ##### Example: 411 | ```js 412 | await bot.screenshot({ path: "screenshot.png" }); 413 | ``` 414 | 415 | **Parameters:** 416 | 417 | | Name | Type | Description | 418 | | ---- | ------ | ------------------------------------------------------- | 419 | | path | string | File path to save the screenshot (e.g., `"./snap.png"`) | 420 | 421 | 422 | >[!note] 423 | > This is equivalent to `page.screenshot()`. You can use options like `fullPage`, `quality`, or `type` exactly as you would with Puppeteer. 424 | 425 | 426 | #### `logout()` 427 | Logs the user out of their Snapchat account by navigating to the logout endpoint. Once executed, the browser will be redirected to the Snapchat login page, effectively ending the session. 428 | ##### Example: 429 | ```js 430 | await bot.saveCookies(username) //saves the cookies 431 | await bot.logout(); 432 | ``` 433 | 434 | > [!note] 435 | > Snapchat Logout Behavior 436 | > 437 | > When you call `bot.logout()`, Snapchat **automatically redirects** the browser to the login screen. 438 | > If you call `bot.saveCookies()` **after** logging out, it will save the cookies of a **logged-out session**, which won't be useful for auto-login. 439 | > ✅ **Tip:** Always call `bot.saveCookies()` **before** logging out if you want to preserve an active login session for reuse later. 440 | 441 | 442 | #### `closeBrowser()` 443 | Closes the currently running browser instance. 444 | Use this at the end of your workflow to properly shut down the bot and free up system resources. 445 | ##### Example: 446 | ```js 447 | await bot.closeBrowser(); 448 | ``` 449 | 450 | >[!tip] 451 | You can safely call this at the end of your bot script to ensure everything is cleaned up. 452 | If you're planning to restart the bot or open a new session later, make sure you’ve saved any necessary data (like cookies or screenshots) **before** calling `closeBrowser()`. 453 | 454 | #### `blockTypingNotifications(bool)` 455 | Prevents Snapchat from sending the "User is typing..." indicator while the bot is interacting with chats. 456 | This is especially useful for maintaining a more professional presence — for example, seeing “Spotify is typing...” might feel off-brand or unpolished. 457 | 458 | ##### Example: 459 | ```js 460 | await bot.blockTypingNotifications(true); 461 | ``` 462 | 463 | > [!caution] 464 | Once enabled, this setting cannot be reverted during the current browser session. If you wish to bring back the typing indicator, you must **remove this line from your code** and **launch a fresh browser instance**. 465 | 466 | 467 | #### `openFriendRequests()` 468 | Navigates to the **Friend Requests** page on Snapchat. 469 | 470 | >[!note] 471 | This feature is still under development. While it successfully opens the friend request page, it currently does **not** accept or reject requests. 472 | > 473 | >The method hasn't been split into separate `accept()` or `reject()` methods yet due to testing limitations — primarily because a suitable test account with incoming requests wasn't available. 474 | > 475 | >Expect updates in future versions as more testing becomes possible. 476 | 477 | ##### Example: 478 | ```js 479 | await bot.openFriendRequests(); 480 | await bot.wait(1000); // give it a second to load fully 481 | await bot.screenshot({ path: "friend_requests.png" }); 482 | ``` 483 | 484 | >[!tip] 485 | > Until the ability to accept/reject is fully implemented, combining this method with a screenshot gives a nice workaround to **manually review** friend requests later. 486 | 487 | -------------------------------------------------------------------------------- /snapbot.js: -------------------------------------------------------------------------------- 1 | import puppeteer from "puppeteer-extra"; 2 | import Stealth from "puppeteer-extra-plugin-stealth"; 3 | 4 | puppeteer.use(Stealth()); 5 | 6 | import fs from "fs"; 7 | import fsPromise from "fs/promises"; 8 | 9 | function delay(time) { 10 | return new Promise(function (resolve) { 11 | setTimeout(resolve, time); 12 | }); 13 | } 14 | 15 | const lastTestedVersion = "v13.38.0"; 16 | 17 | export default class SnapBot { 18 | constructor() { 19 | this.page = null; 20 | this.browser = null; 21 | } 22 | async launchSnapchat(obj, cookiefile) { 23 | try { 24 | const options = { 25 | ...obj, 26 | // executablePath: "/usr/bin/google-chrome", // for docker 27 | }; 28 | this.browser = await puppeteer.launch(options); 29 | 30 | if (cookiefile) { 31 | try { 32 | const cookiesString = fs.readFileSync( 33 | `./${cookiefile}-cookies.json`, 34 | "utf-8" 35 | ); 36 | const cookies = JSON.parse(cookiesString); 37 | await this.browser.setCookie(...cookies); 38 | console.log("Cookies set"); 39 | } catch (error) { 40 | console.error("Error in using cookies", error); 41 | } 42 | } 43 | 44 | const context = this.browser.defaultBrowserContext(); 45 | 46 | await context.overridePermissions("https://web.snapchat.com", [ 47 | "camera", 48 | "microphone", 49 | ]); 50 | 51 | this.page = await context.newPage(); 52 | 53 | await this.page.setViewport({ 54 | width: 1920, 55 | height: 1080, 56 | deviceScaleFactor: 1, 57 | }); 58 | await this.page.setUserAgent( 59 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36" 60 | ); 61 | 62 | //gets the version 63 | this.page.on("console", (msg) => { 64 | if (msg.type() === "log") { 65 | const text = msg.text(); 66 | if (text.includes("Snapchat")) { 67 | console.log("Snapchat for Web Build info:", text); 68 | const version = text.match(/v\d+\.\d+\.\d+/); 69 | const currentVersion = version[0]; 70 | console.log("Version", currentVersion); 71 | //check version 72 | if (currentVersion != lastTestedVersion) { 73 | console.warn( 74 | `⚠️ Warning: Some methods were last tested on version ${lastTestedVersion} \n\n` + 75 | `Detected current version is ${currentVersion}\n\n` + 76 | `Some features might not work properly.\n` + 77 | `If you encounter issues, please try updating the project using 'git pull'.\n` + 78 | `If the problem persists, consider raising an issue or contacting the developer.` 79 | ); 80 | } 81 | } 82 | } 83 | }); 84 | 85 | await this.page.goto("https://www.snapchat.com/?original_referrer=none"); 86 | } catch (error) { 87 | console.error(`Error while Starting Snapchat : ${error}`); 88 | } 89 | } 90 | 91 | async login(credentials) { 92 | const { username, password } = credentials; 93 | if (username == "" || password == "") { 94 | throw new Error("Credentials cannot be empty"); 95 | } 96 | try { 97 | // Enter username 98 | const defaultLoginBtn = await this.page.$("#ai_input"); 99 | const loginBtn = await this.page.$('input[name="accountIdentifier"]'); 100 | 101 | if (loginBtn) { 102 | this.page.waitForNetworkIdle(); 103 | console.log("Entering username..."); 104 | await this.page.type('input[name="accountIdentifier"]', username, { 105 | delay: 100, 106 | }); 107 | } 108 | if (defaultLoginBtn) { 109 | console.log("Entering username..."); 110 | await this.page.type("#ai_input", username, { delay: 100 }); 111 | } 112 | 113 | await this.page.click("button[type='submit']"); 114 | } catch (e) { 115 | console.log("Username field error:", e); 116 | } 117 | try { 118 | //Enter Password 119 | console.log("Waiting for password field..."); 120 | await this.page.waitForSelector("#password", { 121 | visible: true, 122 | timeout: 60000, 123 | }); 124 | await this.page.type("#password", password, { delay: 100 }); 125 | console.log("Password field filled."); 126 | } catch (e) { 127 | console.log("Password field loading error:", e); 128 | } 129 | 130 | await this.page.click("button[type='submit']"); 131 | await delay(10000); 132 | //click not now 133 | try { 134 | const notNowBtn = "button.NRgbw.eKaL7.Bnaur"; 135 | console.log("Checking for 'Not now' button..."); 136 | await this.page.waitForSelector(notNowBtn, { 137 | visible: true, 138 | timeout: 5000, 139 | }); 140 | await this.page.click(notNowBtn); 141 | console.log("Clicked 'Not now' button."); 142 | } catch (e) { 143 | console.log("Popup handling error or popup not found:", e); 144 | } 145 | await delay(1000); 146 | } 147 | 148 | async isLogged() { 149 | const defaultLoginBtn = await this.page.$("#ai_input"); 150 | const loginBtn = await this.page.$('input[name="accountIdentifier"]'); 151 | 152 | if (defaultLoginBtn || loginBtn) { 153 | return false; 154 | } 155 | return true; 156 | } 157 | 158 | async handlePopup() { 159 | try { 160 | const notNowBtn = "button.NRgbw.eKaL7.Bnaur"; 161 | const notNowBtnHandle = await this.page.waitForSelector(notNowBtn, { 162 | visible: true, 163 | timeout: 5000, 164 | }); 165 | console.log("Checking for 'Not now' button..."); 166 | if (notNowBtnHandle) { 167 | await this.page.waitForSelector(notNowBtn, { 168 | visible: true, 169 | timeout: 5000, 170 | }); 171 | await this.page.click(notNowBtn); 172 | console.log("Clicked 'Not now' button."); 173 | } else { 174 | console.log("not found"); 175 | } 176 | } catch (error) { 177 | console.log(`could not find Popup`); 178 | } 179 | } 180 | 181 | async captureSnap(obj) { 182 | try { 183 | //updated version here v2.0 184 | let captureBtnFound = false; 185 | const captureButtonSelector = "button.FBYjn.gK0xL.A7Cr_.m3ODJ"; 186 | 187 | const captureButton = await this.page.$(captureButtonSelector); 188 | if (captureButton) { 189 | const isVisible = (await captureButton.boundingBox()) !== null; 190 | if (isVisible) { 191 | await captureButton.click(); 192 | captureBtnFound = true; 193 | } 194 | } 195 | if (!captureBtnFound) { 196 | const svgButtonSelector = "button.qJKfS"; 197 | await delay(1000); 198 | 199 | let isSVGbuttonFound = null; 200 | let retries = 0; 201 | const maxRetries = 3; 202 | 203 | while (!isSVGbuttonFound && retries < maxRetries) { 204 | try { 205 | isSVGbuttonFound = await this.page.waitForSelector( 206 | svgButtonSelector, 207 | { 208 | visible: true, 209 | timeout: 15000, 210 | } 211 | ); 212 | } catch (error) { 213 | console.log("Couldn't find the SVG selector, retrying..."); 214 | } 215 | 216 | if (!isSVGbuttonFound) { 217 | retries++; 218 | console.log(`Retries left: ${maxRetries - retries}`); 219 | await delay(1000); 220 | } 221 | } 222 | 223 | if (isSVGbuttonFound) { 224 | await this.page.click(svgButtonSelector); 225 | console.log("clicked svg button"); 226 | } else { 227 | console.log("SVG button not found after maximum retries."); 228 | } 229 | // Capture button 230 | if (isSVGbuttonFound) { 231 | await delay(1000); 232 | const captureButtonSelector = "button.FBYjn.gK0xL.A7Cr_.m3ODJ"; 233 | const captureButton = await this.page.waitForSelector( 234 | captureButtonSelector, 235 | { visible: true } 236 | ); 237 | await captureButton.click(); 238 | console.log("✅ Clicked the capture button"); 239 | } 240 | } 241 | 242 | await delay(3000); 243 | 244 | // 📸 Add custom image if `obj.path` exists 245 | if (obj.path) { 246 | try { 247 | const imageToBase64 = await fsPromise.readFile(obj.path, "base64"); 248 | const imageData = `data:image/png;base64,${imageToBase64}`; 249 | 250 | // Wait for container 251 | const containerSelector = "#snap-preview-container"; 252 | await this.page.waitForSelector(containerSelector, { visible: true }); 253 | 254 | await this.page.evaluate( 255 | (containerSelector, imageData) => { 256 | const container = document.querySelector(containerSelector); 257 | if (container) { 258 | const img = container.querySelector("img"); 259 | if (img) img.src = imageData; 260 | } 261 | }, 262 | containerSelector, 263 | imageData 264 | ); 265 | // await this.page.evaluate((imageData) => { 266 | // const img = document.querySelector("#snap-preview-container img"); 267 | // if (img) img.src = imageData; // if imageData is already available in the page 268 | // }, imageData); 269 | 270 | console.log("✅ Image added successfully"); 271 | } catch (error) { 272 | console.warn("⚠️ Error adding image:", error); 273 | } 274 | } 275 | 276 | await delay(1000); 277 | 278 | // 📝 Add caption if provided 279 | if (obj.caption) { 280 | await delay(2000); 281 | const captionButtonSelector = 'button[title="Add a caption"]'; 282 | await this.page.waitForSelector(captionButtonSelector, { 283 | visible: true, 284 | }); 285 | await this.page.click(captionButtonSelector); 286 | 287 | await delay(1000); 288 | const textareaSelector = 'textarea.B9QiX[aria-label="Caption Input"]'; 289 | await this.page.waitForSelector(textareaSelector, { visible: true }); 290 | await this.page.type(textareaSelector, obj.caption, { delay: 100 }); 291 | 292 | console.log("✅ Caption added successfully"); 293 | 294 | await delay(1000); 295 | 296 | //caption pos 297 | if (obj.position) { 298 | const elementHandle = await this.page.$(textareaSelector); 299 | if (elementHandle) { 300 | const box = await elementHandle.boundingBox(); 301 | if (box) { 302 | const startX = box.x + box.width / 2; 303 | const startY = box.y + box.height / 2; 304 | const endY = startY + obj.position; 305 | 306 | await this.page.mouse.move(startX, startY); // Move to starting position 307 | await this.page.mouse.down(); // Click and hold (start drag) 308 | await this.page.mouse.move(startX, endY, { steps: 10 }); // Drag smoothly 309 | await this.page.mouse.up(); // Release (drop) 310 | } 311 | } 312 | } 313 | } 314 | } catch (error) { 315 | console.error("❌ Error in capturing the snap:", error); 316 | } 317 | } 318 | 319 | async send(person) { 320 | try { 321 | const button = await this.page.$("button.YatIx.fGS78.eKaL7.Bnaur"); //updated this 322 | 323 | if (button) { 324 | console.log("Button found!"); 325 | await button.click(); 326 | } else { 327 | console.log("Button not found."); 328 | } 329 | await delay(1000); 330 | let selected = ""; 331 | person = person.toLowerCase(); 332 | if (person == "bestfriends") { 333 | selected = "ul.UxcmY li div.Ewflr.cDeBk.A8BRr "; 334 | } else if (person == "groups") { 335 | selected = "li div.RbA83"; 336 | } else if (person == "friends") { 337 | selected = "li div.Ewflr"; 338 | } else if (person == "all") { 339 | console.log("not implemented yet"); 340 | } else { 341 | throw new Error("Option not found"); 342 | } 343 | const accounts = await this.page.$$(selected); 344 | for (const account of accounts) { 345 | const isFriendVisible = await account.evaluate( 346 | (el) => el.offsetWidth > 0 && el.offsetHeight > 0 347 | ); // Check if the div is visible 348 | if (isFriendVisible) { 349 | await account.click(); // Click on the div element 350 | } else { 351 | console.log("account not found."); 352 | } 353 | } 354 | const sendButton = await this.page.$("button[type='submit']"); 355 | await sendButton.click(); 356 | delay(5000); 357 | } catch (error) { 358 | console.error("Error while sending snap", error); 359 | } 360 | } 361 | 362 | async closeBrowser() { 363 | await delay(5000); 364 | await this.browser.close(); 365 | console.log("Snapchat closed"); 366 | } 367 | 368 | async screenshot(obj) { 369 | await this.page.screenshot(obj); 370 | } 371 | 372 | async logout() { 373 | await this.page.waitForSelector("#downshift-1-toggle-button"); 374 | await this.page.click("#downshift-1-toggle-button"); 375 | await this.page.click("#downshift-1-item-9"); 376 | console.log("Logged Out"); 377 | await delay(12000); 378 | } 379 | 380 | async wait(time) { 381 | return new Promise(function (resolve) { 382 | setTimeout(resolve, time); 383 | }); 384 | } 385 | //beta 386 | async openFriendRequests() { 387 | await this.page.waitForSelector('button[title="View friend requests"]'); 388 | const requests = await this.page.$('button[title="View friend requests"]'); 389 | await requests.click(); 390 | } 391 | 392 | async listRecipients() { 393 | await this.page.waitForSelector( 394 | "div.ReactVirtualized__Grid__innerScrollContainer" 395 | ); 396 | const lists = await this.page.$$("div[role='listitem']"); 397 | const data = []; 398 | 399 | for (const listItem of lists) { 400 | const titleSpan = await listItem.$("span[id^='title-']"); 401 | if (titleSpan) { 402 | let id = await this.page.evaluate((el) => el.id, titleSpan); 403 | const name = await this.page.evaluate( 404 | (el) => el.textContent.trim(), 405 | titleSpan 406 | ); 407 | id = id.replace(/^title-/, ""); 408 | data.push({ id, name }); 409 | } 410 | 411 | //status 412 | } 413 | 414 | // console.log(data); 415 | return data; 416 | } 417 | 418 | async sendMessage(obj) { 419 | await this.page.waitForSelector( 420 | "div.ReactVirtualized__Grid__innerScrollContainer" 421 | ); 422 | const lists = await this.page.$$("div[role='listitem']"); 423 | 424 | for (const listItem of lists) { 425 | const titleSpan = await listItem.$("span[id^='title-']"); 426 | if (titleSpan) { 427 | const id = await this.page.evaluate((el) => el.id, titleSpan); 428 | let chatID = "title-" + obj.chat; 429 | if (id === chatID) { 430 | if (!obj.alreadyOpen) { 431 | await titleSpan.click(); 432 | } 433 | 434 | if (obj.message === "") { 435 | // const cleanedID = obj.chat.replace(/^title-/, ""); 436 | // return this.extractChatData(cleanedID); 437 | } 438 | 439 | // if its an array 440 | if (Array.isArray(obj.message)) { 441 | for (let msg of obj.message) { 442 | await this.page.waitForSelector('div[role="textbox"].euyIb'); 443 | await this.page.type('div[role="textbox"].euyIb', `${msg}`); 444 | await this.page.keyboard.press("Enter"); 445 | } 446 | } 447 | //if string 448 | if (typeof obj.message == "string") { 449 | await this.page.waitForSelector('div[role="textbox"].euyIb'); 450 | await this.page.type('div[role="textbox"].euyIb', obj.message, { 451 | delay: 200, 452 | }); 453 | await this.page.keyboard.press("Enter"); 454 | } 455 | 456 | if (obj.exit) { 457 | // await delay(300); 458 | await titleSpan.click(); // go back 459 | } 460 | } 461 | } 462 | } 463 | } 464 | 465 | async saveCookies(username) { 466 | try { 467 | const cookies = await this.browser.cookies(); 468 | fs.writeFileSync( 469 | `./${username}-cookies.json`, 470 | JSON.stringify(cookies, null, 2) 471 | ); 472 | console.log("cookies saved for : ", username); 473 | } catch (error) { 474 | console.error("Error in saving cookies", error); 475 | } 476 | } 477 | 478 | async useCookies(username) { 479 | try { 480 | const cookiesString = fs.readFileSync(`./${username}-cookies.json`); 481 | const cookies = JSON.parse(cookiesString); 482 | await this.browser.setCookie(...cookies); 483 | } catch (error) { 484 | console.error("Error in using cookies", error); 485 | } 486 | } 487 | 488 | async extractChatData(userId) { 489 | return await this.page.evaluate((userId) => { 490 | const output = []; 491 | const $chatList = document.querySelector(`#cv-${userId}`); 492 | if (!$chatList) return []; 493 | 494 | const listItems = $chatList.querySelectorAll("li.T1yt2"); 495 | 496 | let currentTime = null; 497 | let currentConvo = { time: "", conversation: [] }; 498 | 499 | listItems.forEach((li) => { 500 | const timeElem = li.querySelector("time span"); 501 | if (timeElem) { 502 | if (currentTime) output.push({ ...currentConvo }); 503 | currentTime = timeElem.textContent.trim(); 504 | currentConvo = { time: currentTime, conversation: [] }; 505 | return; 506 | } 507 | 508 | const messageBlocks = li.querySelectorAll("li"); 509 | 510 | if (messageBlocks.length > 0) { 511 | messageBlocks.forEach((block) => { 512 | let sender = 513 | block.querySelector("header .nonIntl")?.textContent.trim() || ""; 514 | 515 | if (!sender) { 516 | const borderElem = block.querySelector(".KB4Aq"); 517 | if (borderElem) { 518 | const color = getComputedStyle(borderElem).borderColor; 519 | if (color === "rgb(242, 60, 87)") sender = "Me"; 520 | else if (color === "rgb(14, 173, 255)") sender = "Eren Yeager"; 521 | else sender = "Unknown"; 522 | } 523 | } 524 | 525 | const texts = Array.from(block.querySelectorAll("span.ogn1z")).map( 526 | (span) => span.textContent.trim() 527 | ); 528 | 529 | texts.forEach((text) => { 530 | if (text) currentConvo.conversation.push({ from: sender, text }); 531 | }); 532 | }); 533 | } else { 534 | const borderElem = li.querySelector(".KB4Aq"); 535 | let sender = "Unknown"; 536 | 537 | if (borderElem) { 538 | const color = getComputedStyle(borderElem).borderColor; 539 | sender = color === "rgb(242, 60, 87)" ? "Me" : "Unknown"; 540 | } 541 | 542 | const text = li.querySelector("span.ogn1z")?.textContent.trim(); 543 | if (text) currentConvo.conversation.push({ from: sender, text }); 544 | } 545 | }); 546 | 547 | if (currentConvo.conversation.length > 0) { 548 | output.push(currentConvo); 549 | } 550 | 551 | return output; 552 | }, userId); 553 | } 554 | 555 | async userStatus() { 556 | await this.page.waitForSelector( 557 | "div.ReactVirtualized__Grid__innerScrollContainer" 558 | ); 559 | const lists = await this.page.$$("div[role='listitem']"); 560 | const data = []; 561 | 562 | for (const listItem of lists) { 563 | const titleSpan = await listItem.$("span[id^='title-']"); 564 | if (titleSpan) { 565 | const id = await this.page.evaluate((el) => el.id, titleSpan); 566 | const name = await this.page.evaluate( 567 | (el) => el.textContent.trim(), 568 | titleSpan 569 | ); 570 | 571 | // Get the status span container using the ID 572 | const cleanedID = id.replace(/^title-/, ""); 573 | const statusContainer = await listItem.$(`#status-${cleanedID}`); 574 | const statusParent = statusContainer 575 | ? await this.page.evaluateHandle( 576 | (el) => el.parentElement, 577 | statusContainer 578 | ) 579 | : null; 580 | let status = []; 581 | 582 | if (statusParent) { 583 | const statusSpans = await statusParent.$$("span"); 584 | status = await Promise.all( 585 | statusSpans.map((span) => 586 | this.page.evaluate((el) => el.textContent.trim(), span) 587 | ) 588 | ); 589 | } 590 | let cleanedStatus = [ 591 | ...new Set( 592 | status 593 | .map((text) => text?.trim()) 594 | .filter((text) => text && text !== "·") 595 | ), 596 | ]; 597 | 598 | let structuredStatus = { 599 | type: cleanedStatus[0] || null, 600 | time: cleanedStatus[1] || null, 601 | streak: cleanedStatus[2] || null, 602 | }; 603 | 604 | data.push({ id: cleanedID, name, status: structuredStatus }); 605 | } 606 | } 607 | return data; 608 | } 609 | 610 | async blockTypingNotifications(shouldBlock) { 611 | const client = await this.page.createCDPSession(); 612 | 613 | await client.send("Fetch.enable", { 614 | patterns: [ 615 | { 616 | urlPattern: "*SendTypingNotification*", 617 | requestStage: "Request", 618 | }, 619 | ], 620 | }); 621 | 622 | client.on("Fetch.requestPaused", async (event) => { 623 | const url = event.request.url; 624 | 625 | if ( 626 | shouldBlock && 627 | url.includes( 628 | "https://web.snapchat.com/messagingcoreservice.MessagingCoreService/SendTypingNotification" 629 | ) 630 | ) { 631 | // console.log("[CDPBlock] Aborting request:", url); 632 | await client.send("Fetch.failRequest", { 633 | requestId: event.requestId, 634 | errorReason: "Failed", 635 | }); 636 | } else { 637 | await client.send("Fetch.continueRequest", { 638 | requestId: event.requestId, 639 | }); 640 | } 641 | }); 642 | } 643 | 644 | //select 645 | async useShortcut(shortcutsArray) { 646 | const button = await this.page.$("button.YatIx.fGS78.eKaL7.Bnaur"); 647 | if (button) { 648 | console.log("Send Button found!"); 649 | await button.click(); 650 | } else { 651 | console.log("Send Button not found."); 652 | } 653 | await delay(2000); 654 | for (const emoji of shortcutsArray) { 655 | const clicked = await this.page.$$eval( 656 | "div.THeKv button", 657 | (buttons, emoji) => { 658 | const btn = buttons.find((b) => b.textContent.trim() === emoji); 659 | if (btn) { 660 | btn.click(); 661 | //now press the select 662 | return true; 663 | } 664 | return false; 665 | }, 666 | emoji 667 | ); 668 | 669 | if (clicked) { 670 | await this.page.waitForSelector("button.Y7u8A"); 671 | await this.page.click("button.Y7u8A"); 672 | const reclick = await this.page.$$eval( 673 | "div.THeKv button", 674 | (buttons, emoji) => { 675 | const btn = buttons.find((b) => b.textContent.trim() === emoji); 676 | if (btn) { 677 | btn.click(); 678 | return true; 679 | } 680 | return false; 681 | }, 682 | emoji 683 | ); 684 | } 685 | if (!clicked) { 686 | console.warn(`Shortcut "${emoji}" not found.`); 687 | } 688 | } 689 | //send button 690 | 691 | const sendButton = await this.page.$("button[type='submit']"); 692 | await sendButton.click(); 693 | } 694 | 695 | // add custom methods 696 | static extend(methods) { 697 | Object.assign(SnapBot.prototype, methods); 698 | } 699 | } 700 | --------------------------------------------------------------------------------