├── .babelrc ├── .eslintrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── assets ├── bd-plugins │ └── arRPCBridge.plugin.js ├── bd-themes │ └── HideDownloadAppsButton.theme.css ├── chrome │ ├── _locales │ │ ├── de │ │ │ └── messages.json │ │ └── en │ │ │ └── messages.json │ ├── logo.png │ ├── manifest.json │ ├── options.html │ └── options.js ├── gh-readme │ └── chrome-load-unpacked.png ├── mime-db.json ├── scripts │ └── VfsTool.ps1 └── spinner.example.webm ├── backend ├── jsconfig.json ├── package.json ├── src │ ├── index.js │ └── modules │ │ └── loadingscreen.js └── webpack.config.js ├── common ├── constants.js ├── dom.js ├── ipc.js └── logger.js ├── frontend ├── jsconfig.json ├── package.json ├── src │ ├── app_shims │ │ ├── discordnative.js │ │ ├── electron.js │ │ └── process.js │ ├── index.js │ ├── modules │ │ ├── asar.js │ │ ├── bdasarupdater.js │ │ ├── bdpreload.js │ │ ├── discordmodules.js │ │ ├── events.js │ │ ├── fetch.js │ │ ├── fetch │ │ │ └── nativefetch.js │ │ ├── ipc.js │ │ ├── ipcrenderer.js │ │ ├── localstorage.js │ │ ├── module.js │ │ ├── patches.js │ │ ├── runtimeinfo.js │ │ ├── runtimeoptions.js │ │ ├── startup.js │ │ ├── utilities.js │ │ └── webpack.js │ ├── native_shims │ │ └── discord_voice.js │ └── node_shims │ │ ├── base64-js.js │ │ ├── buffer.js │ │ ├── fs.js │ │ ├── fsentry.js │ │ ├── https.js │ │ ├── ieee754.js │ │ ├── mime-types.js │ │ ├── path.js │ │ ├── request.js │ │ ├── require.js │ │ └── vm.js └── webpack.config.js ├── licenses ├── arrpc │ ├── LICENSE │ └── arrpc.txt ├── asar-peeker │ ├── LICENSE │ └── asar-peeker.txt ├── base64-js │ ├── LICENSE.txt │ └── base64-js.txt ├── betterdiscord │ ├── LICENSE │ └── betterdiscord.txt ├── buffer │ ├── LICENSE │ └── buffer.txt ├── ieee754 │ ├── LICENSE │ └── ieee754.txt ├── mime-db │ ├── LICENSE │ └── mime-db.txt └── mime-types │ ├── LICENSE │ └── mime-types.txt ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── preload ├── jsconfig.json ├── package.json ├── src │ └── index.js └── webpack.config.js └── service ├── jsconfig.json ├── package.json ├── src ├── index.js └── modules │ ├── declarativenetrequest.js │ ├── dnrdebug.js │ └── fetch.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-proposal-class-properties", 4 | "@babel/plugin-proposal-optional-chaining" 5 | ] 6 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "webextensions": true, 5 | "browser": true, 6 | "node": true, 7 | "es2020": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 2022, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | }, 16 | "rules": { 17 | "accessor-pairs": "error", 18 | "block-spacing": ["error", "never"], 19 | "brace-style": ["error", "stroustrup", {"allowSingleLine": true}], 20 | "curly": ["error", "multi-line", "consistent"], 21 | "dot-location": ["error", "property"], 22 | "dot-notation": "error", 23 | "func-call-spacing": "error", 24 | "handle-callback-err": "error", 25 | "key-spacing": "error", 26 | "keyword-spacing": "error", 27 | "new-cap": ["error", {"newIsCap": true}], 28 | "no-array-constructor": "error", 29 | "no-case-declarations": "warn", 30 | "no-caller": "error", 31 | "no-console": "error", 32 | "no-duplicate-imports": "error", 33 | "no-else-return": "error", 34 | "no-eval": "error", 35 | "no-floating-decimal": "error", 36 | "no-implied-eval": "error", 37 | "no-iterator": "error", 38 | "no-label-var": "error", 39 | "no-labels": "error", 40 | "no-lone-blocks": "error", 41 | "no-mixed-spaces-and-tabs": "error", 42 | "no-multi-spaces": "error", 43 | "no-multi-str": "error", 44 | "no-new": "error", 45 | "no-new-func": "error", 46 | "no-new-object": "error", 47 | "no-new-wrappers": "error", 48 | "no-octal-escape": "error", 49 | "no-path-concat": "error", 50 | "no-proto": "error", 51 | "no-prototype-builtins": "off", 52 | "no-redeclare": ["error", {"builtinGlobals": true}], 53 | "no-self-compare": "error", 54 | "no-sequences": "error", 55 | "no-shadow": ["warn", {"builtinGlobals": false, "hoist": "functions"}], 56 | "no-tabs": "error", 57 | "no-template-curly-in-string": "error", 58 | "no-throw-literal": "error", 59 | "no-undef": "error", 60 | "no-undef-init": "error", 61 | "no-unmodified-loop-condition": "error", 62 | "no-unneeded-ternary": "error", 63 | "no-useless-call": "error", 64 | "no-useless-computed-key": "error", 65 | "no-useless-constructor": "error", 66 | "no-useless-rename": "error", 67 | "no-var": "error", 68 | "no-whitespace-before-property": "error", 69 | "object-curly-spacing": ["error", "never", {"objectsInObjects": false}], 70 | "object-property-newline": ["error", {"allowAllPropertiesOnSameLine": true}], 71 | "operator-linebreak": ["error", "none", {"overrides": {"?": "before", ":": "before"}}], 72 | "prefer-const": "error", 73 | "quote-props": ["error", "consistent-as-needed", {"keywords": true}], 74 | "quotes": ["error", "double", {"allowTemplateLiterals": true}], 75 | "rest-spread-spacing": "error", 76 | "semi": "error", 77 | "semi-spacing": "error", 78 | "space-before-blocks": "error", 79 | "space-in-parens": "error", 80 | "space-infix-ops": "error", 81 | "space-unary-ops": ["error", {"words": true, "nonwords": false, "overrides": {"typeof": false}}], 82 | "spaced-comment": ["error", "always", {"exceptions": ["-", "*"]}], 83 | "template-curly-spacing": "error", 84 | "wrap-iife": ["error", "inside"], 85 | "yield-star-spacing": "error", 86 | "yoda": "error" 87 | }, 88 | "globals": { 89 | "DiscordNative": "readonly", 90 | "__non_webpack_require__": "readonly" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: BdBrowser CI Build 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | tags: [ "**" ] 10 | 11 | pull_request: 12 | branches: [ "master" ] 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout source-code 19 | uses: actions/checkout@v4 20 | 21 | - name: Use pnpm 22 | uses: pnpm/action-setup@v3 23 | with: 24 | version: 8.x 25 | 26 | - name: Use Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 19.x 30 | cache: pnpm 31 | 32 | - name: Install dependencies 33 | run: pnpm install --frozen-lockfile 34 | 35 | - name: Build BdBrowser 36 | run: pnpm run build-prod 37 | 38 | - name: Upload artifacts 39 | uses: actions/upload-artifact@v4 40 | with: 41 | name: bdbrowser-extension 42 | path: dist/ 43 | retention-days: 30 44 | if-no-files-found: error 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /.idea 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Strencher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BdBrowser 2 | 3 | BdBrowser is a Chrome extension that loads [BetterDiscord](https://github.com/BetterDiscord/BetterDiscord) in Discord's web client. 4 | 5 | ![GitHub manifest version](https://img.shields.io/github/manifest-json/v/tsukasa/BdBrowser?filename=assets%2Fchrome%2Fmanifest.json&style=for-the-badge) 6 | 7 | ## 🗺 Table of Contents 8 | 9 | - [Features](#-features) 10 | - [Installation](#-installation) 11 | - [Installing Prebuilt Version](#installing-prebuilt-version) 12 | - [Building It Yourself](#building-it-yourself) 13 | - [Using BdBrowser](#-using-bdbrowser) 14 | - [First Launch](#first-launch) 15 | - [Installing Plugins and Themes](#installing-plugins-and-themes) 16 | - [Updating Plugins or Themes](#updating-plugins-or-themes) 17 | - [Updating BetterDiscord](#updating-betterdiscord) 18 | - [Updating BdBrowser](#updating-bdbrowser) 19 | - [Uninstalling BdBrowser](#uninstalling-bdbrowser) 20 | - [Extension Options](#extension-options) 21 | - [Backing up the Virtual Filesystem](#backing-up-the-virtual-filesystem) 22 | - [Restoring from a Backup](#restoring-from-a-backup) 23 | - [Formatting the Virtual Filesystem](#formatting-the-virtual-filesystem) 24 | - [Deleting Files in the Virtual Filesystem](#deleting-files-in-the-virtual-filesystem) 25 | - [Restricting Extension Site Access](#restricting-extension-site-access) 26 | - [Using VfsTool](#-using-vfstool) 27 | - [Extracting Backup Files](#extracting-backup-files) 28 | - [Creating Backup Files](#creating-backup-files) 29 | 30 |   31 | 32 | ## 👓 Features 33 | 34 | * [Manifest V3](https://developer.chrome.com/docs/extensions/mv3/intro/) extension for [Chromium](https://www.chromium.org)-based browsers. 35 | * Enables the use of [BetterDiscord](https://github.com/BetterDiscord/BetterDiscord)'s unmodified `betterdiscord.asar` in a web browser. 36 | * Emulates a virtual filesystem in memory and persists changes in an IndexedDB. 37 | 38 | Plugins do not have access to your real filesystem. 39 | * Service Worker handles outgoing web requests from BetterDiscord components to prevent CORS issues. 40 | * Compatible with all BetterDiscord themes. 41 | * Compatible with most* BetterDiscord plugins. 42 | 43 |   44 | 45 | ## 🛠 Installation 46 | 47 | You can either use the ready-made files present in this repository or build BdBrowser yourself. 48 | The latter is useful if you want to audit the code or make changes to it. 49 | 50 |   51 | 52 | ### Installing Prebuilt Version 53 | 54 | Since the extension is intentionally not available on the Chrome Web Store, you have to download and install it 55 | manually by downloading a prebuilt release from the GitHub releases page, extracting the archive and loading the 56 | resulting folder as an unpacked Chrome extension while developer mode is enabled: 57 | 58 | 1. Download the [latest prebuilt extension release as a zip archive](../../releases/latest/). 59 | 2. Extract the contents of the zip archive into an empty folder. 60 | 3. Open your Chrome extensions page. 61 | 4. Make sure the "Developer mode" toggle on the extension page is enabled. 62 | This enables you to load unpacked extensions from a local folder. 63 | 5. Click the Load unpacked button in the toolbar: 64 | 65 | ![Load unpacked extension](assets/gh-readme/chrome-load-unpacked.png) 66 | 67 | Now select the folder from step 2 you extracted the archive contents into. 68 | 6. Congratulations, you should see the extension loaded and working! 69 | Once you reload your [Discord](https://discord.com/channels/@me) tab, BetterDiscord should load and show the changelog. 70 | 71 |   72 | 73 | ### Building It Yourself 74 | 75 | Building BdBrowser yourself comes with a few prerequisites: 76 | 77 | - [Git](https://git-scm.com) 78 | - [Node.js](https://nodejs.org) 79 | - [pnpm](https://pnpm.io) 80 | - A terminal or command prompt 81 | 82 |   83 | 84 | **Step 1: Clone the BdBrowser repository** 85 | 86 | ```sh 87 | git clone https://github.com/tsukasa/BdBrowser.git BdBrowser 88 | ``` 89 | 90 | **Step 2: Install BdBrowser's dependencies** 91 | ```sh 92 | cd BdBrowser 93 | pnpm install 94 | ``` 95 | 96 | **Step 3: Build BdBrowser** 97 | ```sh 98 | pnpm run build-prod 99 | ``` 100 | 101 | **Step 4: Load the Extension** 102 | 1. Open your Chrome extensions page. 103 | 2. Make sure the "Developer mode" toggle on the extension page is enabled. 104 | This enables you to load unpacked extensions from a local folder. 105 | 3. Click the Load unpacked button in the toolbar: 106 | 107 | ![Load unpacked extension](assets/gh-readme/chrome-load-unpacked.png) 108 | 109 | Now select the `dist` folder in the BdBrowser repository. 110 | 111 |   112 | 113 | ## 🎨 Using BdBrowser 114 | Using BdBrowser is almost exactly the same as using BetterDiscord on your desktop. 115 | 116 | ### First Launch 117 | After enabling the extension and reloading Discord's web client, BdBrowser will 118 | initialize its internal virtual filesystem and download a copy of the latest 119 | `betterdiscord.asar` from BetterDiscord's official GitHub releases page. 120 | 121 | BetterDiscord will be loaded from within the asar file in the virtual filesystem 122 | afterwards. 123 | 124 | Subsequent starts will be quicker because no initialization or download needs 125 | to take place. 126 | 127 |   128 | 129 | ### Installing Plugins and Themes 130 | You can install plugins/themes by pressing the Open [...] Folder button in the plugins/themes 131 | category of BetterDiscord's settings. 132 | 133 | Instead of opening the folder containing your plugins/themes, a file picker will open. 134 | Choose one or multiple files of the same type (plugins or themes). 135 | 136 | The files will get installed into the virtual filesystem and will be available through their 137 | respective category immediately afterwards. 138 | 139 |   140 | 141 | ### Updating Plugins or Themes 142 | You can use the normal ways of updating plugins or themes: 143 | * Use the Updater in the settings. 144 | * Use plugin-specific update routines. 145 | * Use the Plugin Repo or Theme Repo plugins. 146 | 147 | Of course, you can also download a copy of the updated file manually and add it to the 148 | virtual filesystem again. You do not need to remove the old file, following the instructions 149 | for [installing plugins/themes](#installing-plugins-and-themes) automatically overwrites a 150 | file if it already exists. 151 | 152 |   153 | 154 | ### Updating BetterDiscord 155 | BetterDiscord got updated? Updating within BdBrowser is just as simple as on the desktop: 156 | 157 | * Recommended: [Create a VFS backup!](#backing-up-the-virtual-filesystem) 158 | * Open the Discord settings. 159 | * Navigate to BetterDiscord's "Updates" category. 160 | * Install the available BetterDiscord update by clicking the Update! button. 161 | * You will be prompted to reload the site. 162 | 163 | In case BetterDiscord does no longer work after the update, you should 164 | [restore the old version](#restoring-from-a-backup) from a backup and check 165 | if there might be a new version of BdBrowser available that addresses the issue. 166 | 167 | If a new version of the extension is available, please follow the instructions to 168 | [update BdBrowser](#updating-bdbrowser). 169 | 170 |   171 | 172 | ### Updating BdBrowser 173 | BetterDiscord is no longer working within BdBrowser? BdBrowser got updated? 174 | Then it might be time to update your local BdBrowser installation to ensure 175 | you have all the latest compatibility improvements and bug fixes. 176 | 177 | Updating the extension is pretty much a repeat of [installing it](#-installation) with a few 178 | notable differences: 179 | 180 | * Use the same folder as you did for the installation. 181 | * You do not need to load the unpacked folder again because it is already loaded. 182 | Uninstalling the old version does not delete your BdBrowser settings or filesystem. 183 | * Instead, please click the Update button on your Chrome extension page. 184 | * After updating, please perform a hard reload of Discord's page (Shift + F5). 185 | 186 | Note: Simply replacing the files/folder and restarting Chrome is _not_ sufficient. 187 | 188 |   189 | 190 | ### Uninstalling BdBrowser 191 | To uninstall BdBrowser, simply remove the extension as you would with every other 192 | Chrome extension. 193 | 194 | The data stored within the virtual filesystem will be kept. 195 | 196 |   197 | 198 | ### Extension Options 199 | BdBrowser has a few extension options that you can access either by right-clicking the 200 | extension icon in your toolbar or the Details button on Chrome's extensions page. 201 | 202 |   203 | 204 | **Do not load BetterDiscord Renderer** 205 | 206 | When loading a Discord tab while the option is active, BdBrowser will initialize 207 | the virtual filesystem but not inject BetterDiscord's renderer into the active page. 208 | 209 | This is useful if you are stuck in a reload loop or experience crashes due to a 210 | BetterDiscord or plugin failure and want to diagnose, backup, or import data in 211 | the virtual filesystem. 212 | 213 |   214 | 215 | **Disable all Plugins on Reload** 216 | 217 | On the next load of a Discord tab, BetterDiscord's plugin.json will be altered by 218 | BdBrowser to disable all BetterDiscord plugins before injecting BetterDiscord's 219 | renderer. 220 | 221 | This option automatically turns itself off again after it was used. 222 | 223 |   224 | 225 | **Delete Asar from VFS on Reload** 226 | 227 | Automatically deletes the `betterdiscord.asar` file from the virtual filesystem on 228 | the next load of a Discord tab. Doing so will prompt BdBrowser's asar updater to 229 | download a fresh copy of BetterDiscord's latest asar file from BetterDiscord's GitHub 230 | releases. 231 | 232 | Using this option is tantamount to re-installing BetterDiscord on a regular desktop 233 | client and is sometimes the only option to fix crashes caused by incompatibilities 234 | between BetterDiscord's renderer and the Discord web application. 235 | 236 | User data like plugins, themes, or settings are unaffected by this option and stay 237 | untouched. 238 | 239 | This option automatically turns itself off again after it was used. 240 | 241 |   242 | 243 | ### Backing up the Virtual Filesystem 244 | Backups are great. You can download a serialized copy of your virtual filesystem 245 | through the console: 246 | 247 | ```javascript 248 | require("fs").exportVfsBackup(); 249 | ``` 250 | 251 |   252 | 253 | ### Restoring from a Backup 254 | If you made a [backup](#backing-up-the-virtual-filesystem) of your virtual filesystem, 255 | you can restore it at any point. 256 | 257 | Open the console and use the following command: 258 | ```javascript 259 | require("fs").importVfsBackup(); 260 | ``` 261 | 262 | A file picker will open. Choose a [backup](#backing-up-the-virtual-filesystem) file. 263 | 264 | All files contained within the backup will be restored to the version serialized in 265 | the backup. Files that do not exist in the backup stay untouched. 266 | 267 | After importing the backup, please refresh the page. 268 | 269 |   270 | 271 | ### Formatting the Virtual Filesystem 272 | If you want to format the virtual filesystem and start over from scratch, you can 273 | manually trigger this via the console: 274 | 275 | ```javascript 276 | require("fs").formatVfs(true); 277 | ``` 278 | 279 | Entering this command will immediately wipe the virtual filesystem. 280 | 281 | Please reload the page after using the command. 282 | 283 |   284 | 285 | ### Deleting Files in the Virtual Filesystem 286 | In some rare cases you might want to remove orphaned or ill-behaving files from the extension's virtual filesystem 287 | but cannot find/uninstall them through BetterDiscord's settings. 288 | 289 | You can perform these operations via your browser's developer tools console: 290 | 291 | ```javascript 292 | /* Read the contents of the virtual plugins folder */ 293 | require("fs").readdirSync(BdApi.Plugins.folder) 294 | /* Removes the file named "SomePluginName.plugin.js" from the virtual plugins folder */ 295 | require("fs").unlinkSync(require("path").join(BdApi.Plugins.folder, "SomePluginName.plugin.js")) 296 | ``` 297 | 298 | Reload the Discord tab afterwards. 299 | 300 |   301 | 302 | ### Restricting Extension Site Access 303 | By default, BdBrowser is allowed to read and change all your data on all sites through 304 | the extension's "Site access" setting (accessible through Chrome's extension page and 305 | clicking the Details button for an extension). 306 | 307 | The default is set liberally so that all web requests handled by the service worker 308 | will work out of the box. Otherwise, the user would manually have to configure 309 | the allowed domains beforehand as part of the onboarding/installation. 310 | 311 | If you know exactly which domains your specific set of themes/plugins query, 312 | you can harden the configuration by changing the Site Access setting from 313 | `On all sites` to `On specific sites` and adding the allowed domains manually. 314 | 315 |   316 | 317 | ## 🧰 Using VfsTool 318 | BdBrowser comes with the `VfsTool.ps1` Powershell script that allows you to create and 319 | extract virtual filesystem files. 320 | 321 | The script is located under `assets/scripts/VfsTool.ps1` in the extension directory. 322 | 323 |   324 | 325 | ### Extracting Backup Files 326 | If you have an existing [backup file](#backing-up-the-virtual-filesystem), you can extract its contents 327 | with this command: 328 | 329 | ```powershell 330 | .\VfsTool.ps1 -Operation Extract -Path C:\path\to\your\bdbrowser_backup.json -OutputPath C:\temp 331 | ``` 332 | 333 | The output path has to exist already, otherwise the script will fail. 334 | 335 |   336 | 337 | ### Creating Backup Files 338 | You can create a virtual filesystem backup file for use in BdBrowser from your real filesystem - 339 | whether this is from using your existing BetterDiscord appdata or a tailor-made structure of files 340 | to inject files into the virtual filesystem. 341 | 342 | The resulting files can be [imported into the virtual filesystem](#restoring-from-a-backup). 343 | 344 |   345 | 346 | **Create a Backup from your real AppData** 347 | 348 | Using the VfsTool you can easily create a backup that contains your current BetterDiscord configuration 349 | (themes, plugins and BetterDiscord settings) for use in BdBrowser. 350 | 351 | ```powershell 352 | .\VfsTool.ps1 -Operation Create -Path "$($env:APPDATA)\betterdiscord" -OutputPath C:\temp\bdbrowser_backup_MyRealBdConfig.json 353 | ``` 354 | 355 |   356 | 357 | **Create a Backup containing a custom BetterDiscord asar File** 358 | 359 | Since a [restore](#restoring-from-a-backup) only overwrites files present in the backup itself, 360 | you can create handy partial backup files to up- or downgrade BetterDiscord via VfsTool: 361 | 362 | Step 1: Create an empty root directory. 363 | 364 | ```sh 365 | mkdir bdtemp 366 | ``` 367 | 368 | Step 2: Create a "data" directory within the root directory. 369 | 370 | ```sh 371 | cd bdtemp 372 | mkdir data 373 | ``` 374 | 375 | Step 3: Place a `betterdiscord.asar` from [GitHub](https://github.com/BetterDiscord/BetterDiscord/releases) in the `data` directory. 376 | 377 | Step 4: Create the backup file through the VfsTool. 378 | 379 | ```powershell 380 | .\VfsTool -Operation Create -Path C:\temp\bdtemp -OutputPath C:\temp\bdbrowser_backup_bdasar.json 381 | ``` 382 | 383 | Note: Please make sure you mind the folder and file names, as the virtual filesystem is case-sensitive! 384 | 385 | --- 386 | 387 | [Back to Top](#bdbrowser) 388 | -------------------------------------------------------------------------------- /assets/bd-plugins/arRPCBridge.plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name arRpcBridge 3 | * @author tsukasa, OpenAsar 4 | * @version 0.0.4 5 | * @description Connect to arRPC, an open Discord RPC server for atypical setups. Loosely based on bridge_mod.js by OpenAsar. 6 | * @website https://github.com/tsukasa/BdBrowser 7 | * @source https://raw.githubusercontent.com/tsukasa/BdBrowser/refs/heads/master/assets/bd-plugins/arRPCBridge.plugin.js 8 | */ 9 | 10 | // This plugin is loosely based on bridge_mod.js by OpenAsar, which is licensed under the MIT license. 11 | // See licenses/arrpc/arrpc.txt for more details. 12 | 13 | const WEBSOCKET_ADDRESS = "ws://127.0.0.1:1337"; 14 | const DISPATCH_TYPE = "LOCAL_ACTIVITY_UPDATE"; 15 | 16 | module.exports = class arRpcBridgePlugin { 17 | constructor(meta) { 18 | this.meta = meta; 19 | this.api = new BdApi(this.meta.name); 20 | 21 | const {Webpack, Logger} = this.api; 22 | 23 | this.Webpack = Webpack; 24 | this.Logger = Logger; 25 | 26 | this.Dispatcher = this.Webpack.getModule(m => m.dispatch && m.subscribe);; 27 | this.AssetManager = this.Webpack.getByKeys("fetchAssetIds", "getAssetImage"); 28 | this.fetchApplicationsRPC = this.Webpack.getByRegex("IPC.*APPLICATION_RPC"); 29 | 30 | this.apps = {}; 31 | this.connectErrorNotice = undefined; 32 | this.currentPid = 0; 33 | this.currentSocketId = 0; 34 | this.webSocket = undefined; 35 | this.pluginIsStarted = false; 36 | } 37 | 38 | closeNotice() { 39 | this.connectErrorNotice?.apply(); 40 | this.connectErrorNotice = undefined; 41 | } 42 | 43 | async getApplication(applicationId) { 44 | let socket = {}; 45 | await this.fetchApplicationsRPC(socket, applicationId); 46 | return socket.application; 47 | } 48 | 49 | async getAsset(applicationId, key) { 50 | const asset = await this.AssetManager.fetchAssetIds(applicationId, [key, undefined]); 51 | return asset.find(e => typeof e !== 'undefined'); 52 | } 53 | 54 | onWebSocketCloseHandler = async () => { 55 | const isConnected = await new Promise(res => 56 | setTimeout(() => res(this.webSocket.readyState === WebSocket.OPEN), 1000)); 57 | 58 | if (!isConnected) { 59 | this.Logger.log("Connection to arRPC server failed."); 60 | 61 | if(this.connectErrorNotice) 62 | return; 63 | 64 | this.connectErrorNotice = this.api.showNotice("Connection to arRPC failed, please ensure the local server is running.", { 65 | type: "error", 66 | buttons: [{ 67 | label: "Connect", 68 | onClick: () => { 69 | this.closeNotice(); 70 | this.openWebSocketConnection(); 71 | } 72 | }] 73 | }); 74 | } 75 | }; 76 | 77 | onWebSocketOpenHandler = (event) => { 78 | this.Logger.info(`Connected to arRPC server via ${WEBSOCKET_ADDRESS}.`); 79 | } 80 | 81 | onWebSocketMessageHandler = async (event) => { 82 | const msg = JSON.parse(event.data); 83 | 84 | if (msg.activity?.assets?.large_image) 85 | msg.activity.assets.large_image = 86 | await this.getAsset(msg.activity.application_id, msg.activity.assets.large_image); 87 | 88 | if (msg.activity?.assets?.small_image) 89 | msg.activity.assets.small_image = 90 | await this.getAsset(msg.activity.application_id, msg.activity.assets.small_image); 91 | 92 | if (msg.activity) { 93 | const appId = msg.activity.application_id; 94 | 95 | if (!this.apps[appId]) 96 | this.apps[appId] = await this.getApplication(appId); 97 | 98 | const app = this.apps[appId]; 99 | 100 | if(!msg.activity.name) 101 | msg.activity.name = app.name; 102 | } 103 | 104 | if(msg.pid) 105 | this.currentPid = msg.pid; 106 | 107 | if(msg.socketId) 108 | this.currentSocketId = msg.socketId; 109 | 110 | this.Dispatcher.dispatch({type: DISPATCH_TYPE, ...msg}); 111 | }; 112 | 113 | openWebSocketConnection() { 114 | if(!this.pluginIsStarted) 115 | return; 116 | 117 | this.webSocket?.close(); 118 | this.webSocket = new WebSocket(WEBSOCKET_ADDRESS); 119 | this.webSocket.addEventListener("close", this.onWebSocketCloseHandler); 120 | this.webSocket.addEventListener("open", this.onWebSocketOpenHandler); 121 | this.webSocket.addEventListener("message", this.onWebSocketMessageHandler); 122 | } 123 | 124 | start() { 125 | this.pluginIsStarted = true; 126 | 127 | this.openWebSocketConnection(); 128 | } 129 | 130 | stop() { 131 | this.pluginIsStarted = false; 132 | 133 | this.closeNotice(); 134 | 135 | this.webSocket?.removeEventListener("close", this.onWebSocketCloseHandler); 136 | this.webSocket?.removeEventListener("open", this.onWebSocketOpenHandler); 137 | this.webSocket?.removeEventListener("message", this.onWebSocketMessageHandler); 138 | this.webSocket?.close(); 139 | 140 | const emptyActivity = {activity: null, pid: this.currentPid, socketId: this.currentSocketId}; 141 | this.Dispatcher.dispatch({type: DISPATCH_TYPE, ...emptyActivity}); 142 | } 143 | }; 144 | -------------------------------------------------------------------------------- /assets/bd-themes/HideDownloadAppsButton.theme.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Hide Download Apps Button 3 | * @author tsukasa 4 | * @version 1.0 5 | * @source https://github.com/tsukasa/BdBrowser 6 | * @description Removes the unwanted "Download Apps" button from the guild list in the web-client. 7 | */ 8 | 9 | ul[data-list-id='guildsnav'] > div[class^='scroller'] > div[class^='listItem']:nth-last-of-type(2), 10 | ul[data-list-id='guildsnav'] > div[class^='scroller'] > div[class^='listItem']:nth-last-of-type(3) { 11 | visibility: hidden; 12 | display: none; 13 | } 14 | -------------------------------------------------------------------------------- /assets/chrome/_locales/de/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName": { 3 | "message": "BDBrowser" 4 | }, 5 | "extDesc": { 6 | "message": "Erlaubt das Laden von BetterDiscord im Web Client. Manche Plugins funktionieren ggf. nicht." 7 | }, 8 | "titlePersistentOptions": { 9 | "message": "Dauerhafte Optionen" 10 | }, 11 | "titleResetOnReloadOptions": { 12 | "message": "Einmalige Optionen" 13 | }, 14 | "disableBdRenderer": { 15 | "message": "BetterDiscord Renderer nicht laden" 16 | }, 17 | "disableBdPluginsOnReload": { 18 | "message": "Beim Laden alle Plugins deaktivieren" 19 | }, 20 | "deleteBdRendererOnReload": { 21 | "message": "Beim Laden Asar aus VFS löschen" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /assets/chrome/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName": { 3 | "message": "BDBrowser" 4 | }, 5 | "extDesc": { 6 | "message": "Allows you to use BetterDiscord on the web client. Some plugins might not be working correctly." 7 | }, 8 | "titlePersistentOptions": { 9 | "message": "Persistent Options" 10 | }, 11 | "titleResetOnReloadOptions": { 12 | "message": "Reset on Reload Options" 13 | }, 14 | "disableBdRenderer": { 15 | "message": "Do not load BetterDiscord Renderer" 16 | }, 17 | "disableBdPluginsOnReload": { 18 | "message": "Disable all Plugins on Reload" 19 | }, 20 | "deleteBdRendererOnReload": { 21 | "message": "Delete Asar from VFS on Reload" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /assets/chrome/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukasa/BdBrowser/82bd51c6cb44e639f485208456b8c95c3dd02d2f/assets/chrome/logo.png -------------------------------------------------------------------------------- /assets/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_extName__", 3 | "version": "1.12.5.20250517", 4 | "description": "__MSG_extDesc__", 5 | "homepage_url": "https://github.com/tsukasa/BdBrowser", 6 | "icons": { 7 | "16": "assets/chrome/logo.png", 8 | "48": "assets/chrome/logo.png", 9 | "128": "assets/chrome/logo.png" 10 | }, 11 | "manifest_version": 3, 12 | "default_locale": "en", 13 | "permissions": [ 14 | "declarativeNetRequestWithHostAccess", 15 | "storage" 16 | ], 17 | "host_permissions": [ 18 | "" 19 | ], 20 | "options_ui": { 21 | "page": "assets/chrome/options.html", 22 | "open_in_tab": false 23 | }, 24 | "background": { 25 | "service_worker": "js/service.js" 26 | }, 27 | "content_scripts": [{ 28 | "matches": [ 29 | "*://discord.com/*", 30 | "*://canary.discord.com/*", 31 | "*://ptb.discord.com/*" 32 | ], 33 | "run_at": "document_start", 34 | "js": [ 35 | "js/backend.js" 36 | ] 37 | }], 38 | "web_accessible_resources": [{ 39 | "resources": [ 40 | "assets/spinner.webm", 41 | "bd/betterdiscord.asar", 42 | "bd/renderer.js", 43 | "js/frontend.js", 44 | "js/preload.js" 45 | ], 46 | "matches": [ 47 | "*://*.discord.com/*" 48 | ] 49 | }] 50 | } 51 | -------------------------------------------------------------------------------- /assets/chrome/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | BDBrowser Extension Options 7 | 8 | 9 | 10 |
11 |

12 | #titlePersistentOptions 13 |

14 |

15 | 16 | 17 |

18 |
19 |
20 |

21 | #titleResetOnReloadOptions 22 |

23 |

24 | 25 | 26 |

27 |

28 | 29 | 30 |

31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /assets/chrome/options.js: -------------------------------------------------------------------------------- 1 | const saveOptions = () => { 2 | const disableRenderer = document.getElementById('disableBdRenderer').checked; 3 | const disablePluginsOnReload = document.getElementById('disableBdPluginsOnReload').checked; 4 | const deleteRendererOnReload = document.getElementById('deleteBdRendererOnReload').checked; 5 | 6 | chrome.storage.sync.set( 7 | { 8 | disableBdRenderer: disableRenderer, 9 | disableBdPluginsOnReload: disablePluginsOnReload, 10 | deleteBdRendererOnReload: deleteRendererOnReload 11 | }, 12 | () => { 13 | 14 | } 15 | ); 16 | }; 17 | 18 | const restoreOptions = () => { 19 | chrome.storage.sync.get( 20 | { 21 | disableBdRenderer: false, 22 | disableBdPluginsOnReload: false, 23 | deleteBdRendererOnReload: false 24 | }, 25 | (options) => { 26 | document.getElementById('disableBdRenderer').checked = options.disableBdRenderer; 27 | document.getElementById('disableBdPluginsOnReload').checked = options.disableBdPluginsOnReload; 28 | document.getElementById('deleteBdRendererOnReload').checked = options.deleteBdRendererOnReload; 29 | } 30 | ); 31 | }; 32 | 33 | /** 34 | * Loads the i18n messages for the options page. 35 | * The i18n messages are loaded from the _locales folder. 36 | */ 37 | const loadI18n = () => { 38 | let elements = document.querySelectorAll("[i18n]"); 39 | 40 | elements.forEach(element => { 41 | const i18nElement = element.getAttribute("i18n"); 42 | const i18nMessage = chrome.i18n.getMessage(i18nElement); 43 | 44 | if (i18nMessage) { 45 | element.innerText = i18nMessage; 46 | } 47 | }); 48 | } 49 | 50 | /** 51 | * Operations to perform once options page has loaded. 52 | */ 53 | const onContentLoaded = () => { 54 | loadI18n(); 55 | restoreOptions(); 56 | } 57 | 58 | const initialize = () => { 59 | document.addEventListener("DOMContentLoaded", onContentLoaded); 60 | 61 | document.getElementById("disableBdRenderer").addEventListener("change", saveOptions); 62 | document.getElementById("disableBdPluginsOnReload").addEventListener("change", saveOptions); 63 | document.getElementById("deleteBdRendererOnReload").addEventListener("change", saveOptions); 64 | } 65 | 66 | initialize(); 67 | -------------------------------------------------------------------------------- /assets/gh-readme/chrome-load-unpacked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukasa/BdBrowser/82bd51c6cb44e639f485208456b8c95c3dd02d2f/assets/gh-readme/chrome-load-unpacked.png -------------------------------------------------------------------------------- /assets/scripts/VfsTool.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | A simple tool that can create and extract BdBrowser VFS backup files. 4 | 5 | .DESCRIPTION 6 | The VFS Tool script supports two operations: Create and Extract. 7 | 8 | When used in "Create" mode, you can specify the root directory of your 9 | BetterDiscord AppData path (i.e. C:\Users\Meow\AppData\Roaming\BetterDiscord), 10 | a destination path for the backup file and have the script create a serialized 11 | copy of your real filesystem that you can then upload into BdBrowser's virtual 12 | filesystem. 13 | 14 | So in short: 15 | Parameter -Path points to the BetterDiscord AppData path. 16 | Parameter -OutputPath points to the filename for the backup file to create. 17 | 18 | --- 19 | 20 | If you use the script in "Extract" mode, you do instead specify the path to 21 | the VFS backup file and set the root output path where the structure should 22 | be extracted to. 23 | 24 | So in short: 25 | Parameter -Path points to the VFS backup file to extract. 26 | Parameter -OutputPath points to the root directory where it should be extracted to. 27 | 28 | .PARAMETER Operation 29 | Defines in which mode this script operates. 30 | 31 | Can either be "Create" to create a new VFS backup file from the real filesystem or 32 | "Extract" to extract the contents of a VFS backup file. 33 | 34 | .PARAMETER Path 35 | When in "Create" mode: 36 | Absolute path to the root of BetterDiscord's AppData path. 37 | 38 | When in "Extract" mode: 39 | Relative or absolute path to the BdBrowser VFS Backup file. 40 | 41 | .PARAMETER OutputPath 42 | When in "Create" mode: 43 | Relative or absolute path where the BdBrowser VFS Backup file should be written to. 44 | 45 | When in "Extract" mode: 46 | Relative or absolute path to the directory where the backup file structure should 47 | be extracted to. 48 | 49 | .EXAMPLE 50 | .\VfsTool.ps1 -Operation Create -Path "C:\Users\Meow\AppData\Roaming\BetterDiscord" -OutputPath "C:\Temp\Backup.json" 51 | 52 | Creates a new VFS backup file in C:\Temp\Backup.json and serializes the data from the specified -Path. 53 | 54 | .EXAMPLE 55 | .\VfsTool.ps1 -Operation Extract -Path "C:\Temp\Backup.json" -OutputPath "C:\Temp\Extracted" 56 | 57 | Extracts the contents of the VFS backup file "C:\Temp\Backup.json" into "C:\Temp\Extracted". 58 | 59 | Please note that the target directory has to exist already! 60 | #> 61 | 62 | [CmdletBinding()] 63 | param ( 64 | [Parameter(Mandatory)] 65 | [ValidateNotNullOrEmpty()] 66 | [ValidateSet("Extract", "Create")] 67 | [String] $Operation, 68 | [Parameter(Mandatory)] 69 | [ValidateNotNullOrEmpty()] 70 | [String] $Path, 71 | [Parameter(Mandatory)] 72 | [ValidateNotNullOrEmpty()] 73 | [String] $OutputPath 74 | ) 75 | 76 | function Initialize() { 77 | switch($Operation) { 78 | "Create" { CreateBackup } 79 | "Extract" { ExtractFromBackup } 80 | } 81 | } 82 | 83 | function CreateBackup() { 84 | $now = [DateTimeOffset]::Now.ToUnixTimeSeconds() 85 | $pathContents = [Ordered] @{} 86 | 87 | # Create dummy file structure for expected root directories 88 | 89 | $pathContents["AppData"] = @{ 90 | "atime" = $now 91 | "birthtime" = $now 92 | "ctime" = $now 93 | "fullName" = "AppData" 94 | "mtime" = $now 95 | "nodeType" = "dir" 96 | "pathName" = "" 97 | "size" = 0 98 | } 99 | 100 | $pathContents["AppData/BetterDiscord"] = @{ 101 | "atime" = $now 102 | "birthtime" = $now 103 | "ctime" = $now 104 | "fullName" = "AppData/BetterDiscord" 105 | "mtime" = $now 106 | "nodeType" = "dir" 107 | "pathName" = "AppData" 108 | "size" = 0 109 | } 110 | 111 | # Now evaluate the contents of the given input path 112 | 113 | Push-Location -Path:$Path 114 | 115 | foreach($item in Get-ChildItem -Path:$Path -Recurse -Depth 99 -Exclude "emotes.asar") { 116 | $itemPathRelative = (Resolve-Path -Path $item -Relative).TrimStart('\', '.') 117 | $itemPathParent = "" 118 | $itemPath = (Join-Path -Path "AppData/BetterDiscord" -ChildPath $itemPathRelative).Replace('\', '/') 119 | 120 | $itemPathParent = [System.IO.Path]::GetDirectoryName($itemPath).TrimEnd('\').Replace('\', '/') 121 | 122 | if($item -is [System.IO.DirectoryInfo]) { 123 | $pathContents[$itemPath] = @{ 124 | "atime" = $now 125 | "birthtime" = $now 126 | "ctime" = $now 127 | "fullName" = $itemPath 128 | "mtime" = $now 129 | "nodeType" = "dir" 130 | "pathName" = $itemPathParent 131 | "size" = 0 132 | } 133 | } 134 | else 135 | { 136 | $pathContents[$itemPath] = @{ 137 | "atime" = $now 138 | "birthtime" = $now 139 | "contents" = [Convert]::ToBase64String([IO.File]::ReadAllBytes($item.FullName)) 140 | "ctime" = $now 141 | "fullName" = $itemPath 142 | "mtime" = $now 143 | "nodeType" = "file" 144 | "pathName" = $itemPathParent 145 | "size" = $item.Length 146 | } 147 | } 148 | } 149 | 150 | Pop-Location 151 | 152 | $pathContents | Sort-Object -Property Name | ConvertTo-Json -Compress | Set-Content -Path:$OutputPath 153 | } 154 | 155 | function ExtractFromBackup() { 156 | $fileContent = Get-Content -Path:$Path -Raw | ConvertFrom-Json 157 | 158 | foreach($key in $fileContent.psobject.properties.name) { 159 | $obj = $fileContent.$key 160 | 161 | $targetPath = Join-Path -Path $OutputPath -ChildPath $obj.fullName 162 | 163 | if($obj.nodeType -eq "dir") { 164 | New-Item -Path $targetPath -ItemType Directory -Force | Out-Null 165 | } 166 | 167 | if($obj.nodeType -eq "file") { 168 | # Try to create the directory structure again. 169 | $folderPath = Join-Path -Path $OutputPath -ChildPath $obj.pathName 170 | New-Item -Path $folderPath -ItemType Directory -Force | Out-Null 171 | 172 | # Now write out the file. 173 | $fileBytes = [Convert]::FromBase64String($obj.contents) 174 | [IO.File]::WriteAllBytes($targetPath, $fileBytes) 175 | } 176 | } 177 | } 178 | 179 | Initialize 180 | -------------------------------------------------------------------------------- /assets/spinner.example.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukasa/BdBrowser/82bd51c6cb44e639f485208456b8c95c3dd02d2f/assets/spinner.example.webm -------------------------------------------------------------------------------- /backend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "common": ["../common"] 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bdbrowser/backend", 3 | "description": "BDBrowser Backend", 4 | "version": "0.0.0", 5 | "main": "src/index.js", 6 | "private": true, 7 | "scripts": { 8 | "build": "webpack --progress --color", 9 | "build-prod": "webpack --stats minimal --mode production", 10 | "lint": "eslint --ext .js src/" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/index.js: -------------------------------------------------------------------------------- 1 | import {IPCEvents} from "common/constants"; 2 | import DOM from "common/dom"; 3 | import IPC from "common/ipc"; 4 | import Logger from "common/logger"; 5 | import LoadingScreen from "./modules/loadingscreen"; 6 | 7 | /** 8 | * Initializes the "backend" side of BdBrowser. 9 | * Some parts need to fire early (document_start) in order to patch 10 | * objects in Discord's DOM while other parts are to be fired later 11 | * (document_idle) after the page has loaded. 12 | */ 13 | function initialize() { 14 | const doOnDocumentComplete = () => { 15 | registerEvents(); 16 | 17 | Logger.log("Backend", "Initializing modules."); 18 | injectFrontend(chrome.runtime.getURL("js/frontend.js")); 19 | }; 20 | 21 | const documentCompleteCallback = () => { 22 | if (document.readyState !== "complete") { 23 | return; 24 | } 25 | 26 | document.removeEventListener("readystatechange", documentCompleteCallback); 27 | doOnDocumentComplete(); 28 | }; 29 | 30 | // Preload should fire as early as possible and is the reason for 31 | // running the backend during `document_start`. 32 | injectPreload(); 33 | 34 | if (document.readyState === "complete") { 35 | doOnDocumentComplete(); 36 | } 37 | else { 38 | document.addEventListener("readystatechange", documentCompleteCallback); 39 | } 40 | 41 | LoadingScreen.replaceLoadingAnimation(); 42 | } 43 | 44 | /** 45 | * Injects the Frontend script into the page. 46 | * Should fire when the page is complete (document_idle). 47 | * @param {string} scriptUrl - Internal URL to the script 48 | */ 49 | function injectFrontend(scriptUrl) { 50 | Logger.log("Backend", "Loading frontend script from:", scriptUrl); 51 | DOM.injectJS("BetterDiscordBrowser-frontend", scriptUrl, false); 52 | } 53 | 54 | /** 55 | * Injects the Preload script into the page. 56 | * Should fire as soon as possible (document_start). 57 | */ 58 | function injectPreload() { 59 | Logger.log("Backend", "Injecting preload.js into document to prepare environment..."); 60 | 61 | const scriptElement = document.createElement("script"); 62 | scriptElement.src = chrome.runtime.getURL("js/preload.js"); 63 | 64 | (document.head || document.documentElement).appendChild(scriptElement); 65 | } 66 | 67 | function registerEvents() { 68 | Logger.log("Backend", "Registering events."); 69 | 70 | const ipcMain = new IPC("backend"); 71 | 72 | ipcMain.on(IPCEvents.GET_MANIFEST_INFO, (event) => { 73 | ipcMain.reply(event, chrome.runtime.getManifest()); 74 | }); 75 | 76 | ipcMain.on(IPCEvents.GET_RESOURCE_URL, (event, data) => { 77 | ipcMain.reply(event, chrome.runtime.getURL(data.url)); 78 | }); 79 | 80 | ipcMain.on(IPCEvents.GET_EXTENSION_OPTIONS, (event) => { 81 | chrome.storage.sync.get( 82 | { 83 | disableBdRenderer: false, 84 | disableBdPluginsOnReload: false, 85 | deleteBdRendererOnReload: false 86 | }, 87 | (options) => { 88 | ipcMain.reply(event, options); 89 | } 90 | ); 91 | }); 92 | 93 | ipcMain.on(IPCEvents.SET_EXTENSION_OPTIONS, (event, data) => { 94 | chrome.storage.sync.set( 95 | data, 96 | () => { 97 | Logger.log("Backend", "Saved extension options:", data); 98 | }); 99 | }); 100 | 101 | ipcMain.on(IPCEvents.INJECT_CSS, (_, data) => { 102 | DOM.injectCSS(data.id, data.css); 103 | }); 104 | 105 | ipcMain.on(IPCEvents.INJECT_THEME, (_, data) => { 106 | DOM.injectTheme(data.id, data.css); 107 | }); 108 | 109 | ipcMain.on(IPCEvents.MAKE_REQUESTS, (event, data) => { 110 | // If the data is an object instead of a string, we probably 111 | // deal with a "request"-style request and have to re-order 112 | // the options. 113 | if (data.url && typeof(data.url) === "object") { 114 | // Deep clone data.url into the options and remove the url 115 | data.options = JSON.parse(JSON.stringify(data.url)); 116 | data.options.url = undefined; 117 | 118 | if (data.url.url) { 119 | data.url = data.url.url; 120 | } 121 | } 122 | 123 | chrome.runtime.sendMessage( 124 | { 125 | operation: "fetch", 126 | parameters: { 127 | url: data.url, 128 | options: data.options 129 | } 130 | }, (response) => { 131 | try { 132 | if (response.error) { 133 | if (!data.url.startsWith(chrome.runtime.getURL(""))) { 134 | // eslint-disable-next-line no-console 135 | console.error("BdBrowser Backend MAKE_REQUESTS failed:", data.url, response.error); 136 | } 137 | ipcMain.reply(event, undefined); 138 | } 139 | else { 140 | // Response body comes in as a normal array, so requires 141 | // another round of casting into Uint8Array for the buffer. 142 | response.body = new Uint8Array(response.body).buffer; 143 | ipcMain.reply(event, response); 144 | } 145 | } 146 | catch (error) { 147 | Logger.error("Backend", "MAKE_REQUESTS failed:", error, data.url, response); 148 | ipcMain.reply(event, undefined); 149 | } 150 | } 151 | ); 152 | }); 153 | } 154 | 155 | initialize(); 156 | -------------------------------------------------------------------------------- /backend/src/modules/loadingscreen.js: -------------------------------------------------------------------------------- 1 | const LOADING_ANIMATION_SELECTOR = `video[data-testid="app-spinner"]`; 2 | 3 | export default class LoadingScreen { 4 | static #loadingObserver = new MutationObserver(() => { 5 | if (document.readyState === "complete") { 6 | this.#loadingObserver.disconnect(); 7 | } 8 | 9 | const loadingAnimationElement = document.querySelector(LOADING_ANIMATION_SELECTOR); 10 | if (loadingAnimationElement) { 11 | this.#loadingObserver.disconnect(); 12 | 13 | // Should be a WebM file with VP9 codec (400px x 400px) so the alpha channel gets preserved. 14 | const customAnimationSource = document.createElement("source"); 15 | customAnimationSource.src = chrome.runtime.getURL("assets/spinner.webm"); 16 | customAnimationSource.type = "video/webm"; 17 | 18 | loadingAnimationElement.prepend(customAnimationSource); 19 | } 20 | }); 21 | 22 | /** 23 | * Inserts the custom loading screen spinner animation from 24 | * `assets/spinner.webm` into the playlist. 25 | * 26 | * If the file cannot be found, the video player will automatically 27 | * choose one of the default Discord animations. 28 | */ 29 | static replaceLoadingAnimation() { 30 | this.#loadingObserver.observe(document, { 31 | childList: true, 32 | subtree: true 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = (env, argv) => ({ 4 | mode: "development", 5 | target: "node", 6 | devtool: argv.mode === "production" ? undefined : "eval-source-map", 7 | entry: path.resolve(__dirname, "src", "index.js"), 8 | optimization: { 9 | minimize: false 10 | }, 11 | output: { 12 | filename: "backend.js", 13 | path: path.resolve(__dirname, "..", "dist", "js") 14 | }, 15 | resolve: { 16 | extensions: [".js", ".jsx"], 17 | modules: [ 18 | path.resolve(__dirname, "src", "modules") 19 | ], 20 | alias: { 21 | common: path.join(__dirname, "..", "common"), 22 | assets: path.join(__dirname, "..", "assets") 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /common/constants.js: -------------------------------------------------------------------------------- 1 | export const IPCEvents = { 2 | GET_MANIFEST_INFO: "bdbrowser-get-extension-manifest", 3 | GET_RESOURCE_URL: "bdbrowser-get-extension-resourceurl", 4 | GET_EXTENSION_OPTIONS: "bdbrowser-get-extension-options", 5 | HANDLE_PROTOCOL: "bd-handle-protocol", 6 | SET_EXTENSION_OPTIONS: "bdbrowser-set-extension-options", 7 | INJECT_CSS: "bdbrowser-inject-css", 8 | INJECT_THEME: "bdbrowser-inject-theme", 9 | MAKE_REQUESTS: "bdbrowser-make-requests" 10 | }; 11 | 12 | export const FilePaths = { 13 | BD_ASAR_PATH: "AppData/BetterDiscord/data/betterdiscord.asar", 14 | BD_ASAR_VERSION_PATH: "AppData/BetterDiscord/data/bd-asar-version.txt", 15 | BD_CONFIG_PLUGINS_PATH: "AppData/BetterDiscord/data/&1/plugins.json", 16 | LOCAL_BD_ASAR_PATH: "bd/betterdiscord.asar", 17 | LOCAL_BD_RENDERER_PATH: "bd/renderer.js" 18 | }; 19 | 20 | export const AppHostVersion = "1.0.9007"; 21 | 22 | export default { 23 | IPCEvents, 24 | FilePaths, 25 | AppHostVersion 26 | }; 27 | -------------------------------------------------------------------------------- /common/dom.js: -------------------------------------------------------------------------------- 1 | export default class DOM { 2 | /** 3 | * @returns {HTMLElement} 4 | */ 5 | static createElement(type, options = {}, ...children) { 6 | const node = document.createElement(type); 7 | 8 | Object.assign(node, options); 9 | 10 | for (const child of children) { 11 | node.append(child); 12 | } 13 | 14 | return node; 15 | } 16 | 17 | static injectTheme(id, css) { 18 | const [bdThemes] = document.getElementsByTagName("bd-themes"); 19 | 20 | const style = this.createElement("style", { 21 | id: id, 22 | type: "text/css", 23 | innerHTML: css, 24 | }); 25 | 26 | style.setAttribute("data-bd-native", ""); 27 | bdThemes.append(style); 28 | } 29 | 30 | static injectCSS(id, css) { 31 | const style = this.createElement("style", { 32 | id: id, 33 | type: "text/css", 34 | innerHTML: css 35 | }); 36 | 37 | this.headAppend(style); 38 | } 39 | 40 | static removeCSS(id) { 41 | const style = document.querySelector("style#" + id); 42 | 43 | if (style) { 44 | style.remove(); 45 | } 46 | } 47 | 48 | static injectJS(id, src, silent = true) { 49 | const script = this.createElement("script", { 50 | id: id, 51 | type: "text/javascript", 52 | src: src 53 | }); 54 | 55 | this.headAppend(script); 56 | 57 | if (silent) { 58 | script.addEventListener("load", () => { 59 | script.remove(); 60 | }, {once: true}); 61 | } 62 | } 63 | } 64 | 65 | const callback = () => { 66 | if (document.readyState !== "complete") { 67 | return; 68 | } 69 | 70 | document.removeEventListener("readystatechange", callback); 71 | DOM.headAppend = document.head.append.bind(document.head); 72 | }; 73 | 74 | if (document.readyState === "complete") { 75 | DOM.headAppend = document.head.append.bind(document.head); 76 | } 77 | else { 78 | document.addEventListener("readystatechange", callback); 79 | } 80 | -------------------------------------------------------------------------------- /common/ipc.js: -------------------------------------------------------------------------------- 1 | const IPC_REPLY_SUFFIX = "-reply"; 2 | 3 | export default class IPC { 4 | constructor(context) { 5 | if (!context) { 6 | throw new Error("Context is required"); 7 | } 8 | 9 | this.context = context; 10 | } 11 | 12 | createHash() { 13 | return Math.random().toString(36).substring(2, 10); 14 | } 15 | 16 | reply(message, data) { 17 | this.send(message.event.concat(IPC_REPLY_SUFFIX), data, void 0, message.hash); 18 | } 19 | 20 | on(event, listener, once = false) { 21 | const wrappedListener = (message) => { 22 | if (message.data.event !== event || message.data.context === this.context) { 23 | return; 24 | } 25 | 26 | const returnValue = listener(message.data, message.data.data); 27 | 28 | if (returnValue === true && once) { 29 | window.removeEventListener("message", wrappedListener); 30 | } 31 | }; 32 | 33 | window.addEventListener("message", wrappedListener); 34 | } 35 | 36 | send(event, data, callback = null, hash) { 37 | if (!hash) { 38 | hash = this.createHash(); 39 | } 40 | 41 | if (callback) { 42 | this.on(event.concat(IPC_REPLY_SUFFIX), message => { 43 | if (message.hash === hash) { 44 | callback(message.data); 45 | return true; 46 | } 47 | 48 | return false; 49 | }, true); 50 | } 51 | 52 | window.postMessage({ 53 | source: "betterdiscord-browser".concat("-", this.context), 54 | event: event, 55 | context: this.context, 56 | hash: hash, 57 | data 58 | }); 59 | } 60 | 61 | sendAwait(event, data, hash) { 62 | return new Promise((resolve) => { 63 | const callback = (d) => { 64 | resolve(d); 65 | }; 66 | 67 | this.send(event, data, callback, hash); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /common/logger.js: -------------------------------------------------------------------------------- 1 | export default class Logger { 2 | static #parseType(type) { 3 | switch (type) { 4 | case "info": 5 | case "warn": 6 | case "error": 7 | return type; 8 | default: 9 | return "log"; 10 | } 11 | } 12 | 13 | static #log(type, module, ...message) { 14 | type = this.#parseType(type); 15 | // eslint-disable-next-line no-console 16 | console[type](`%c[BDBrowser]%c %c[${module}]%c`, "color: #3E82E5; font-weight: 700;", "", "color: #396CB8", "", ...message); 17 | } 18 | 19 | static log(module, ...message) { 20 | this.#log("log", module, ...message); 21 | } 22 | 23 | static info(module, ...message) { 24 | this.#log("info", module, ...message); 25 | } 26 | 27 | static warn(module, ...message) { 28 | this.#log("warn", module, ...message); 29 | } 30 | 31 | static error(module, ...message) { 32 | this.#log("error", module, ...message); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "common": ["../common"] 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bdbrowser/frontend", 3 | "description": "BDBrowser Frontend", 4 | "version": "0.0.0", 5 | "main": "src/index.js", 6 | "private": true, 7 | "scripts": { 8 | "build": "webpack --progress --color", 9 | "build-prod": "webpack --stats minimal --mode production", 10 | "lint": "eslint --ext .js src/" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/app_shims/discordnative.js: -------------------------------------------------------------------------------- 1 | import process from "app_shims/process"; 2 | import discord_voice from "native_shims/discord_voice"; 3 | import {AppHostVersion} from "common/constants"; 4 | 5 | export const app = { 6 | getReleaseChannel() { 7 | if (window.location.href.includes("canary")) return "canary"; 8 | if (window.location.href.includes("ptb")) return "ptb"; 9 | return "stable"; 10 | }, 11 | 12 | getVersion() { 13 | return AppHostVersion; 14 | }, 15 | 16 | async getPath(path) { 17 | switch (path) { 18 | case "appData": 19 | return process.env.APPDATA; 20 | 21 | default: 22 | throw new Error("Cannot find path: " + path); 23 | } 24 | }, 25 | 26 | relaunch() { 27 | window.location.reload(); 28 | } 29 | }; 30 | 31 | export const nativeModules = { 32 | requireModule(module) { 33 | switch (module) { 34 | case "discord_voice": 35 | return discord_voice; 36 | 37 | default: 38 | throw new Error("Cannot find module: " + module); 39 | } 40 | } 41 | }; 42 | 43 | export default { 44 | app, 45 | nativeModules 46 | }; 47 | -------------------------------------------------------------------------------- /frontend/src/app_shims/electron.js: -------------------------------------------------------------------------------- 1 | import ipcRenderer from "modules/ipcrenderer"; 2 | 3 | ipcRenderer.initialize(); 4 | export {ipcRenderer}; 5 | 6 | export const remote = { 7 | app: { 8 | getAppPath: () => "ElectronAppPath" 9 | }, 10 | getCurrentWindow: () => null, 11 | getCurrentWebContents: () => ({ 12 | on: () => {} 13 | }) 14 | }; 15 | 16 | export const shell = { 17 | openItem: () => {}, 18 | openExternal: () => {} 19 | }; 20 | 21 | export const clipboard = { 22 | write: (data) => { 23 | if (typeof(data) != "object") return; 24 | if (data.text) { 25 | clipboard.writeText(data.text); 26 | } 27 | }, 28 | writeText: text => navigator.clipboard.writeText(text), 29 | }; 30 | 31 | export default { 32 | clipboard, 33 | ipcRenderer, 34 | remote, 35 | shell 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/src/app_shims/process.js: -------------------------------------------------------------------------------- 1 | export default { 2 | platform: "win32", 3 | env: { 4 | APPDATA: "AppData", 5 | DISCORD_APP_PATH: "AppData/Discord/AppPath", 6 | DISCORD_USER_DATA: "AppData/Discord/UserData", 7 | BETTERDISCORD_DATA_PATH: "AppData/BetterDiscord" 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import fs from "node_shims/fs"; 2 | import startup from "modules/startup"; 3 | import RuntimeOptions from "modules/runtimeoptions"; 4 | 5 | import "patches"; 6 | 7 | (async () => { 8 | startup.prepareWindow(); 9 | 10 | await RuntimeOptions.initializeOptions(); 11 | 12 | if (!await fs.openDatabase()) { 13 | throw new Error("BdBrowser Error: IndexedDB VFS database connection could not be established!"); 14 | } 15 | 16 | if (!await fs.initializeVfs()) { 17 | throw new Error("BdBrowser Error: IndexedDB VFS could not be initialized!"); 18 | } 19 | 20 | if (!await startup.checkAndDownloadBetterDiscordAsar()) { 21 | throw new Error("BdBrowser Error: Downloading betterdiscord.asar or writing into VFS failed!"); 22 | } 23 | 24 | if (!await startup.loadBetterDiscord()) { 25 | throw new Error("BdBrowser Error: Cannot load BetterDiscord renderer for injection!"); 26 | } 27 | })(); 28 | -------------------------------------------------------------------------------- /frontend/src/modules/asar.js: -------------------------------------------------------------------------------- 1 | const headerSizeIndex = 12, 2 | headerOffset = 16, 3 | uInt32Size = 4, 4 | textDecoder = new TextDecoder("utf-8"); 5 | 6 | // Essentially just ripped from the chromium-pickle-js source, thanks for 7 | // doing my math homework. 8 | const alignInt = (i, alignment) => 9 | i + (alignment - (i % alignment)) % alignment; 10 | 11 | /** 12 | * 13 | * @param {ArrayBuffer} archive Asar archive to open 14 | * @returns {ArchiveData} 15 | */ 16 | const openAsar = archive => { 17 | if (archive.length > Number.MAX_SAFE_INTEGER) { 18 | throw new Error("Asar archive too large."); 19 | } 20 | 21 | const headerSize = new DataView(archive).getUint32(headerSizeIndex, true), 22 | // Pickle wants to align the headers so that the payload length is 23 | // always a multiple of 4. This means you'll get "padding" bytes 24 | // after the header if you don't round up the stored value. 25 | // 26 | // IMO why not just store the aligned int and have us trim the json, 27 | // but it's whatever. 28 | headerEnd = headerOffset + headerSize, 29 | filesOffset = alignInt(headerEnd, uInt32Size), 30 | rawHeader = archive.slice(headerOffset, headerEnd), 31 | buffer = archive.slice(filesOffset); 32 | 33 | /** 34 | * @typedef {Object} ArchiveData 35 | * @property {Object} header - The asar file's manifest, containing the pointers to each index's files in the buffer 36 | * @property {ArrayBuffer} buffer - The contents of the archive, concatenated together. 37 | */ 38 | return { 39 | header: JSON.parse(textDecoder.decode(rawHeader)), 40 | buffer 41 | }; 42 | }; 43 | 44 | const crawlHeader = function self(files, dirname) { 45 | const prefix = itemName => 46 | (dirname ? dirname + "/" : "") + itemName; 47 | 48 | let children = []; 49 | 50 | for (const filename in files) { 51 | const extraFiles = files[filename].files; 52 | 53 | if (extraFiles) { 54 | const extra = self(extraFiles, filename); 55 | 56 | children = children.concat(extra); 57 | } 58 | 59 | children.push(filename); 60 | } 61 | 62 | return children.map(prefix); 63 | }; 64 | 65 | /** 66 | * These paths must be absolute and posix-style, without a leading forward slash. 67 | * @typedef {String} ArchivePath 68 | */ 69 | 70 | /** 71 | * An Asar archive 72 | * @class 73 | * @param {ArrayBuffer} archive The archive to open 74 | */ 75 | class Asar { 76 | constructor(archive) { 77 | const {header, buffer} = openAsar(archive); 78 | 79 | this.header = header; 80 | this.buffer = buffer; 81 | this.contents = crawlHeader(header); 82 | } 83 | 84 | /** 85 | * Retrieves information on a directory or file from the archive's header 86 | * @param {ArchivePath} path The path to the dirent 87 | * @returns {Object} 88 | */ 89 | find(path) { 90 | const navigate = (currentItem, navigateTo) => { 91 | if (currentItem.files) { 92 | const nextItem = currentItem.files[navigateTo]; 93 | 94 | if (!nextItem) { 95 | if (path == "/") { // This breaks it lol 96 | return this.header; 97 | } 98 | 99 | throw new PathError(path, `${navigateTo} could not be found.`); 100 | } 101 | 102 | return nextItem; 103 | } 104 | 105 | throw new PathError(path, `${navigateTo} is not a directory.`); 106 | }; 107 | 108 | return path 109 | .split("/") 110 | .reduce(navigate, this.header); 111 | } 112 | 113 | /** 114 | * Open a file in the archive 115 | * @param {ArchivePath} path The path to the file 116 | * @returns {ArrayBuffer} The file's contents 117 | */ 118 | get(path) { 119 | const {offset, size} = this.find(path), 120 | offsetInt = parseInt(offset); 121 | 122 | return this.buffer.slice(offsetInt, offsetInt + size); 123 | } 124 | } 125 | 126 | class PathError extends Error { 127 | constructor(path, message) { 128 | super(`Invalid path "${path}": ${message}`); 129 | 130 | this.name = "PathError"; 131 | } 132 | } 133 | 134 | export {Asar as default}; 135 | -------------------------------------------------------------------------------- /frontend/src/modules/bdasarupdater.js: -------------------------------------------------------------------------------- 1 | import Logger from "common/logger"; 2 | import fs from "node_shims/fs"; 3 | import request from "node_shims/request"; 4 | import {FilePaths} from "common/constants"; 5 | 6 | const USER_AGENT = "BdBrowser Updater"; 7 | const LOGGER_SECTION = "AsarUpdater"; 8 | 9 | export default class BdAsarUpdater { 10 | /** 11 | * Gets the version of BetterDiscord's asar according to the version file in the VFS. 12 | * @returns {string} - Version number or `0.0.0` if no value is set yet. 13 | */ 14 | static getVfsBetterDiscordAsarVersion() { 15 | if (!fs.existsSync(FilePaths.BD_ASAR_VERSION_PATH)) { 16 | return "0.0.0"; 17 | } 18 | 19 | return fs.readFileSync(FilePaths.BD_ASAR_VERSION_PATH).toString(); 20 | } 21 | 22 | /** 23 | * Sets the version of BetterDiscord's asar in the version file within the VFS. 24 | * @param {string} versionString 25 | */ 26 | static setVfsBetterDiscordAsarVersion(versionString) { 27 | fs.writeFileSync(FilePaths.BD_ASAR_VERSION_PATH, versionString); 28 | } 29 | 30 | /** 31 | * Returns whether a BetterDiscord asar exists in the VFS. 32 | * @returns {boolean} 33 | */ 34 | static get hasBetterDiscordAsarInVfs() { 35 | return fs.existsSync(FilePaths.BD_ASAR_PATH); 36 | } 37 | 38 | /** 39 | * Returns a Buffer containing the contents of the asar file. 40 | * If the file is not present in the VFS, a ENOENT exception is thrown. 41 | * @returns {*|Buffer} 42 | */ 43 | static get asarFile() { 44 | if (this.hasBetterDiscordAsarInVfs) return fs.readFileSync(FilePaths.BD_ASAR_PATH); 45 | return fs.statSync(FilePaths.BD_ASAR_PATH); 46 | } 47 | 48 | /** 49 | * Checks BetterDiscord's GitHub releases for the latest version and returns 50 | * the update information to the caller. 51 | * @returns {Promise<{hasUpdate: boolean, data: any, remoteVersion: *}>} 52 | */ 53 | static async getCurrentBdVersionInfo() { 54 | Logger.log(LOGGER_SECTION, "Checking for latest BetterDiscord version..."); 55 | 56 | const resp = await fetch("https://api.github.com/repos/BetterDiscord/BetterDiscord/releases/latest", { 57 | method: "GET", 58 | headers: { 59 | "Accept": "application/json", 60 | "Content-Type": "application/json", 61 | "User-Agent": USER_AGENT 62 | } 63 | }); 64 | 65 | const data = await resp.json(); 66 | const remoteVersion = data.tag_name.startsWith("v") ? data.tag_name.slice(1) : data.tag_name; 67 | const hasUpdate = remoteVersion > this.getVfsBetterDiscordAsarVersion(); 68 | 69 | Logger.log(LOGGER_SECTION, `Latest stable BetterDiscord version is ${remoteVersion}.`); 70 | 71 | return { 72 | data, 73 | remoteVersion, 74 | hasUpdate 75 | }; 76 | } 77 | 78 | /** 79 | * Downloads the betterdiscord.asar specified in updateInfo and saves the file into the VFS. 80 | * @param updateInfo 81 | * @param remoteVersion 82 | * @returns {Promise} 83 | */ 84 | static async downloadBetterDiscordAsar(updateInfo, remoteVersion) { 85 | try { 86 | const asar = updateInfo.assets.find(a => a.name === "betterdiscord.asar"); 87 | 88 | Logger.log(LOGGER_SECTION, `Downloading BetterDiscord v${remoteVersion} into VFS...`); 89 | const startTime = performance.now(); 90 | 91 | const buff = await new Promise((resolve, reject) => 92 | request(asar.url, { 93 | headers: { 94 | "Accept": "application/octet-stream", 95 | "Content-Type": "application/octet-stream", 96 | "User-Agent": USER_AGENT 97 | }}, (err, resp, body) => { 98 | if (err || resp.statusCode !== 200) { 99 | return reject(err || `${resp.statusCode} ${resp.statusMessage}`); 100 | } 101 | return resolve(body); 102 | }) 103 | ); 104 | 105 | Logger.info(LOGGER_SECTION, "Download complete, saving into VFS..."); 106 | fs.writeFileSync(FilePaths.BD_ASAR_PATH, buff); 107 | 108 | Logger.info(LOGGER_SECTION, `Persisting version information in: ${FilePaths.BD_ASAR_VERSION_PATH}`); 109 | this.setVfsBetterDiscordAsarVersion(remoteVersion); 110 | 111 | const endTime = performance.now(); 112 | Logger.info(LOGGER_SECTION, `betterdiscord.asar installed, took ${(endTime - startTime).toFixed(2)}ms.`); 113 | return true; 114 | } 115 | catch (err) { 116 | Logger.error(LOGGER_SECTION, "Failed to download BetterDiscord", err); 117 | return false; 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /frontend/src/modules/bdpreload.js: -------------------------------------------------------------------------------- 1 | import electron from "app_shims/electron"; 2 | import fs from "node_shims/fs"; 3 | import https from "node_shims/https"; 4 | import path from "node_shims/path"; 5 | import {default as nativeFetch} from "modules/fetch/nativefetch"; 6 | import ipcRenderer from "modules/ipc"; 7 | import {IPCEvents} from "common/constants"; 8 | 9 | export default { 10 | electron: electron, 11 | filesystem: { 12 | readFile: fs.readFileSync, 13 | writeFile: fs.writeFileSync, 14 | readDirectory: fs.readdirSync, 15 | createDirectory: fs.mkdirSync, 16 | deleteDirectory: fs.rmdirSync, 17 | exists: fs.existsSync, 18 | getRealPath: fs.realpathSync, 19 | rename: fs.renameSync, 20 | renameSync: fs.renameSync, 21 | rm: fs.rmSync, 22 | rmSync: fs.rmSync, 23 | unlinkSync: fs.unlinkSync, 24 | createWriteStream: () => {}, 25 | watch: fs.watch, 26 | getStats: fs.statSync 27 | }, 28 | nativeFetch: nativeFetch, 29 | https: https, 30 | path: path, 31 | setProtocolListener: (callback) => { 32 | ipcRenderer.on(IPCEvents.HANDLE_PROTOCOL, (url) => callback(url)); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /frontend/src/modules/discordmodules.js: -------------------------------------------------------------------------------- 1 | import Webpack from "modules/webpack"; 2 | 3 | export default { 4 | /* User Stores and Utils */ 5 | get UserStore() {return Webpack.getByProps("getCurrentUser", "getUser");}, 6 | 7 | /* Electron & Other Internals with Utils */ 8 | get ElectronModule() {return Webpack.getByProps("setBadge");}, 9 | get Dispatcher() {return Webpack.getByProps("dispatch", "subscribe", "wait", "unsubscribe", "register");}, 10 | get RouterModule() {return Webpack.getByProps("listeners", "rewrites", "flushRoute");} 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/src/modules/events.js: -------------------------------------------------------------------------------- 1 | import Logger from "common/logger"; 2 | 3 | export default class Events { 4 | 5 | constructor() { 6 | this.eventListeners = {}; 7 | } 8 | 9 | static get EventEmitter() { 10 | return Events; 11 | } 12 | 13 | dispatch(event, ...args) { 14 | this.emit(event, ...args); 15 | } 16 | 17 | emit(event, ...args) { 18 | if (!this.eventListeners[event]) return; 19 | 20 | this.eventListeners[event].forEach(listener => { 21 | try { 22 | listener(...args); 23 | } 24 | catch (error) { 25 | Logger.error("Events", `Could not fire event [${event}] for ${listener.toString().slice(0, 20)}:`, error); 26 | } 27 | }); 28 | } 29 | 30 | on(event, callback) { 31 | if (!this.eventListeners[event]) { 32 | this.eventListeners[event] = new Set(); 33 | } 34 | 35 | this.eventListeners[event].add(callback); 36 | } 37 | 38 | off(event, callback) { 39 | return this.removeListener(event, callback); 40 | } 41 | 42 | removeListener(event, callback) { 43 | if (!this.eventListeners[event]) return; 44 | this.eventListeners[event].delete(callback); 45 | } 46 | 47 | setMaxListeners() { 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/modules/fetch.js: -------------------------------------------------------------------------------- 1 | import {IPCEvents} from "common/constants"; 2 | import ipcRenderer from "modules/ipc"; 3 | 4 | export default function fetch(url, options) { 5 | return new Promise(resolve => { 6 | ipcRenderer.send( 7 | IPCEvents.MAKE_REQUESTS, 8 | {url: url, options: options}, 9 | data => { 10 | if (options && options._wrapInResponse === false) { 11 | resolve(data); 12 | } 13 | else { 14 | const res = new Response(data.body); 15 | Object.defineProperty(res, "headers", {value: data.headers}); 16 | Object.defineProperty(res, "ok", {value: data.ok}); 17 | Object.defineProperty(res, "redirected", {value: data.redirected}); 18 | Object.defineProperty(res, "status", {value: data.status}); 19 | Object.defineProperty(res, "statusCode", {value: data.status}); 20 | Object.defineProperty(res, "statusText", {value: data.statusText}); 21 | Object.defineProperty(res, "type", {value: data.type}); 22 | Object.defineProperty(res, "url", {value: data.url}); 23 | resolve(res); 24 | } 25 | } 26 | ); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/modules/fetch/nativefetch.js: -------------------------------------------------------------------------------- 1 | import fetch from "modules/fetch"; 2 | import {Buffer} from "node_shims/buffer"; 3 | 4 | function nativeFetch(url, options) { 5 | let state = "PENDING"; 6 | let data = {content: [], headers: null, statusCode: null, url: url, statusText: "", redirected: false}; 7 | let listenerCallback; 8 | let errorCallback; 9 | 10 | // NOTE: Since BetterDiscord's renderer/src/modules/api/fetch.js creates their own Response object, 11 | // BdBrowser merely needs to ensure that the raw object values can be mapped properly. 12 | fetch(url,{headers: options.headers || {}, method: options.method || "GET", _wrapInResponse: false}) 13 | .then(res => { 14 | data = res; 15 | data.content = Buffer.from(res.body); 16 | 17 | // Clean up unwanted properties 18 | delete data.body; 19 | 20 | state = "DONE"; 21 | listenerCallback(); 22 | }) 23 | .catch(error => { 24 | state = "ABORTED"; 25 | errorCallback(error); 26 | }); 27 | 28 | return { 29 | onComplete(callback) { 30 | listenerCallback = callback; 31 | }, 32 | onError(callback) { 33 | errorCallback = callback; 34 | }, 35 | readData() { 36 | switch (state) { 37 | case "PENDING": 38 | throw new Error("Cannot read data before request is done!"); 39 | case "ABORTED": 40 | throw new Error("Request was aborted."); 41 | case "DONE": 42 | return data; 43 | } 44 | } 45 | }; 46 | } 47 | 48 | export default nativeFetch; 49 | -------------------------------------------------------------------------------- /frontend/src/modules/ipc.js: -------------------------------------------------------------------------------- 1 | import IPC from "common/ipc"; 2 | 3 | const ipcRenderer = new IPC("frontend"); 4 | 5 | export default ipcRenderer; 6 | -------------------------------------------------------------------------------- /frontend/src/modules/ipcrenderer.js: -------------------------------------------------------------------------------- 1 | import DiscordModules from "modules/discordmodules"; 2 | import Logger from "common/logger"; 3 | import DOM from "common/dom"; 4 | import fs from "node_shims/fs"; 5 | import {IPCEvents} from "common/constants"; 6 | 7 | // https://developer.mozilla.org/en/docs/Web/API/Page_Visibility_API 8 | const [hidden, visibilityChange] = (() => { 9 | if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support 10 | return ["hidden", "visibilitychange"]; 11 | } 12 | else if (typeof document.msHidden !== "undefined") { 13 | return ["msHidden", "msvisibilitychange"]; 14 | } 15 | else if (typeof document.webkitHidden !== "undefined") { 16 | return ["webkitHidden", "webkitvisibilitychange"]; 17 | } 18 | })(); 19 | 20 | export default class IPCRenderer { 21 | static listeners = {}; 22 | 23 | static addWindowListeners() { 24 | document.addEventListener(visibilityChange, () => { 25 | if (document[hidden]) { 26 | this.fire("bd-window-maximize"); 27 | } 28 | else { 29 | this.fire("bd-window-minimize"); 30 | } 31 | }); 32 | } 33 | 34 | static createEvent(event) { 35 | if (!this.listeners[event]) { 36 | this.listeners[event] = new Set(); 37 | } 38 | } 39 | 40 | static fire(event, ...args) { 41 | if (this.listeners[event]) { 42 | for (const listener of this.listeners[event]) { 43 | listener(...args); 44 | } 45 | } 46 | } 47 | 48 | static initialize() { 49 | this.addWindowListeners(); 50 | } 51 | 52 | static async invoke(event) { 53 | switch (event) { 54 | case "bd-get-accent-color": 55 | // Right now it appears there is no proper cross-platform way to get the system accent color. 56 | // According https://stackoverflow.com/a/71539151 this seems to be the best compromise. 57 | return "Highlight"; 58 | 59 | default: 60 | Logger.log("IPCRenderer", "INVOKE:", event); 61 | } 62 | } 63 | 64 | static on(event, callback) { 65 | switch (event) { 66 | case "bd-did-navigate-in-page": 67 | return this.onSwitch(callback); 68 | 69 | case IPCEvents.HANDLE_PROTOCOL: 70 | return this.onSwitch(callback); 71 | 72 | default: 73 | this.createEvent(event); 74 | this.listeners[event].add(callback); 75 | } 76 | } 77 | 78 | static onSwitch(callback) { 79 | DiscordModules.RouterModule.listeners.add(callback); 80 | } 81 | 82 | static send(event, ...args) { 83 | switch (event) { 84 | case "bd-relaunch-app": 85 | document.location.reload(); 86 | break; 87 | case "bd-open-path": 88 | // In case there is more than one argument, we cannot deal with this. 89 | if (args.length !== 1) { 90 | Logger.log("IPCRenderer", "IPCRenderer bd-open-path called:", args); 91 | break; 92 | } 93 | // If this becomes a more prominent issue, a proper implementation might be required... 94 | const pathElement = args[0].split("/").pop(); 95 | let acceptedFileTypes = "*.*"; 96 | if (pathElement !== "themes" && pathElement !== "plugins") { 97 | Logger.log("IPCRenderer", "IPCRenderer bd-open-path called with unsupported path type:", args); 98 | break; 99 | } 100 | switch (pathElement.toLowerCase()) { 101 | case "themes": 102 | acceptedFileTypes = ".theme.css"; 103 | break; 104 | case "plugins": 105 | acceptedFileTypes = ".plugin.js"; 106 | break; 107 | } 108 | const inputEl = DOM.createElement("input", {type: "file", multiple: "multiple", accept: acceptedFileTypes}); 109 | inputEl.addEventListener("change", () => { 110 | for (const file of inputEl.files) { 111 | const reader = new FileReader(); 112 | reader.onload = () => { 113 | fs.writeFileSync(`AppData/BetterDiscord/${pathElement}/${file.name}`, new Uint8Array(reader.result)); 114 | }; 115 | reader.readAsArrayBuffer(file); 116 | } 117 | }); 118 | inputEl.click(); 119 | break; 120 | default: 121 | Logger.log("IPCRenderer", "IPCRenderer SEND:", event, args); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /frontend/src/modules/localstorage.js: -------------------------------------------------------------------------------- 1 | export default class LocalStorage { 2 | 3 | static { 4 | if (!this.localStorage && window.bdbrowserLocalStorage) { 5 | this.localStorage = window.bdbrowserLocalStorage; 6 | delete window.bdbrowserLocalStorage; 7 | } 8 | } 9 | 10 | static getItem(key, fallbackValue) { 11 | let value = this.localStorage.getItem(key); 12 | 13 | if (value != null) { 14 | try { 15 | value = JSON.parse(value); 16 | } 17 | catch (e) { 18 | value = fallbackValue; 19 | } 20 | } 21 | else { 22 | value = fallbackValue; 23 | } 24 | 25 | return value; 26 | } 27 | 28 | static setItem(key, item) { 29 | this.localStorage.setItem(key, JSON.stringify(item)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/modules/module.js: -------------------------------------------------------------------------------- 1 | import fs from "node_shims/fs"; 2 | import {extname} from "node_shims/path"; 3 | import Logger from "common/logger"; 4 | 5 | const globalPaths = []; 6 | const _extensions = { 7 | ".json": (module, filename) => { 8 | const filecontent = fs.readFileSync(filename); 9 | module.exports = JSON.parse(filecontent); 10 | }, 11 | ".js": (module, filename) => { 12 | Logger.warn("Module", module, filename); 13 | } 14 | }; 15 | 16 | function _require(path, req) { 17 | const extension = extname(path); 18 | const loader = _extensions[extension]; 19 | 20 | if (!loader) { 21 | throw new Error(`Unknown file extension ${path}`); 22 | } 23 | 24 | const existsFile = fs.existsSync(path); 25 | 26 | if (!path) { 27 | Logger.warn("Module", path); 28 | } 29 | 30 | if (!existsFile) { 31 | throw new Error("Module not found!"); 32 | } 33 | 34 | if (req.cache[path]) { 35 | return req.cache[path]; 36 | } 37 | 38 | const final = { 39 | exports: {}, 40 | filename: path, 41 | _compile: content => { 42 | // eslint-disable-next-line no-eval 43 | const {module} = eval(`((module, global) => { 44 | ${content} 45 | 46 | return { 47 | module 48 | }; 49 | })({exports: {}}, window)`); 50 | 51 | if (Object.keys(module.exports).length) { 52 | final.exports = module.exports; 53 | } 54 | 55 | return final.exports; 56 | } 57 | }; 58 | loader(final, path); 59 | return req.cache[path] = final.exports; 60 | } 61 | 62 | export default { 63 | Module: {globalPaths, _extensions}, 64 | _require 65 | }; 66 | -------------------------------------------------------------------------------- /frontend/src/modules/patches.js: -------------------------------------------------------------------------------- 1 | import DOM from "common/dom"; 2 | import {IPCEvents} from "common/constants"; 3 | import Logger from "common/logger"; 4 | import ipcRenderer from "modules/ipc"; 5 | 6 | const appendMethods = ["append", "appendChild", "prepend"]; 7 | const originalInsertBefore = document.head.insertBefore; 8 | 9 | (() => { 10 | document.head.insertBefore = function () { 11 | return originalInsertBefore.apply(this, arguments); 12 | }; 13 | 14 | disableSentry(); 15 | })(); 16 | 17 | function disableSentry() { 18 | // eslint-disable-next-line no-console 19 | for (const method of Object.keys(console)) { 20 | // eslint-disable-next-line no-console 21 | if (console[method]?.__sentry_original__) { 22 | // eslint-disable-next-line no-console 23 | console[method] = console[method].__sentry_original__; 24 | } 25 | } 26 | } 27 | 28 | function patchMethods(node, callback) { 29 | for (const method of appendMethods) { 30 | const original = node[method]; 31 | 32 | node[method] = function () { 33 | const data = { 34 | args: arguments, 35 | callOriginalMethod: () => original.apply(this, arguments) 36 | }; 37 | 38 | return callback(data); 39 | }; 40 | 41 | node[method].__bd_original = original; 42 | } 43 | 44 | return () => { 45 | for (const method of appendMethods) { 46 | const original = node[method].__bd_original; 47 | if (original) { 48 | node[method] = original; 49 | } 50 | } 51 | }; 52 | } 53 | 54 | patchMethods(document.head, data => { 55 | const [node] = data.args; 56 | 57 | if (node?.id === "monaco-style") { 58 | ipcRenderer.send(IPCEvents.MAKE_REQUESTS, {url: node.href}, monacoStyleData => { 59 | const dataBody = new TextDecoder().decode(monacoStyleData.body); 60 | DOM.injectCSS(node.id, dataBody); 61 | if (typeof node.onload === "function") node.onload(); 62 | Logger.log("CSP:Bypass", "Loaded monaco stylesheet."); 63 | }); 64 | 65 | return node; 66 | } 67 | else if (node?.localName === "bd-head") { 68 | patchMethods(node, bdHeadData => { 69 | const [headNode] = bdHeadData.args; 70 | 71 | if (headNode.localName === "bd-scripts") { 72 | patchMethods(headNode, bdScriptsData => { 73 | const [scriptsNode] = bdScriptsData.args; 74 | ipcRenderer.send(IPCEvents.MAKE_REQUESTS, {url: scriptsNode.src}, scriptsResponse => { 75 | const dataBody = new TextDecoder().decode(scriptsResponse.body); 76 | // eslint-disable-next-line no-eval 77 | eval(dataBody); 78 | if (typeof scriptsNode.onload === "function") scriptsNode.onload(); 79 | Logger.log("CSP:Bypass", `Loaded script with url ${scriptsNode.src}`); 80 | }); 81 | }); 82 | } 83 | else if (headNode?.localName === "bd-themes") { 84 | patchMethods(headNode, bdThemesData => { 85 | const [nativeNode] = bdThemesData.args; 86 | if (nativeNode.getAttribute("data-bd-native")) { 87 | return bdThemesData.callOriginalMethod(); 88 | } 89 | injectTheme(nativeNode); 90 | if (typeof nativeNode.onload === "function") nativeNode.onload(); 91 | Logger.log("CSP:Bypass", `Loaded theme ${nativeNode.id}`); 92 | }); 93 | } 94 | 95 | bdHeadData.callOriginalMethod(); 96 | }); 97 | } 98 | else if (node?.src?.includes("monaco-editor")) { 99 | ipcRenderer.send(IPCEvents.MAKE_REQUESTS, {url: node.src}, monacoEditorData => { 100 | const dataBody = new TextDecoder().decode(monacoEditorData.body); 101 | // eslint-disable-next-line no-eval 102 | eval(dataBody); 103 | if (typeof node.onload === "function") node.onload(); 104 | Logger.log("CSP:Bypass", `Loaded script with url ${node.src}`); 105 | }); 106 | return; 107 | } 108 | else if (node?.id?.endsWith("-script-container")) { 109 | Logger.log("CSP:Bypass", `Loading plugin ${node.id.replace("-script-container", "")}`); 110 | // eslint-disable-next-line no-eval 111 | eval(`(() => { 112 | try { 113 | ${node.textContent} 114 | } 115 | catch (err) { 116 | Logger.error("Patches", "Failed to load plugin:", err); 117 | } 118 | })()`); 119 | return; 120 | } 121 | 122 | return data.callOriginalMethod(); 123 | }); 124 | 125 | function injectTheme(node) { 126 | ipcRenderer.send(IPCEvents.INJECT_THEME, {id: node.id, css: node.textContent}); 127 | } 128 | -------------------------------------------------------------------------------- /frontend/src/modules/runtimeinfo.js: -------------------------------------------------------------------------------- 1 | import {IPCEvents} from "common/constants"; 2 | import BdAsarUpdater from "modules/bdasarupdater"; 3 | import ipcRenderer from "modules/ipc"; 4 | 5 | const UNKNOWN_VERSION = "UNKNOWN"; 6 | 7 | let runtimeInfo; 8 | let activeVersionObserver; 9 | 10 | (async () => { 11 | const manifestInfo = await ipcRenderer.sendAwait(IPCEvents.GET_MANIFEST_INFO); 12 | const bdVersion = BdAsarUpdater.getVfsBetterDiscordAsarVersion(); 13 | 14 | runtimeInfo = { 15 | manifest: manifestInfo, 16 | bdVersion: bdVersion, 17 | rendererSourceName: "Unknown", 18 | isVfsFile: false 19 | }; 20 | })(); 21 | 22 | /*** 23 | * Adds a MutationObserver to inject BdBrowser version information 24 | * into the user settings. 25 | */ 26 | export function addExtensionVersionInfo() { 27 | const idSpanVersion = "bdbrowser-ver-info"; 28 | const idSpanRenderer = "bdbrowser-rndr-info"; 29 | const versionSelector = `div[class*="side"] div[class*="info"] span[class*="line"] span[class*="versionHash"]`; 30 | 31 | const addVersionInfoObserver = new MutationObserver(() => { 32 | if (document.querySelector(`#${idSpanVersion}`)) return; 33 | 34 | const discordBuildInfo = document.querySelector(versionSelector)?.parentNode; 35 | if (!discordBuildInfo) return; 36 | 37 | const addInfoSpanElement = (spanId, text = "", additionalStyles = [""]) => { 38 | const el = document.createElement("span"); 39 | el.id = spanId; 40 | el.textContent = text; 41 | el.setAttribute("class", discordBuildInfo.getAttribute("class")); 42 | el.setAttribute("data-text-variant", discordBuildInfo.getAttribute("data-text-variant")); 43 | el.setAttribute("style", discordBuildInfo.getAttribute("style") 44 | .concat(";", "text-transform: none !important;", additionalStyles.join(";"))); 45 | return el; 46 | }; 47 | 48 | const bdbVersionInfo = addInfoSpanElement( 49 | idSpanVersion, 50 | `${runtimeInfo.manifest.name} ${runtimeInfo.manifest.version}` 51 | ); 52 | discordBuildInfo.after(bdbVersionInfo); 53 | 54 | const bdbRendererInfo = addInfoSpanElement( 55 | idSpanRenderer, 56 | getFormattedBdRendererSourceString(), 57 | [(runtimeInfo.isVfsFile ? "" : "color: var(--text-warning);")] 58 | ); 59 | bdbVersionInfo.after(bdbRendererInfo); 60 | }); 61 | 62 | if (!activeVersionObserver) { 63 | addVersionInfoObserver.observe(document.body, { 64 | childList: true, 65 | subtree: true 66 | }); 67 | activeVersionObserver = addVersionInfoObserver; 68 | } 69 | } 70 | 71 | /** 72 | * Returns a pre-defined string indicating the source of the BetterDiscord renderer. 73 | * @returns {string} - A formatted string showing BetterDiscord renderer source and version. 74 | */ 75 | export function getFormattedBdRendererSourceString() { 76 | const version = (runtimeInfo.bdVersion === UNKNOWN_VERSION) ? UNKNOWN_VERSION : "v" + runtimeInfo.bdVersion; 77 | const hostFs = runtimeInfo.isVfsFile ? "VFS" : "local"; 78 | 79 | return `${runtimeInfo.rendererSourceName} (${version}, ${hostFs})`; 80 | } 81 | 82 | /** 83 | * Returns an object containing the manifest and runtime information. 84 | * @returns {object} - An object containing runtime information. 85 | */ 86 | export function getRuntimeInfo() { 87 | return runtimeInfo; 88 | } 89 | 90 | /** 91 | * Reads the version number of the BetterDiscord renderer 92 | * from the script body and makes it available via 93 | * `getRuntimeInfo()`. 94 | * @param {string} bdBodyScript - The script body to parse 95 | */ 96 | export function parseBetterDiscordVersion(bdBodyScript) { 97 | const versionNumberRegex = /version:"(.*?)"/; 98 | const versionMatches = bdBodyScript.match(versionNumberRegex); 99 | let versionString = UNKNOWN_VERSION; 100 | 101 | if (versionMatches) { 102 | versionString = versionMatches.at(-1); 103 | } 104 | 105 | runtimeInfo.bdVersion = versionString; 106 | 107 | // If we are dealing with an asar file, we should also update the 108 | // version file in the VFS, so people can easily filter their 109 | // backups. 110 | if (runtimeInfo.rendererSourceName === "betterdiscord.asar" && runtimeInfo.isVfsFile) { 111 | BdAsarUpdater.setVfsBetterDiscordAsarVersion(versionString); 112 | } 113 | } 114 | 115 | /** 116 | * Sets whether the BetterDiscord renderer has been loaded from an asar file within the VFS. 117 | * @param {String} sourceName 118 | * @param {Boolean} isVfsFile 119 | */ 120 | export function setBdRendererSource(sourceName, isVfsFile) { 121 | runtimeInfo.rendererSourceName = sourceName; 122 | runtimeInfo.isVfsFile = isVfsFile; 123 | } 124 | 125 | export default { 126 | addExtensionVersionInfo, 127 | getFormattedBdRendererSourceString, 128 | getRuntimeInfo, 129 | parseBetterDiscordVersion, 130 | setBdRendererSource 131 | }; 132 | -------------------------------------------------------------------------------- /frontend/src/modules/runtimeoptions.js: -------------------------------------------------------------------------------- 1 | import Logger from "common/logger"; 2 | import {FilePaths, IPCEvents} from "common/constants"; 3 | import {app} from "app_shims/discordnative"; 4 | import fs from "node_shims/fs"; 5 | import ipcRenderer from "modules/ipc"; 6 | 7 | const LOGGER_SECTION = "RuntimeOptions"; 8 | 9 | let extensionOptions = {}; 10 | 11 | export default class RuntimeOptions { 12 | static async initializeOptions() { 13 | extensionOptions = await ipcRenderer.sendAwait(IPCEvents.GET_EXTENSION_OPTIONS); 14 | } 15 | 16 | static getOption(optionName) { 17 | return extensionOptions[optionName]; 18 | } 19 | 20 | static setOption(optionName, optionValue) { 21 | extensionOptions[optionName] = optionValue; 22 | } 23 | 24 | static async saveOptions() { 25 | ipcRenderer.send(IPCEvents.SET_EXTENSION_OPTIONS, extensionOptions); 26 | } 27 | 28 | static get shouldStartBetterDiscordRenderer() { 29 | return this.getOption("disableBdRenderer") !== true; 30 | } 31 | 32 | /** 33 | * Checks if the BetterDiscord asar file exists in the VFS and deletes it if it does. 34 | * Setting is controlled by the "deleteBdRendererOnReload" option. 35 | * @returns {Promise} 36 | */ 37 | static async checkAndPerformBetterDiscordAsarRemoval() { 38 | if (this.getOption("deleteBdRendererOnReload") && fs.existsSync(FilePaths.BD_ASAR_PATH)) { 39 | fs.unlinkSync(FilePaths.BD_ASAR_PATH); 40 | Logger.log(LOGGER_SECTION, "Forced BetterDiscord asar file removal from VFS complete."); 41 | } 42 | 43 | this.setOption("deleteBdRendererOnReload", false); 44 | await this.saveOptions(); 45 | } 46 | 47 | /** 48 | * Disables all BetterDiscord plugins by setting their value to false in the plugins.json file. 49 | * @returns {Promise} 50 | */ 51 | static async disableAllBetterDiscordPlugins() { 52 | if (!this.getOption("disableBdPluginsOnReload")) return; 53 | 54 | const pluginConfigPath = FilePaths.BD_CONFIG_PLUGINS_PATH.replace("&1", app.getReleaseChannel()); 55 | 56 | if (!fs.existsSync(pluginConfigPath)) return; 57 | 58 | const rawFileData = fs.readFileSync(pluginConfigPath); 59 | const plugins = JSON.parse(new TextDecoder().decode(rawFileData)); 60 | 61 | for (const plugin in plugins) { 62 | plugins[plugin] = false; 63 | } 64 | 65 | fs.writeFileSync(pluginConfigPath, JSON.stringify(plugins, null, 4)); 66 | 67 | this.setOption("disableBdPluginsOnReload", false); 68 | await this.saveOptions(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/src/modules/startup.js: -------------------------------------------------------------------------------- 1 | import {FilePaths, IPCEvents} from "common/constants"; 2 | import Logger from "common/logger"; 3 | import {default as Asar} from "modules/asar"; 4 | import BdAsarUpdater from "modules/bdasarupdater"; 5 | import bdPreload from "modules/bdpreload"; 6 | import {Buffer} from "node_shims/buffer"; 7 | import DiscordModules from "modules/discordmodules"; 8 | import DiscordNative from "app_shims/discordnative"; 9 | import ipcRenderer from "modules/ipc"; 10 | import process from "app_shims/process"; 11 | import require from "node_shims/require"; 12 | import runtimeInfo from "modules/runtimeinfo"; 13 | import RuntimeOptions from "modules/runtimeoptions"; 14 | 15 | let allowRequireOverride = false; 16 | let bdPreloadHasInitialized = false; 17 | let requireFunc; 18 | 19 | /** 20 | * Checks for the existence of a BetterDiscord asar file in the VFS. 21 | * If the file is not present, the function will attempt to download 22 | * the latest version from BetterDiscord's GitHub releases page. 23 | * @returns {Promise} - Success or failure 24 | */ 25 | async function checkAndDownloadBetterDiscordAsar() { 26 | await RuntimeOptions.checkAndPerformBetterDiscordAsarRemoval(); 27 | 28 | if (BdAsarUpdater.hasBetterDiscordAsarInVfs) { 29 | return true; 30 | } 31 | 32 | Logger.log("Frontend", "No BetterDiscord asar present in VFS, will try to download a copy..."); 33 | const versionCheckData = await BdAsarUpdater.getCurrentBdVersionInfo(); 34 | const updateWasSuccess = await BdAsarUpdater.downloadBetterDiscordAsar(versionCheckData.data, versionCheckData.remoteVersion); 35 | 36 | Logger.info("Frontend", `Asar update reports ${updateWasSuccess ? "success" : "failure"}.`); 37 | return updateWasSuccess; 38 | } 39 | 40 | /** 41 | * Decides where to load the BetterDiscord renderer from 42 | * and returns a string containing the script body. 43 | * @returns {Promise} scriptBody - A string containing the BetterDiscord renderer source to eval. 44 | */ 45 | async function getBdRendererScript() { 46 | 47 | /** 48 | * Attempts to get the contents of a web_accessible_resource of the extension. 49 | * @param url 50 | * @returns {Promise} 51 | */ 52 | const tryGetLocalFile = async (url) => { 53 | const localRendererUrl = await ipcRenderer.sendAwait(IPCEvents.GET_RESOURCE_URL, {url: url}); 54 | return await ipcRenderer.sendAwait(IPCEvents.MAKE_REQUESTS, {url: localRendererUrl}); 55 | }; 56 | 57 | /** 58 | * Tries to load the betterdiscord.js from the ./js folder. 59 | * @returns {Promise} 60 | */ 61 | const tryGetLocalBetterDiscordJs = async () => { 62 | const localFileContents = await tryGetLocalFile(FilePaths.LOCAL_BD_RENDERER_PATH); 63 | if (!localFileContents) return; 64 | 65 | Logger.info("Frontend", "Reading renderer.js from local extension folder..."); 66 | runtimeInfo.setBdRendererSource("renderer.js", false); 67 | return localFileContents.body; 68 | }; 69 | 70 | /** 71 | * Tries to load the betterdiscord.asar from the ./js folder. 72 | * @returns {Promise} 73 | */ 74 | const tryGetLocalBetterDiscordAsar = async () => { 75 | const localFileContents = await tryGetLocalFile(FilePaths.LOCAL_BD_ASAR_PATH); 76 | if (!localFileContents) return; 77 | 78 | Logger.info("Frontend", "Reading betterdiscord.asar from local extension folder..."); 79 | runtimeInfo.setBdRendererSource("betterdiscord.asar", false); 80 | return new Asar(localFileContents.body).get("renderer.js"); 81 | }; 82 | 83 | /** 84 | * Tries to load the betterdiscord.asar from the VFS. 85 | * @returns {undefined|ArrayBuffer} 86 | */ 87 | const tryGetVfsBetterDiscordAsar = () => { 88 | Logger.info("Frontend", "Reading betterdiscord.asar in the VFS..."); 89 | runtimeInfo.setBdRendererSource("betterdiscord.asar", true); 90 | return new Asar(BdAsarUpdater.asarFile.buffer).get("renderer.js"); 91 | }; 92 | 93 | /** 94 | * Gets the BetterDiscord renderer script body. 95 | * @returns {Promise} 96 | */ 97 | const getRenderer = async () => { 98 | return await tryGetLocalBetterDiscordJs() || await tryGetLocalBetterDiscordAsar() || tryGetVfsBetterDiscordAsar(); 99 | }; 100 | 101 | const bdBodyBuffer = await getRenderer(); 102 | return new TextDecoder().decode(bdBodyBuffer); 103 | } 104 | 105 | /** 106 | * Loads and injects the BetterDiscord renderer into the page context. 107 | * Also initializes any BdBrowser-specific DOM modifications. 108 | * @returns {Promise} 109 | */ 110 | async function loadBetterDiscord() { 111 | const connectionOpenEvent = "CONNECTION_OPEN"; 112 | 113 | const bdScriptBody = await getBdRendererScript(); 114 | if (!bdScriptBody) return false; 115 | 116 | runtimeInfo.parseBetterDiscordVersion(bdScriptBody); 117 | 118 | const callback = async () => { 119 | DiscordModules.Dispatcher.unsubscribe(connectionOpenEvent, callback); 120 | try { 121 | Logger.log("Frontend", "Loading BetterDiscord renderer..."); 122 | // eslint-disable-next-line no-eval 123 | eval(`(() => { 124 | ${bdScriptBody} 125 | })()\n//# sourceURL=bdbrowser://renderer.js`); 126 | } 127 | catch (error) { 128 | Logger.error("Frontend", "Failed to load BetterDiscord:\n", error); 129 | } 130 | }; 131 | 132 | runtimeInfo.addExtensionVersionInfo(); 133 | 134 | // Disable all plugins if the user has requested it. 135 | await RuntimeOptions.disableAllBetterDiscordPlugins(); 136 | 137 | if (!RuntimeOptions.shouldStartBetterDiscordRenderer) { 138 | Logger.log("Frontend", "BetterDiscord renderer disabled by user."); 139 | return true; 140 | } 141 | 142 | Logger.log("Frontend", "Registering callback."); 143 | DiscordModules.Dispatcher.subscribe(connectionOpenEvent, callback); 144 | 145 | return true; 146 | } 147 | 148 | /** 149 | * Prepares global window objects. 150 | */ 151 | function prepareWindow() { 152 | requireFunc = require.bind({}); 153 | window.require = requireFunc; 154 | 155 | window.Buffer = Buffer; 156 | window.DiscordNative = DiscordNative; 157 | window.global = window; 158 | window.process = process; 159 | 160 | // Provide self-immolating BetterDiscord preload. 161 | window.BetterDiscordPreload = () => { 162 | if (bdPreloadHasInitialized) return null; 163 | bdPreloadHasInitialized = true; 164 | return bdPreload; 165 | }; 166 | 167 | // Prevent the _very first_ override of window.require by BetterDiscord to 168 | // keep BdBrowser's own version intact. However, allow later changes to it 169 | // (i.e. for Monaco). 170 | Object.defineProperty(window, "require", { 171 | get() { 172 | return requireFunc; 173 | }, 174 | set(newValue) { 175 | // eslint-disable-next-line no-setter-return 176 | if (!allowRequireOverride) return (allowRequireOverride = true); 177 | requireFunc = newValue; 178 | } 179 | }); 180 | } 181 | 182 | export default { 183 | checkAndDownloadBetterDiscordAsar, 184 | loadBetterDiscord, 185 | prepareWindow 186 | }; 187 | -------------------------------------------------------------------------------- /frontend/src/modules/utilities.js: -------------------------------------------------------------------------------- 1 | import {Buffer} from "node_shims/buffer"; 2 | 3 | export default class Utilities { 4 | /** 5 | * Converts an {@link ArrayBuffer} to a base64 string. 6 | * @param {ArrayBuffer} buffer - The ArrayBuffer to be converted into a base64 string. 7 | * @returns {string} The base64 string representation of the ArrayBuffer's data. 8 | */ 9 | static arrayBufferToBase64(buffer) { 10 | const buf = Buffer.from(buffer); 11 | return buf.toString("base64"); 12 | } 13 | 14 | /** 15 | * Converts a base64 string to an {@link ArrayBuffer}. 16 | * @param {string} b64String - The base64 string that is to be converted. 17 | * @returns {Uint8Array} An Uint8Array representation of the data contained within the b64String. 18 | */ 19 | static base64ToArrayBuffer(b64String) { 20 | const buf = Buffer.from(b64String, "base64"); 21 | return new Uint8Array(buf); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/modules/webpack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows for grabbing and searching through Discord's webpacked modules. 3 | * @module WebpackModules 4 | * @version 0.0.2 5 | */ 6 | import Logger from "common/logger"; 7 | 8 | /** 9 | * Checks if a given module matches a set of parameters. 10 | * @callback module:WebpackModules.Filters~filter 11 | * @param {*} module - module to check 12 | * @returns {boolean} - True if the module matches the filter, false otherwise 13 | */ 14 | 15 | /** 16 | * Filters for use with {@link module:WebpackModules} but may prove useful elsewhere. 17 | */ 18 | export class Filters { 19 | /** 20 | * Generates a {@link module:WebpackModules.Filters~filter} that filters by a set of properties. 21 | * @param {Array} props - Array of property names 22 | * @param {module:WebpackModules.Filters~filter} filter - Additional filter 23 | * @returns {module:WebpackModules.Filters~filter} - A filter that checks for a set of properties 24 | */ 25 | static byKeys(props, filter = m => m) { 26 | return module => { 27 | if (!module) return false; 28 | if (typeof(module) !== "object" && typeof(module) !== "function") return false; 29 | const component = filter(module); 30 | if (!component) return false; 31 | for (let p = 0; p < props.length; p++) { 32 | if (!(props[p] in component)) return false; 33 | } 34 | return true; 35 | }; 36 | } 37 | 38 | /** 39 | * Generates a {@link module:WebpackModules.Filters~filter} that filters by a set of properties on the object's prototype. 40 | * @param {Array} fields - Array of property names 41 | * @param {module:WebpackModules.Filters~filter} filter - Additional filter 42 | * @returns {module:WebpackModules.Filters~filter} - A filter that checks for a set of properties on the object's prototype 43 | */ 44 | static byPrototypeKeys(fields, filter = m => m) { 45 | return module => { 46 | if (!module) return false; 47 | if (typeof(module) !== "object" && typeof(module) !== "function") return false; 48 | const component = filter(module); 49 | if (!component) return false; 50 | if (!component.prototype) return false; 51 | for (let f = 0; f < fields.length; f++) { 52 | if (!(fields[f] in component.prototype)) return false; 53 | } 54 | return true; 55 | }; 56 | } 57 | 58 | /** 59 | * Generates a {@link module:WebpackModules.Filters~filter} that filters by a regex. 60 | * @param {RegExp} search - A RegExp to check on the module 61 | * @param {module:WebpackModules.Filters~filter} filter - Additional filter 62 | * @returns {module:WebpackModules.Filters~filter} - A filter that checks for a set of properties 63 | */ 64 | static byRegex(search, filter = m => m) { 65 | return module => { 66 | const method = filter(module); 67 | if (!method) return false; 68 | let methodString = ""; 69 | try {methodString = method.toString([]);} 70 | catch (err) {methodString = method.toString();} 71 | return methodString.search(search) !== -1; 72 | }; 73 | } 74 | 75 | /** 76 | * Generates a {@link module:WebpackModules.Filters~filter} that filters by source code content. 77 | * @param {...(string|RegExp)} searches - Strings or RegExps to match against the module's source 78 | * @returns {module:WebpackModules.Filters~filter} - A filter that checks module source 79 | */ 80 | static bySource(...searches) { 81 | return (exports, module) => { 82 | if (!module?.id) return false; 83 | let source = ""; 84 | try { 85 | source = WebpackModules.require.m[module.id].toString(); 86 | } 87 | catch (err) { 88 | return false; 89 | } 90 | if (!source) return false; 91 | 92 | return searches.every(search => { 93 | if (typeof search === "string") return source.includes(search); 94 | return Boolean(source.match(search)); 95 | }); 96 | }; 97 | } 98 | 99 | /** 100 | * Generates a {@link module:WebpackModules.Filters~filter} that filters by strings. 101 | * @param {...String} search - A RegExp to check on the module 102 | * @returns {module:WebpackModules.Filters~filter} - A filter that checks for a set of strings 103 | */ 104 | static byStrings(...strings) { 105 | return module => { 106 | if (!module?.toString || typeof(module?.toString) !== "function") return; // Not stringable 107 | let moduleString = ""; 108 | try {moduleString = module?.toString([]);} 109 | catch (err) {moduleString = module?.toString();} 110 | if (!moduleString) return false; // Could not create string 111 | for (const s of strings) { 112 | if (!moduleString.includes(s)) return false; 113 | } 114 | return true; 115 | }; 116 | } 117 | 118 | /** 119 | * Generates a {@link module:WebpackModules.Filters~filter} that filters by a set of properties. 120 | * @param {string} name - Name the module should have 121 | * @returns {module:WebpackModules.Filters~filter} - A filter that checks for a set of properties 122 | */ 123 | static byDisplayName(name) { 124 | return module => { 125 | return module && module.displayName === name; 126 | }; 127 | } 128 | 129 | /** 130 | * Generates a {@link module:WebpackModules.Filters~filter} that filters by a set of properties. 131 | * @param {string} name - Name the store should have (usually includes the word Store) 132 | * @returns {module:WebpackModules.Filters~filter} - A filter that checks for a set of properties 133 | */ 134 | static byStoreName(name) { 135 | return module => { 136 | return module?._dispatchToken && module?.getName?.() === name; 137 | }; 138 | } 139 | 140 | /** 141 | * Generates a combined {@link module:WebpackModules.Filters~filter} from a list of filters. 142 | * @param {...module:WebpackModules.Filters~filter} filters - A list of filters 143 | * @returns {module:WebpackModules.Filters~filter} - Combinatory filter of all arguments 144 | */ 145 | static combine(...filters) { 146 | return (exports, module, id) => { 147 | return filters.every(filter => filter(exports, module, id)); 148 | }; 149 | } 150 | } 151 | 152 | 153 | const hasThrown = new WeakSet(); 154 | 155 | const wrapFilter = filter => (exports, module, moduleId) => { 156 | try { 157 | if (exports?.default?.getToken || exports?.default?.getEmail || exports?.default?.showToken) return false; 158 | if (exports.getToken || exports.getEmail || exports.showToken) return false; 159 | return filter(exports, module, moduleId); 160 | } 161 | catch (error) { 162 | if (!hasThrown.has(filter)) Logger.warn("WebpackModules~getModule", "Module filter threw an exception.", error, {filter, module}); 163 | hasThrown.add(filter); 164 | return false; 165 | } 166 | }; 167 | 168 | const TypedArray = Object.getPrototypeOf(Uint8Array); 169 | function shouldSkipModule(exports) { 170 | if (!exports) return true; 171 | if (exports.TypedArray) return true; 172 | if (exports === window) return true; 173 | if (exports === document.documentElement) return true; 174 | if (exports[Symbol.toStringTag] === "DOMTokenList") return true; 175 | if (exports === Symbol) return true; 176 | if (exports instanceof Window) return true; 177 | if (exports instanceof TypedArray) return true; 178 | return false; 179 | } 180 | 181 | export default class WebpackModules { 182 | 183 | static find(filter, first = true) {return this.getModule(filter, {first});} 184 | static findAll(filter) {return this.getModule(filter, {first: false});} 185 | static findByUniqueProperties(props, first = true) {return first ? this.getByProps(...props) : this.getAllByProps(...props);} 186 | static findByDisplayName(name) {return this.getByDisplayName(name);} 187 | 188 | /** 189 | * A Proxy that returns the module source by ID. 190 | */ 191 | static modules = new Proxy({}, { 192 | ownKeys() {return Object.keys(WebpackModules.require.m);}, 193 | getOwnPropertyDescriptor() { 194 | return { 195 | enumerable: true, 196 | configurable: true, // Not actually 197 | }; 198 | }, 199 | get(_, k) { 200 | return WebpackModules.require.m[k]; 201 | }, 202 | set() { 203 | throw new Error("[WebpackModules~modules] Setting modules is not allowed."); 204 | } 205 | }); 206 | 207 | /** 208 | * Finds a module using a filter function. 209 | * @param {function} filter A function to use to filter modules 210 | * @param {object} [options] Set of options to customize the search 211 | * @param {Boolean} [options.first=true] Whether to return only the first matching module 212 | * @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export 213 | * @param {Boolean} [options.searchExports=false] Whether to execute the filter on webpack export getters. 214 | * @param {Boolean} [options.raw=false] Whether to return the whole Module object when matching exports 215 | * @return {Any} 216 | */ 217 | static getModule(filter, options = {}) { 218 | const {first = true, defaultExport = true, searchExports = false, raw = false} = options; 219 | const wrappedFilter = wrapFilter(filter); 220 | 221 | const modules = this.getAllModules(); 222 | const rm = []; 223 | const indices = Object.keys(modules); 224 | for (let i = 0; i < indices.length; i++) { 225 | const index = indices[i]; 226 | if (!modules.hasOwnProperty(index)) continue; 227 | 228 | let module = null; 229 | try {module = modules[index];} 230 | catch {continue;} 231 | 232 | const {exports} = module; 233 | if (shouldSkipModule(exports)) continue; 234 | 235 | if (typeof(exports) === "object" && searchExports && !exports.TypedArray) { 236 | for (const key in exports) { 237 | let foundModule = null; 238 | let wrappedExport = null; 239 | try {wrappedExport = exports[key];} 240 | catch {continue;} 241 | 242 | if (!wrappedExport) continue; 243 | if (wrappedFilter(wrappedExport, module, index)) foundModule = wrappedExport; 244 | if (!foundModule) continue; 245 | if (raw) foundModule = module; 246 | if (first) return foundModule; 247 | rm.push(foundModule); 248 | } 249 | } 250 | else { 251 | let foundModule = null; 252 | if (exports.Z && wrappedFilter(exports.Z, module, index)) foundModule = defaultExport ? exports.Z : exports; 253 | if (exports.ZP && wrappedFilter(exports.ZP, module, index)) foundModule = defaultExport ? exports.ZP : exports; 254 | if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports; 255 | if (wrappedFilter(exports, module, index)) foundModule = exports; 256 | if (!foundModule) continue; 257 | if (raw) foundModule = module; 258 | if (first) return foundModule; 259 | rm.push(foundModule); 260 | } 261 | 262 | 263 | } 264 | 265 | return first || rm.length == 0 ? undefined : rm; 266 | } 267 | 268 | /** 269 | * Finds multiple modules using multiple filters. 270 | * 271 | * @param {...object} queries Whether to return only the first matching module 272 | * @param {Function} queries.filter A function to use to filter modules 273 | * @param {Boolean} [queries.first=true] Whether to return only the first matching module 274 | * @param {Boolean} [queries.defaultExport=true] Whether to return default export when matching the default export 275 | * @param {Boolean} [queries.searchExports=false] Whether to execute the filter on webpack export getters. 276 | * @param {Boolean} [queries.raw=false] Whether to return the whole Module object when matching exports 277 | * @return {Any} 278 | */ 279 | static getBulk(...queries) { 280 | const modules = this.getAllModules(); 281 | const returnedModules = Array(queries.length); 282 | const indices = Object.keys(modules); 283 | for (let i = 0; i < indices.length; i++) { 284 | const index = indices[i]; 285 | if (!modules.hasOwnProperty(index)) continue; 286 | const module = modules[index]; 287 | const {exports} = module; 288 | if (shouldSkipModule(exports)) continue; 289 | 290 | for (let q = 0; q < queries.length; q++) { 291 | const query = queries[q]; 292 | const {filter, first = true, defaultExport = true, searchExports = false, raw = false} = query; 293 | if (first && returnedModules[q]) continue; // If they only want the first, and we already found it, move on 294 | if (!first && !returnedModules[q]) returnedModules[q] = []; // If they want multiple and we haven't setup the subarry, do it now 295 | 296 | const wrappedFilter = wrapFilter(filter); 297 | 298 | if (typeof(exports) === "object" && searchExports && !exports.TypedArray) { 299 | for (const key in exports) { 300 | let foundModule = null; 301 | const wrappedExport = exports[key]; 302 | if (!wrappedExport) continue; 303 | if (wrappedFilter(wrappedExport, module, index)) foundModule = wrappedExport; 304 | if (!foundModule) continue; 305 | if (raw) foundModule = module; 306 | if (first) returnedModules[q] = foundModule; 307 | else returnedModules[q].push(foundModule); 308 | } 309 | } 310 | else { 311 | let foundModule = null; 312 | if (exports.Z && wrappedFilter(exports.Z, module, index)) foundModule = defaultExport ? exports.Z : exports; 313 | if (exports.ZP && wrappedFilter(exports.ZP, module, index)) foundModule = defaultExport ? exports.ZP : exports; 314 | if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, index)) foundModule = defaultExport ? exports.default : exports; 315 | if (wrappedFilter(exports, module, index)) foundModule = exports; 316 | if (!foundModule) continue; 317 | if (raw) foundModule = module; 318 | if (first) returnedModules[q] = foundModule; 319 | else returnedModules[q].push(foundModule); 320 | } 321 | } 322 | } 323 | 324 | return returnedModules; 325 | } 326 | 327 | /** 328 | * Searches for a module by value, returns module & matched key. Useful in combination with the Patcher. 329 | * @param {(value: any, index: number, array: any[]) => boolean} filter A function to use to filter the module 330 | * @param {object} [options] Set of options to customize the search 331 | * @param {any} [options.target=null] Optional module target to look inside. 332 | * @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export 333 | * @param {Boolean} [options.searchExports=false] Whether to execute the filter on webpack export getters. 334 | * @param {Boolean} [options.raw=false] Whether to return the whole Module object when matching exports 335 | * @return {[Any, string]} 336 | */ 337 | static *getWithKey(filter, {target = null, ...rest} = {}) { 338 | yield target ??= this.getModule(exports => 339 | Object.values(exports).some(filter), 340 | rest 341 | ); 342 | 343 | yield target && Object.keys(target).find(k => filter(target[k])); 344 | } 345 | 346 | /** 347 | * Gets a module's mangled properties by mapping them to friendly names. 348 | * @template T The type of the resulting object with friendly property names. 349 | * @memberof Webpack 350 | * @param {(exports: any, module: any, id: any) => boolean | string | RegExp} filter Filter to find the module. Can be a filter function, string, or RegExp for source matching. 351 | * @param {Record boolean>} mangled Object mapping desired property names to their filter functions. 352 | * @param {object} [options] Options to configure the search. 353 | * @param {boolean} [options.defaultExport=true] Whether to return default export when matching the default export. 354 | * @param {boolean} [options.searchExports=false] Whether to execute the filter on webpack exports. 355 | * @param {boolean} [options.raw=false] Whether to return the whole Module object when matching exports 356 | * @returns {T} Object containing the mangled properties with friendly names. 357 | */ 358 | static getMangled(filter, mangled, options = {}) { 359 | const {defaultExport = false, searchExports = false, raw = false} = options; 360 | 361 | if (typeof filter === "string" || filter instanceof RegExp) { 362 | filter = Filters.bySource(filter); 363 | } 364 | 365 | const returnValue = {}; 366 | let module = this.getModule( 367 | (exports, moduleInstance, id) => { 368 | if (!(exports instanceof Object)) return false; 369 | return filter(exports, moduleInstance, id); 370 | }, 371 | {defaultExport, searchExports, raw} 372 | ); 373 | 374 | if (!module) return returnValue; 375 | if (raw) module = module.exports; 376 | 377 | const mangledEntries = Object.entries(mangled); 378 | 379 | for (const searchKey in module) { 380 | if (!Object.prototype.hasOwnProperty.call(module, searchKey)) continue; 381 | 382 | for (const [key, propertyFilter] of mangledEntries) { 383 | if (key in returnValue) continue; 384 | 385 | if (propertyFilter(module[searchKey])) { 386 | Object.defineProperty(returnValue, key, { 387 | get() { 388 | return module[searchKey]; 389 | }, 390 | set(value) { 391 | module[searchKey] = value; 392 | }, 393 | enumerable: true, 394 | configurable: false, 395 | }); 396 | } 397 | } 398 | } 399 | 400 | return returnValue; 401 | } 402 | 403 | /** 404 | * Finds all modules matching a filter function. 405 | * @param {Function} filter A function to use to filter modules 406 | */ 407 | static getModules(filter) {return this.getModule(filter, {first: false});} 408 | 409 | /** 410 | * Finds a module by its display name. 411 | * @param {String} name The display name of the module 412 | * @return {Any} 413 | */ 414 | static getByDisplayName(name) { 415 | return this.getModule(Filters.byDisplayName(name)); 416 | } 417 | 418 | /** 419 | * Finds a module using its code. 420 | * @param {RegEx} regex A regular expression to use to filter modules 421 | * @param {Boolean} first Whether to return the only the first matching module 422 | * @return {Any} 423 | */ 424 | static getByRegex(regex, first = true) { 425 | return this.getModule(Filters.byRegex(regex), {first}); 426 | } 427 | 428 | /** 429 | * Finds a single module using properties on its prototype. 430 | * @param {...string} prototypes Properties to use to filter modules 431 | * @return {Any} 432 | */ 433 | static getByPrototypes(...prototypes) { 434 | return this.getModule(Filters.byPrototypeKeys(prototypes)); 435 | } 436 | 437 | /** 438 | * Finds all modules with a set of properties of its prototype. 439 | * @param {...string} prototypes Properties to use to filter modules 440 | * @return {Any} 441 | */ 442 | static getAllByPrototypes(...prototypes) { 443 | return this.getModule(Filters.byPrototypeKeys(prototypes), {first: false}); 444 | } 445 | 446 | /** 447 | * Finds a single module using its own properties. 448 | * @param {...string} props Properties to use to filter modules 449 | * @return {Any} 450 | */ 451 | static getByProps(...props) { 452 | return this.getModule(Filters.byKeys(props)); 453 | } 454 | 455 | /** 456 | * Finds all modules with a set of properties. 457 | * @param {...string} props Properties to use to filter modules 458 | * @return {Any} 459 | */ 460 | static getAllByProps(...props) { 461 | return this.getModule(Filters.byKeys(props), {first: false}); 462 | } 463 | 464 | /** 465 | * Finds a single module using a set of strings. 466 | * @param {...String} props Strings to use to filter modules 467 | * @return {Any} 468 | */ 469 | static getByString(...strings) { 470 | return this.getModule(Filters.byStrings(...strings)); 471 | } 472 | 473 | /** 474 | * Finds a module using its source code. 475 | * @param {String|RegExp} match String or regular expression to use to filter modules 476 | * @param {Boolean} first Whether to return only the first matching module 477 | * @return {Any} 478 | */ 479 | static getBySource(match, first = true) { 480 | return this.getModule(Filters.bySource(match), {first}); 481 | } 482 | 483 | /** 484 | * Finds all modules matching source code content. 485 | * @param {String|RegExp} match String or regular expression to use to filter modules 486 | * @return {Any} 487 | */ 488 | static getAllBySource(match) { 489 | return this.getModule(Filters.bySource(match), {first: false}); 490 | } 491 | 492 | /** 493 | * Finds all modules with a set of strings. 494 | * @param {...String} strings Strings to use to filter modules 495 | * @return {Any} 496 | */ 497 | static getAllByString(...strings) { 498 | return this.getModule(Filters.byStrings(...strings), {first: false}); 499 | } 500 | 501 | /** 502 | * Finds a module that lazily loaded. 503 | * @param {(m) => boolean} filter A function to use to filter modules. 504 | * @param {object} [options] Set of options to customize the search 505 | * @param {AbortSignal} [options.signal] AbortSignal of an AbortController to cancel the promise 506 | * @param {Boolean} [options.defaultExport=true] Whether to return default export when matching the default export 507 | * @param {Boolean} [options.searchExports=false] Whether to execute the filter on webpack export getters. 508 | * @param {Boolean} [options.raw=false] Whether to return the whole Module object when matching exports 509 | * @returns {Promise} 510 | */ 511 | static getLazy(filter, options = {}) { 512 | const {signal: abortSignal, defaultExport = true, searchExports = false, raw = false} = options; 513 | const fromCache = this.getModule(filter, {defaultExport, searchExports}); 514 | if (fromCache) return Promise.resolve(fromCache); 515 | 516 | const wrappedFilter = wrapFilter(filter); 517 | 518 | return new Promise((resolve) => { 519 | const cancel = () => this.removeListener(listener); 520 | const listener = function(exports, module, id) { 521 | if (shouldSkipModule(exports)) return; 522 | 523 | let foundModule = null; 524 | if (typeof(exports) === "object" && searchExports && !exports.TypedArray) { 525 | for (const key in exports) { 526 | foundModule = null; 527 | const wrappedExport = exports[key]; 528 | if (!wrappedExport) continue; 529 | if (wrappedFilter(wrappedExport, module, id)) foundModule = wrappedExport; 530 | } 531 | } 532 | else { 533 | if (exports.Z && wrappedFilter(exports.Z, module, id)) foundModule = defaultExport ? exports.Z : exports; 534 | if (exports.ZP && wrappedFilter(exports.ZP, module, id)) foundModule = defaultExport ? exports.ZP : exports; 535 | if (exports.__esModule && exports.default && wrappedFilter(exports.default, module, id)) foundModule = defaultExport ? exports.default : exports; 536 | if (wrappedFilter(exports, module, id)) foundModule = exports; 537 | 538 | } 539 | 540 | if (!foundModule) return; 541 | if (raw) foundModule = module; 542 | cancel(); 543 | resolve(foundModule); 544 | }; 545 | 546 | this.addListener(listener); 547 | abortSignal?.addEventListener("abort", () => { 548 | cancel(); 549 | resolve(); 550 | }); 551 | }); 552 | } 553 | 554 | /** 555 | * Discord's __webpack_require__ function. 556 | */ 557 | static get require() { 558 | if (this._require) return this._require; 559 | const id = Symbol("bdbrowser" + Math.random().toString().slice(2, 3)); 560 | let __discord_webpack_require__; 561 | 562 | if (typeof(window[this.chunkName]) !== "undefined") { 563 | window[this.chunkName].push([[id], 564 | {}, 565 | __internal_require__ => { 566 | if ("b" in __internal_require__) { 567 | __discord_webpack_require__ = __internal_require__; 568 | } 569 | } 570 | ]); 571 | } 572 | 573 | delete __discord_webpack_require__.m[id]; 574 | delete __discord_webpack_require__.c[id]; 575 | return this._require = __discord_webpack_require__; 576 | } 577 | 578 | /** 579 | * Returns all loaded modules. 580 | * @return {Array} 581 | */ 582 | static getAllModules() { 583 | return this.require.c; 584 | } 585 | 586 | // Webpack Chunk Observing 587 | static get chunkName() {return "webpackChunkdiscord_app";} 588 | 589 | static initialize() { 590 | this.handlePush = this.handlePush.bind(this); 591 | this.listeners = new Set(); 592 | 593 | this.__ORIGINAL_PUSH__ = window[this.chunkName].push; 594 | Object.defineProperty(window[this.chunkName], "push", { 595 | configurable: true, 596 | get: () => this.handlePush, 597 | set: (newPush) => { 598 | this.__ORIGINAL_PUSH__ = newPush; 599 | 600 | Object.defineProperty(window[this.chunkName], "push", { 601 | value: this.handlePush, 602 | configurable: true, 603 | writable: true 604 | }); 605 | } 606 | }); 607 | } 608 | 609 | /** 610 | * Adds a listener for when discord loaded a chunk. Useful for subscribing to lazy loaded modules. 611 | * @param {Function} listener - Function to subscribe for chunks 612 | * @returns {Function} A cancelling function 613 | */ 614 | static addListener(listener) { 615 | this.listeners.add(listener); 616 | return this.removeListener.bind(this, listener); 617 | } 618 | 619 | /** 620 | * Removes a listener for when discord loaded a chunk. 621 | * @param {Function} listener 622 | * @returns {boolean} 623 | */ 624 | static removeListener(listener) {return this.listeners.delete(listener);} 625 | 626 | static handlePush(chunk) { 627 | const [, modules] = chunk; 628 | 629 | for (const moduleId in modules) { 630 | const originalModule = modules[moduleId]; 631 | 632 | modules[moduleId] = (module, exports, require) => { 633 | try { 634 | Reflect.apply(originalModule, null, [module, exports, require]); 635 | 636 | const listeners = [...this.listeners]; 637 | for (let i = 0; i < listeners.length; i++) { 638 | try {listeners[i](exports, module, module.id);} 639 | catch (error) { 640 | Logger.stacktrace("WebpackModules", "Could not fire callback listener:", error); 641 | } 642 | } 643 | } 644 | catch (error) { 645 | Logger.stacktrace("WebpackModules", "Could not patch pushed module", error); 646 | } 647 | finally { 648 | require.m[moduleId] = originalModule; 649 | } 650 | }; 651 | 652 | Object.assign(modules[moduleId], originalModule, { 653 | toString: () => originalModule.toString() 654 | }); 655 | } 656 | 657 | return Reflect.apply(this.__ORIGINAL_PUSH__, window[this.chunkName], [chunk]); 658 | } 659 | } 660 | 661 | WebpackModules.initialize(); -------------------------------------------------------------------------------- /frontend/src/native_shims/discord_voice.js: -------------------------------------------------------------------------------- 1 | import Logger from "common/logger"; 2 | import {Buffer} from "node_shims/buffer"; 3 | import fs from "node_shims/fs"; 4 | 5 | let mediaRecorder; 6 | let audioChunks = []; 7 | let mediaStream; 8 | 9 | const MIME_TYPE = "audio/webm; codecs=opus"; 10 | const TARGET_FILE = "AppData/BetterDiscord/data/recording.webm"; 11 | 12 | export default { 13 | startLocalAudioRecording(options, callback) { 14 | if (typeof(options) === "function") { 15 | callback = options; 16 | options = {}; 17 | } 18 | 19 | navigator.mediaDevices.getUserMedia({audio: true}) 20 | .then(stream => { 21 | // Save the stream to stop it later 22 | mediaStream = stream; 23 | 24 | // Initialize the MediaRecorder, Chrome does not support audio/ogg! 25 | mediaRecorder = new MediaRecorder(stream, {mimeType: MIME_TYPE}); 26 | 27 | mediaRecorder.ondataavailable = event => { 28 | audioChunks.push(event.data); 29 | }; 30 | 31 | mediaRecorder.start(); 32 | 33 | if (callback) { 34 | callback(true); 35 | } 36 | }) 37 | .catch(err => { 38 | Logger.error("discord_voice", "Error during MediaRecorder initialization: ", err); 39 | 40 | if (callback) { 41 | callback(false); 42 | } 43 | }); 44 | }, 45 | stopLocalAudioRecording(asyncCallback) { 46 | if (mediaRecorder) { 47 | mediaRecorder.onstop = async () => { 48 | const audioBlob = new Blob(audioChunks, {type: MIME_TYPE}); 49 | const arrayBuffer = await audioBlob.arrayBuffer(); 50 | const buffer = Buffer.from(arrayBuffer); 51 | 52 | fs.writeFileSync(TARGET_FILE, buffer); 53 | 54 | // Cleanup audioChunks to save memory 55 | audioChunks = []; 56 | 57 | // Stop all tracks to release the microphone 58 | mediaStream.getTracks().forEach(track => track.stop()); 59 | 60 | // Now delete the mediaRecorder 61 | mediaRecorder = null; 62 | 63 | if (asyncCallback) { 64 | asyncCallback(TARGET_FILE); 65 | } 66 | }; 67 | mediaRecorder.stop(); 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /frontend/src/node_shims/base64-js.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var lookup = [] 3 | var revLookup = [] 4 | var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array 5 | 6 | var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 7 | for (var i = 0, len = code.length; i < len; ++i) { 8 | lookup[i] = code[i] 9 | revLookup[code.charCodeAt(i)] = i 10 | } 11 | 12 | // Support decoding URL-safe base64 strings, as Node.js does. 13 | // See: https://en.wikipedia.org/wiki/Base64#URL_applications 14 | revLookup['-'.charCodeAt(0)] = 62 15 | revLookup['_'.charCodeAt(0)] = 63 16 | 17 | function getLens (b64) { 18 | var len = b64.length 19 | 20 | if (len % 4 > 0) { 21 | throw new Error('Invalid string. Length must be a multiple of 4') 22 | } 23 | 24 | // Trim off extra bytes after placeholder bytes are found 25 | // See: https://github.com/beatgammit/base64-js/issues/42 26 | var validLen = b64.indexOf('=') 27 | if (validLen === -1) validLen = len 28 | 29 | var placeHoldersLen = validLen === len 30 | ? 0 31 | : 4 - (validLen % 4) 32 | 33 | return [validLen, placeHoldersLen] 34 | } 35 | 36 | // base64 is 4/3 + up to two characters of the original data 37 | function byteLength (b64) { 38 | var lens = getLens(b64) 39 | var validLen = lens[0] 40 | var placeHoldersLen = lens[1] 41 | return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen 42 | } 43 | 44 | function _byteLength (b64, validLen, placeHoldersLen) { 45 | return ((validLen + placeHoldersLen) * 3 / 4) - placeHoldersLen 46 | } 47 | 48 | function toByteArray (b64) { 49 | var tmp 50 | var lens = getLens(b64) 51 | var validLen = lens[0] 52 | var placeHoldersLen = lens[1] 53 | 54 | var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen)) 55 | 56 | var curByte = 0 57 | 58 | // if there are placeholders, only get up to the last complete 4 chars 59 | var len = placeHoldersLen > 0 60 | ? validLen - 4 61 | : validLen 62 | 63 | var i 64 | for (i = 0; i < len; i += 4) { 65 | tmp = 66 | (revLookup[b64.charCodeAt(i)] << 18) | 67 | (revLookup[b64.charCodeAt(i + 1)] << 12) | 68 | (revLookup[b64.charCodeAt(i + 2)] << 6) | 69 | revLookup[b64.charCodeAt(i + 3)] 70 | arr[curByte++] = (tmp >> 16) & 0xFF 71 | arr[curByte++] = (tmp >> 8) & 0xFF 72 | arr[curByte++] = tmp & 0xFF 73 | } 74 | 75 | if (placeHoldersLen === 2) { 76 | tmp = 77 | (revLookup[b64.charCodeAt(i)] << 2) | 78 | (revLookup[b64.charCodeAt(i + 1)] >> 4) 79 | arr[curByte++] = tmp & 0xFF 80 | } 81 | 82 | if (placeHoldersLen === 1) { 83 | tmp = 84 | (revLookup[b64.charCodeAt(i)] << 10) | 85 | (revLookup[b64.charCodeAt(i + 1)] << 4) | 86 | (revLookup[b64.charCodeAt(i + 2)] >> 2) 87 | arr[curByte++] = (tmp >> 8) & 0xFF 88 | arr[curByte++] = tmp & 0xFF 89 | } 90 | 91 | return arr 92 | } 93 | 94 | function tripletToBase64 (num) { 95 | return lookup[num >> 18 & 0x3F] + 96 | lookup[num >> 12 & 0x3F] + 97 | lookup[num >> 6 & 0x3F] + 98 | lookup[num & 0x3F] 99 | } 100 | 101 | function encodeChunk (uint8, start, end) { 102 | var tmp 103 | var output = [] 104 | for (var i = start; i < end; i += 3) { 105 | tmp = 106 | ((uint8[i] << 16) & 0xFF0000) + 107 | ((uint8[i + 1] << 8) & 0xFF00) + 108 | (uint8[i + 2] & 0xFF) 109 | output.push(tripletToBase64(tmp)) 110 | } 111 | return output.join('') 112 | } 113 | 114 | function fromByteArray (uint8) { 115 | var tmp 116 | var len = uint8.length 117 | var extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes 118 | var parts = [] 119 | var maxChunkLength = 16383 // must be multiple of 3 120 | 121 | // go through the array every three bytes, we'll deal with trailing stuff later 122 | for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { 123 | parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength))) 124 | } 125 | 126 | // pad the end with zeros, but make sure to not forget the extra bytes 127 | if (extraBytes === 1) { 128 | tmp = uint8[len - 1] 129 | parts.push( 130 | lookup[tmp >> 2] + 131 | lookup[(tmp << 4) & 0x3F] + 132 | '==' 133 | ) 134 | } else if (extraBytes === 2) { 135 | tmp = (uint8[len - 2] << 8) + uint8[len - 1] 136 | parts.push( 137 | lookup[tmp >> 10] + 138 | lookup[(tmp >> 4) & 0x3F] + 139 | lookup[(tmp << 2) & 0x3F] + 140 | '=' 141 | ) 142 | } 143 | 144 | return parts.join('') 145 | } 146 | 147 | export default { 148 | byteLength, 149 | toByteArray, 150 | fromByteArray 151 | }; 152 | -------------------------------------------------------------------------------- /frontend/src/node_shims/fsentry.js: -------------------------------------------------------------------------------- 1 | import {Buffer} from "node_shims/buffer"; 2 | import path from "node_shims/path"; 3 | 4 | export default class VfsEntry { 5 | constructor(fullName, nodeType) { 6 | this.fullName = fullName; 7 | this.pathName = path.dirname(this.fullName); 8 | this.nodeType = nodeType; 9 | } 10 | 11 | fullName = ""; 12 | pathName = ""; 13 | nodeType = undefined; 14 | birthtime = Date.now(); 15 | atime = Date.now(); 16 | ctime = Date.now(); 17 | mtime = Date.now(); 18 | contents = new Buffer([]); 19 | size = 0; 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/node_shims/https.js: -------------------------------------------------------------------------------- 1 | import {Buffer} from "node_shims/buffer"; 2 | import Events from "modules/events"; 3 | import fetch from "modules/fetch"; 4 | 5 | export function request(url, options, callback) { 6 | if (typeof options === "function") { 7 | callback = options; 8 | options = {}; 9 | } 10 | 11 | if (typeof url === "object") { 12 | options = JSON.parse(JSON.stringify(url)); 13 | options.url = undefined; 14 | url = url.url; 15 | } 16 | 17 | // TODO: Refactor `fetch()` into a more generic function 18 | // that does not require these hacks... 19 | options._wrapInResponse = false; 20 | 21 | const emitter = new Events(); 22 | 23 | callback(emitter); 24 | 25 | fetch(url, options) 26 | .then(data => { 27 | emitter.emit("data", Buffer.from(data.body)); 28 | 29 | const res = new Response(); 30 | Object.defineProperty(res, "headers", {value: data.headers}); 31 | Object.defineProperty(res, "ok", {value: data.ok}); 32 | Object.defineProperty(res, "redirected", {value: data.redirected}); 33 | Object.defineProperty(res, "status", {value: data.status}); 34 | Object.defineProperty(res, "statusCode", {value: data.status}); 35 | Object.defineProperty(res, "statusText", {value: data.statusText}); 36 | Object.defineProperty(res, "type", {value: data.type}); 37 | Object.defineProperty(res, "url", {value: data.url}); 38 | emitter.emit("end", res); 39 | }) 40 | .catch(error => { 41 | emitter.emit("error", error); 42 | }); 43 | 44 | return emitter; 45 | } 46 | 47 | export function createServer() { 48 | return { 49 | listen: () => {}, 50 | close: () => {} 51 | }; 52 | } 53 | 54 | export function get() { 55 | request.apply(this, arguments); 56 | } 57 | 58 | const https = request; 59 | https.get = request; 60 | https.createServer = createServer; 61 | https.request = request; 62 | 63 | export default https; 64 | -------------------------------------------------------------------------------- /frontend/src/node_shims/ieee754.js: -------------------------------------------------------------------------------- 1 | /* ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 2 | /* eslint-disable */ 3 | 4 | const read = function (buffer, offset, isLE, mLen, nBytes) { 5 | let e, m 6 | const eLen = (nBytes * 8) - mLen - 1 7 | const eMax = (1 << eLen) - 1 8 | const eBias = eMax >> 1 9 | let nBits = -7 10 | let i = isLE ? (nBytes - 1) : 0 11 | const d = isLE ? -1 : 1 12 | let s = buffer[offset + i] 13 | 14 | i += d 15 | 16 | e = s & ((1 << (-nBits)) - 1) 17 | s >>= (-nBits) 18 | nBits += eLen 19 | while (nBits > 0) { 20 | e = (e * 256) + buffer[offset + i] 21 | i += d 22 | nBits -= 8 23 | } 24 | 25 | m = e & ((1 << (-nBits)) - 1) 26 | e >>= (-nBits) 27 | nBits += mLen 28 | while (nBits > 0) { 29 | m = (m * 256) + buffer[offset + i] 30 | i += d 31 | nBits -= 8 32 | } 33 | 34 | if (e === 0) { 35 | e = 1 - eBias 36 | } else if (e === eMax) { 37 | return m ? NaN : ((s ? -1 : 1) * Infinity) 38 | } else { 39 | m = m + Math.pow(2, mLen) 40 | e = e - eBias 41 | } 42 | return (s ? -1 : 1) * m * Math.pow(2, e - mLen) 43 | } 44 | 45 | const write = function (buffer, value, offset, isLE, mLen, nBytes) { 46 | let e, m, c 47 | let eLen = (nBytes * 8) - mLen - 1 48 | const eMax = (1 << eLen) - 1 49 | const eBias = eMax >> 1 50 | const rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0) 51 | let i = isLE ? 0 : (nBytes - 1) 52 | const d = isLE ? 1 : -1 53 | const s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0 54 | 55 | value = Math.abs(value) 56 | 57 | if (isNaN(value) || value === Infinity) { 58 | m = isNaN(value) ? 1 : 0 59 | e = eMax 60 | } else { 61 | e = Math.floor(Math.log(value) / Math.LN2) 62 | if (value * (c = Math.pow(2, -e)) < 1) { 63 | e-- 64 | c *= 2 65 | } 66 | if (e + eBias >= 1) { 67 | value += rt / c 68 | } else { 69 | value += rt * Math.pow(2, 1 - eBias) 70 | } 71 | if (value * c >= 2) { 72 | e++ 73 | c /= 2 74 | } 75 | 76 | if (e + eBias >= eMax) { 77 | m = 0 78 | e = eMax 79 | } else if (e + eBias >= 1) { 80 | m = ((value * c) - 1) * Math.pow(2, mLen) 81 | e = e + eBias 82 | } else { 83 | m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen) 84 | e = 0 85 | } 86 | } 87 | 88 | while (mLen >= 8) { 89 | buffer[offset + i] = m & 0xff 90 | i += d 91 | m /= 256 92 | mLen -= 8 93 | } 94 | 95 | e = (e << mLen) | m 96 | eLen += mLen 97 | while (eLen > 0) { 98 | buffer[offset + i] = e & 0xff 99 | i += d 100 | e /= 256 101 | eLen -= 8 102 | } 103 | 104 | buffer[offset + i - d] |= s * 128 105 | } 106 | 107 | export default { 108 | read, 109 | write 110 | }; 111 | -------------------------------------------------------------------------------- /frontend/src/node_shims/mime-types.js: -------------------------------------------------------------------------------- 1 | import {extname} from "node_shims/path"; 2 | import db from "assets/mime-db.json"; 3 | 4 | // mime-types, mime-db 5 | // Copyright (c) 2014 Jonathan Ong 6 | // Copyright (c) 2015-2022 Douglas Christopher Wilson 7 | // MIT Licensed 8 | // 9 | // ============================================================================ 10 | // 11 | // (The MIT License) 12 | // 13 | // Copyright (c) 2014 Jonathan Ong 14 | // Copyright (c) 2015-2022 Douglas Christopher Wilson 15 | // 16 | // Permission is hereby granted, free of charge, to any person obtaining 17 | // a copy of this software and associated documentation files (the 18 | // 'Software'), to deal in the Software without restriction, including 19 | // without limitation the rights to use, copy, modify, merge, publish, 20 | // distribute, sublicense, and/or sell copies of the Software, and to 21 | // permit persons to whom the Software is furnished to do so, subject to 22 | // the following conditions: 23 | // 24 | // The above copyright notice and this permission notice shall be 25 | // included in all copies or substantial portions of the Software. 26 | // 27 | // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 28 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 29 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 30 | // IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 31 | // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 32 | // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 33 | // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | // 35 | // ============================================================================ 36 | 37 | /** 38 | * Module variables. 39 | * @private 40 | */ 41 | 42 | const EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/; 43 | const TEXT_TYPE_REGEXP = /^text\//i; 44 | 45 | const extensionMapList = Object.create(null); 46 | const typeMapList = Object.create(null); 47 | 48 | // Populate the extensions/types maps 49 | populateMaps(extensionMapList, typeMapList); 50 | 51 | /** 52 | * Get the default charset for a MIME type. 53 | * 54 | * @param {string} inputMimeType 55 | * @return {boolean|string} 56 | */ 57 | function getCharset (inputMimeType) { 58 | if (!inputMimeType || typeof inputMimeType !== "string") { 59 | return false; 60 | } 61 | 62 | const match = EXTRACT_TYPE_REGEXP.exec(inputMimeType); 63 | const mime = match && db[match[1].toLowerCase()]; 64 | 65 | if (mime && mime.charset) { 66 | return mime.charset; 67 | } 68 | 69 | // default text/* to utf-8 70 | if (match && TEXT_TYPE_REGEXP.test(match[1])) { 71 | return "UTF-8"; 72 | } 73 | 74 | return false; 75 | } 76 | 77 | /** 78 | * Create a full Content-Type header given a MIME type or extension. 79 | * 80 | * @param {string} inputData 81 | * @return {boolean|string} 82 | */ 83 | function getContentType (inputData) { 84 | if (!inputData || typeof inputData !== "string") return false; 85 | 86 | let mime = inputData; 87 | 88 | if (inputData.indexOf("/") === -1) { 89 | mime = lookupMimeType(inputData); 90 | } 91 | 92 | if (!mime) return false; 93 | 94 | if (mime.indexOf("charset") === -1) { 95 | const detectedCharset = getCharset(mime); 96 | if (detectedCharset) { 97 | mime += "; charset=" + detectedCharset.toLowerCase(); 98 | } 99 | } 100 | 101 | return mime; 102 | } 103 | 104 | /** 105 | * Get the default extension for a MIME type. 106 | * 107 | * @param {string} inputContentType 108 | * @return {boolean|string} 109 | */ 110 | function getExtension (inputContentType) { 111 | if (!inputContentType || typeof inputContentType !== "string") return false; 112 | 113 | const match = EXTRACT_TYPE_REGEXP.exec(inputContentType); 114 | const possibleExtensions = match && extensionMapList[match[1].toLowerCase()]; 115 | 116 | if (!possibleExtensions || !possibleExtensions.length) return false; 117 | 118 | return possibleExtensions[0]; 119 | } 120 | 121 | /** 122 | * Lookup the MIME type for a file path/extension. 123 | * 124 | * @param {string} path 125 | * @return {boolean|string} 126 | */ 127 | function lookupMimeType (path) { 128 | if (!path || typeof path !== "string") return false; 129 | 130 | const extension = extname("x." + path) 131 | .toLowerCase() 132 | .substring(1); 133 | 134 | if (!extension) return false; 135 | 136 | return typeMapList[extension] || false; 137 | } 138 | 139 | /** 140 | * Populate the extensions and types maps. 141 | * @private 142 | */ 143 | function populateMaps (extensionMap, typeMap) { 144 | const preference = ["nginx", "apache", undefined, "iana"]; 145 | 146 | Object.keys(db).forEach((type) => { 147 | const mime = db[type]; 148 | const fileExtensions = mime.extensions; 149 | 150 | if (!fileExtensions || !fileExtensions.length) return; 151 | 152 | // mime -> extensions 153 | extensionMap[type] = fileExtensions; 154 | 155 | // extension -> mime 156 | for (let i = 0; i < fileExtensions.length; i++) { 157 | const extension = fileExtensions[i]; 158 | 159 | if (typeMap[extension]) { 160 | const from = preference.indexOf(db[typeMap[extension]].source); 161 | const to = preference.indexOf(mime.source); 162 | 163 | if (typeMap[extension] !== "application/octet-stream" && (from > to || (from === to && typeMap[extension].substring(0, 12) === "application/"))) { 164 | continue; 165 | } 166 | } 167 | 168 | // set the extension -> mime 169 | typeMap[extension] = type; 170 | } 171 | }); 172 | } 173 | 174 | export default { 175 | charset: getCharset, 176 | contentType: getContentType, 177 | extension: getExtension, 178 | lookup: lookupMimeType 179 | }; 180 | -------------------------------------------------------------------------------- /frontend/src/node_shims/path.js: -------------------------------------------------------------------------------- 1 | import {normalizePath} from "node_shims/fs"; 2 | 3 | export function join(...paths) { 4 | let final = ""; 5 | for (let path of paths) { 6 | if (!path) continue; 7 | 8 | path = normalizePath(path); 9 | 10 | if (path[0] === "/") { 11 | path = path.slice(1); 12 | } 13 | 14 | final += path[path.length - 1] === "/" ? path : path + "/"; 15 | } 16 | return final[final.length - 1] === "/" ? final.slice(0, final.length - 1) : final; 17 | } 18 | 19 | export function basename(filename) { 20 | if (typeof(filename) !== "string") { 21 | throw Object.assign(new TypeError(`The "filename" argument must be of type string. Received ${typeof(filename)}.`), { 22 | code: "ERR_INVALID_ARG_TYPE", 23 | }); 24 | } 25 | 26 | return filename?.split("/")?.slice(-1)[0]; 27 | } 28 | 29 | export function resolve(...paths) { 30 | return join(...paths); 31 | } 32 | 33 | export function extname(path) { 34 | let ext = path?.split(".")?.slice(-1)[0]; 35 | 36 | if (ext) { 37 | ext = ".".concat(ext); 38 | } 39 | 40 | return ext; 41 | } 42 | 43 | export function dirname(path) { 44 | return path?.split("/")?.slice(0, -1)?.join("/"); 45 | } 46 | 47 | export function isAbsolute(path) { 48 | path = normalizePath(path); 49 | return path?.startsWith("AppData/"); 50 | } 51 | 52 | export default { 53 | basename, 54 | dirname, 55 | extname, 56 | isAbsolute, 57 | join, 58 | resolve 59 | }; 60 | -------------------------------------------------------------------------------- /frontend/src/node_shims/request.js: -------------------------------------------------------------------------------- 1 | import {IPCEvents} from "common/constants"; 2 | import ipcRenderer from "modules/ipc"; 3 | 4 | const methods = ["get", "put", "post", "delete", "head"]; 5 | const aliases = {del: "delete"}; 6 | 7 | function parseArguments() { 8 | let url, options, callback; 9 | 10 | for (const arg of arguments) { 11 | switch (typeof arg) { 12 | case (arg !== null && "object"): 13 | options = arg; 14 | if ("url" in options) { 15 | url = options.url; 16 | } 17 | break; 18 | 19 | case (!url && "string"): 20 | url = arg; 21 | break; 22 | 23 | case (!callback && "function"): 24 | callback = arg; 25 | break; 26 | } 27 | } 28 | 29 | return {url, options, callback}; 30 | } 31 | 32 | function validOptions(url, callback) { 33 | return typeof url === "string" && typeof callback === "function"; 34 | } 35 | 36 | export default function request() { 37 | const {url, options = {}, callback} = parseArguments.apply(this, arguments); 38 | 39 | if (!validOptions(url, callback)) { 40 | return null; 41 | } 42 | 43 | ipcRenderer.send(IPCEvents.MAKE_REQUESTS, { 44 | url: url, options: options 45 | }, data => { 46 | let bodyData; 47 | 48 | // If the "encoding" parameter is present in the original options, and it is 49 | // set to null, the return value should be an ArrayBuffer. Otherwise, check 50 | // the Mime database for the type to determine whether it is text or not... 51 | if ("encoding" in options && options.encoding === null) { 52 | bodyData = data.body; 53 | } 54 | else if ("Content-Type" in Object(options.headers) && options.headers["Content-Type"] !== "text/plain") { 55 | bodyData = data.body; 56 | } 57 | else { 58 | bodyData = new TextDecoder().decode(data.body); 59 | } 60 | 61 | const res = { 62 | headers: data.headers, 63 | aborted: !data.ok, 64 | complete: true, 65 | end: undefined, 66 | statusCode: data.status, 67 | statusMessage: data.statusText, 68 | url: "" 69 | }; 70 | 71 | callback(null, res, bodyData); 72 | }); 73 | } 74 | 75 | Object.assign(request, Object.fromEntries( 76 | methods.concat(Object.keys(aliases)).map(method => [method, function () { 77 | const {url, options = {}, callback} = parseArguments.apply(this, arguments); 78 | if (!validOptions(url, callback)) return null; 79 | options.method = method; 80 | request(url, options, callback); 81 | }]) 82 | )); 83 | -------------------------------------------------------------------------------- /frontend/src/node_shims/require.js: -------------------------------------------------------------------------------- 1 | import electron from "app_shims/electron"; 2 | import Events from "modules/events"; 3 | import fs from "node_shims/fs"; 4 | import Https from "node_shims/https"; 5 | import mimeTypes from "node_shims/mime-types"; 6 | import Module from "modules/module"; 7 | import * as vm from "node_shims/vm"; 8 | import path from "node_shims/path"; 9 | import process from "app_shims/process"; 10 | import RequestModule from "node_shims/request"; 11 | import buffer from "node_shims/buffer"; 12 | 13 | export default function require(mod) { 14 | switch (mod) { 15 | case "buffer": 16 | return buffer.Buffer; 17 | 18 | case "child_process": 19 | return; 20 | 21 | case "electron": 22 | return electron; 23 | 24 | case "events": 25 | return Events; 26 | 27 | case "fs": 28 | case "original-fs": 29 | return fs; 30 | 31 | case "http": 32 | case "https": 33 | return Https; 34 | 35 | case "mime-types": 36 | return mimeTypes; 37 | 38 | case "module": 39 | return Module; 40 | 41 | case "path": 42 | return path; 43 | 44 | case "process": 45 | return process; 46 | 47 | case "request": 48 | return RequestModule; 49 | 50 | case "url": 51 | return { 52 | parse: (urlString) => { 53 | return new URL(urlString); 54 | } 55 | }; 56 | 57 | case "vm": 58 | return vm; 59 | 60 | default: 61 | return Module._require(mod, require); 62 | } 63 | } 64 | 65 | require.cache = {}; 66 | require.resolve = (modulePath) => { 67 | for (const key of Object.keys(require.cache)) { 68 | if (key.startsWith(modulePath)) { 69 | return require.cache[key]; 70 | } 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /frontend/src/node_shims/vm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compiles a function from a string. 3 | * @param code - The code to compile. 4 | * @param args - The arguments to pass to the function. 5 | * @returns function 6 | */ 7 | export function compileFunction(code, args = []) { 8 | try { 9 | // eslint-disable-next-line no-eval 10 | return eval(`((${args.join(", ")}) => { 11 | try { 12 | ${code} 13 | } 14 | catch (e) { 15 | console.error("Could not load:", e); 16 | } 17 | })`); 18 | } 19 | catch (error) { 20 | return { 21 | name: error.name, 22 | message: error.message, 23 | stack: error.stack 24 | }; 25 | } 26 | } 27 | 28 | export default {compileFunction}; 29 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = (env, argv) => ({ 4 | mode: "development", 5 | target: "node", 6 | devtool: argv.mode === "production" ? undefined : "eval-source-map", 7 | entry: path.resolve(__dirname, "src", "index.js"), 8 | optimization: { 9 | minimize: false 10 | }, 11 | output: { 12 | filename: "frontend.js", 13 | path: path.resolve(__dirname, "..", "dist", "js") 14 | }, 15 | resolve: { 16 | extensions: [".js", ".jsx"], 17 | modules: [ 18 | path.resolve(__dirname, "src", "modules") 19 | ], 20 | alias: { 21 | assets: path.join(__dirname, "..", "assets"), 22 | common: path.join(__dirname, "..", "common"), 23 | modules: path.join(__dirname, "src", "modules"), 24 | app_shims: path.join(__dirname, "src", "app_shims"), 25 | node_shims: path.join(__dirname, "src", "node_shims"), 26 | native_shims: path.join(__dirname, "src", "native_shims") 27 | } 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /licenses/arrpc/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OpenAsar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /licenses/arrpc/arrpc.txt: -------------------------------------------------------------------------------- 1 | arRPC 2 | Copyright(c) 2022 OpenAsar 3 | MIT Licensed 4 | 5 | Homepage: https://github.com/OpenAsar/arrpc 6 | 7 | assets/bd-plugins/arRPCBridge.plugin.js is loosely based on 8 | code from the arRPC repository: examples/bridge_mod.js (commit 877b71f). 9 | -------------------------------------------------------------------------------- /licenses/asar-peeker/LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /licenses/asar-peeker/asar-peeker.txt: -------------------------------------------------------------------------------- 1 | asar-peeker 2 | Copyright © 2021, Dakedres (for territories where public domain is inapplicable) 3 | Unlicense license 4 | 5 | Homepage: https://github.com/Dakedres/asar-peeker 6 | 7 | This project uses the dist/Asar.module.js from asar-peeker (commit 262900a), residing in frontend/src/modules/asar.js. 8 | -------------------------------------------------------------------------------- /licenses/base64-js/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jameson Little 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /licenses/base64-js/base64-js.txt: -------------------------------------------------------------------------------- 1 | base64-js 2 | Copyright (c) 2014 Jameson Little 3 | MIT Licensed 4 | 5 | Homepage: https://github.com/beatgammit/base64-js 6 | 7 | This project uses the index.js from base64-js (commit: 88957c9), residing in frontend/src/node_shims/base64-js.js. 8 | Some minor changes were made to the file to make it work in the scope of this project. 9 | -------------------------------------------------------------------------------- /licenses/betterdiscord/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | -------------------------------------------------------------------------------- /licenses/betterdiscord/betterdiscord.txt: -------------------------------------------------------------------------------- 1 | BetterDiscord 2 | Copyright © 2019-2023, BetterDiscord Team 3 | Apache License 2.0 4 | 5 | Homepage: https://github.com/BetterDiscord/BetterDiscord 6 | https://betterdiscord.app 7 | 8 | The following files are used in a modified state from their upstream counterparts: 9 | - frontend/src/modules/fetch/nativefetch.js (BetterDiscord: preload/src/api/fetch.js) 10 | - frontend/src/modules/webpack.js (BetterDiscord: renderer/src/modules/webpackmodules.js) 11 | - frontend/src/node_shims/request.js (BetterDiscord: renderer/src/polyfill/request.js) 12 | - preload/src/index.js (BetterDiscord: preload/src/patcher.js) 13 | -------------------------------------------------------------------------------- /licenses/buffer/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Feross Aboukhadijeh, and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /licenses/buffer/buffer.txt: -------------------------------------------------------------------------------- 1 | buffer 2 | Copyright (c) Feross Aboukhadijeh, and other contributors. 3 | MIT Licensed 4 | 5 | Homepage: https://github.com/feross/buffer 6 | 7 | This project uses the index.js from buffer (commit: 50cae94), residing in frontend/src/node_shims/buffer.js. 8 | Some minor changes were made to the file to make it work in the scope of this project. 9 | -------------------------------------------------------------------------------- /licenses/ieee754/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2008 Fair Oaks Labs, Inc. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /licenses/ieee754/ieee754.txt: -------------------------------------------------------------------------------- 1 | ieee754 2 | Copyright 2008 Fair Oaks Labs, Inc. 3 | BSD 3-Clause "New" or "Revised" License 4 | 5 | Homepage: https://github.com/feross/ieee754 6 | 7 | This project uses the index.js from ieee754 (commit: 8a0041f), residing in frontend/src/node_shims/ieee754.js. 8 | Some minor changes were made to the file to make it work in the scope of this project. 9 | -------------------------------------------------------------------------------- /licenses/mime-db/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Jonathan Ong 4 | Copyright (c) 2015-2022 Douglas Christopher Wilson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /licenses/mime-db/mime-db.txt: -------------------------------------------------------------------------------- 1 | mime-db 2 | Copyright(c) 2014 Jonathan Ong 3 | Copyright(c) 2015-2022 Douglas Christopher Wilson 4 | MIT Licensed 5 | 6 | Homepage: https://github.com/jshttp/mime-db 7 | 8 | This project uses the db.json from mime-db, residing in assets/mime-db.json. 9 | -------------------------------------------------------------------------------- /licenses/mime-types/LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Jonathan Ong 4 | Copyright (c) 2015-2022 Douglas Christopher Wilson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /licenses/mime-types/mime-types.txt: -------------------------------------------------------------------------------- 1 | mime-types 2 | Copyright(c) 2014 Jonathan Ong 3 | Copyright(c) 2015 Douglas Christopher Wilson 4 | MIT Licensed 5 | 6 | Homepage: https://github.com/jshttp/mime-types 7 | 8 | This project uses a slightly modified version of the mime-types project's index.js, 9 | residing in frontend/src/node_shims/mime-types.js. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bdbrowser", 3 | "description": "An unofficial BetterDiscord injector for the web-client", 4 | "version": "0.0.0", 5 | "author": "Strencher", 6 | "license": "MIT", 7 | "scripts": { 8 | "build-backend": "pnpm --filter backend build", 9 | "build-frontend": "pnpm --filter frontend build", 10 | "build-preload": "pnpm --filter preload build", 11 | "build-service": "pnpm --filter service build", 12 | "copy-assets": "copyfiles -e \"assets/chrome/_locales/**\" -e \"assets/chrome/manifest.json\" -e \"assets/gh-readme/**\" \"assets/**\" \"dist/\"", 13 | "copy-bd": "mkdirp \"dist/bd/\" && echo \"Put the betterdiscord.asar or BetterDiscord's renderer.js in this folder to use a local version that overrides the VFS.\" > \"dist/bd/README.txt\"", 14 | "copy-licenses": "copyfiles -f \"LICENSE\" \"dist/\" && copyfiles \"licenses/**\" \"dist/\"", 15 | "copy-locales": "copyfiles -u 2 \"assets/chrome/_locales/**\" \"dist/\"", 16 | "copy-manifest": "copyfiles -f \"assets/chrome/manifest.json\" \"dist/\"", 17 | "copy-to-dist": "pnpm run copy-assets && pnpm run copy-locales && pnpm run copy-manifest && pnpm run copy-bd && pnpm run copy-licenses", 18 | "build": "pnpm run build-backend && pnpm run build-frontend && pnpm run build-preload && pnpm run build-service && pnpm run copy-to-dist", 19 | "build-prod": "pnpm run lint && pnpm --filter backend build-prod && pnpm --filter frontend build-prod && pnpm --filter preload build-prod && pnpm --filter service build-prod && pnpm run copy-to-dist && echo Finished Build", 20 | "lint": "eslint --ext .js common/ && pnpm --filter backend lint && pnpm --filter frontend lint && pnpm --filter preload lint && pnpm --filter service lint", 21 | "test": "echo \"Error: no test specified\" && exit 1" 22 | }, 23 | "dependencies": { 24 | "@babel/core": "^7.26.0", 25 | "@babel/plugin-proposal-class-properties": "^7.18.6", 26 | "@babel/plugin-proposal-optional-chaining": "^7.21.0", 27 | "copyfiles": "^2.4.1", 28 | "eslint": "^8.57.1", 29 | "mkdirp": "^3.0.1", 30 | "webpack": "^5.97.1", 31 | "webpack-cli": "^4.10.0" 32 | }, 33 | "pnpm": { 34 | "overrides": { 35 | "braces@<3.0.3": ">=3.0.3", 36 | "webpack@<5.94.0": ">=5.94.0", 37 | "micromatch@<4.0.8": ">=4.0.8" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "backend/" 3 | - "frontend/" 4 | - "preload/" 5 | - "service/" 6 | -------------------------------------------------------------------------------- /preload/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "common": ["../common"] 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /preload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bdbrowser/preload", 3 | "description": "BDBrowser Preload", 4 | "version": "0.0.0", 5 | "main": "src/index.js", 6 | "private": true, 7 | "scripts": { 8 | "build": "webpack --progress --color", 9 | "build-prod": "webpack --stats minimal --mode production", 10 | "lint": "eslint --ext .js src/" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /preload/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script is injected by `backend.js` into Discord's webpage during 3 | * document_start to execute early in Discord's (not BetterDiscord's!) loading sequence. 4 | */ 5 | import Logger from "common/logger"; 6 | 7 | /** 8 | * This code is taken from BetterDiscord's preload/src/patcher.js. 9 | * Keep it as vanilla as possible to make it easier to update. 10 | */ 11 | (() =>{ 12 | const chunkName = "webpackChunkdiscord_app"; 13 | const predefine = function (target, prop, effect) { 14 | const value = target[prop]; 15 | Object.defineProperty(target, prop, { 16 | get() {return value;}, 17 | set(newValue) { 18 | Object.defineProperty(target, prop, { 19 | value: newValue, 20 | configurable: true, 21 | enumerable: true, 22 | writable: true 23 | }); 24 | 25 | try { 26 | effect(newValue); 27 | } 28 | catch (error) { 29 | Logger.error("Preload", error); 30 | } 31 | 32 | // eslint-disable-next-line no-setter-return 33 | return newValue; 34 | }, 35 | configurable: true 36 | }); 37 | }; 38 | 39 | if (Reflect.has(window, "localStorage")) { 40 | Logger.log("Preload", "Saving instance of localStorage for internal use..."); 41 | 42 | // Normally this should be considered problematic, however the bdbrowserLocalStorage 43 | // will be picked up and deleted by the LocalStorage class in the frontend. 44 | window.bdbrowserLocalStorage = window.localStorage; 45 | } 46 | 47 | if (!Reflect.has(window, chunkName)) { 48 | Logger.log("Preload", `Preparing ${chunkName} to be configurable...`); 49 | 50 | predefine(window, chunkName, instance => { 51 | instance.push([[Symbol()], {}, require => { 52 | require.d = (target, exports) => { 53 | for (const key in exports) { 54 | if (!Reflect.has(exports, key)) continue; 55 | 56 | try { 57 | Object.defineProperty(target, key, { 58 | get: () => exports[key](), 59 | set: v => {exports[key] = () => v;}, 60 | enumerable: true, 61 | configurable: true 62 | }); 63 | } 64 | catch (error) { 65 | Logger.error("Preload", error); 66 | } 67 | } 68 | }; 69 | }]); 70 | }); 71 | } 72 | })(); 73 | -------------------------------------------------------------------------------- /preload/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = (env, argv) => ({ 4 | mode: "development", 5 | target: "node", 6 | devtool: argv.mode === "production" ? undefined : "eval-source-map", 7 | entry: path.resolve(__dirname, "src", "index.js"), 8 | optimization: { 9 | minimize: false 10 | }, 11 | output: { 12 | filename: "preload.js", 13 | path: path.resolve(__dirname, "..", "dist", "js") 14 | }, 15 | resolve: { 16 | extensions: [".js", ".jsx"], 17 | modules: [ 18 | path.resolve(__dirname, "src", "modules") 19 | ], 20 | alias: { 21 | common: path.join(__dirname, "..", "common"), 22 | assets: path.join(__dirname, "..", "assets") 23 | } 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /service/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "common": ["../common"] 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bdbrowser/service", 3 | "description": "BDBrowser Service Worker", 4 | "version": "0.0.0", 5 | "main": "src/index.js", 6 | "private": true, 7 | "scripts": { 8 | "build": "webpack --progress --color", 9 | "build-prod": "webpack --stats minimal --mode production", 10 | "lint": "eslint --ext .js src/" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /service/src/index.js: -------------------------------------------------------------------------------- 1 | import Logger from "common/logger"; 2 | import decNetReqRules from "./modules/declarativenetrequest"; 3 | import decNetReqDebug from "./modules/dnrdebug"; 4 | import frontendEvents from "./modules/fetch"; 5 | 6 | (() => { 7 | Logger.log("Service", "Initializing service worker..."); 8 | 9 | decNetReqDebug.registerEvents(); 10 | decNetReqRules.registerEvents(); 11 | frontendEvents.registerEvents(); 12 | })(); 13 | -------------------------------------------------------------------------------- /service/src/modules/declarativenetrequest.js: -------------------------------------------------------------------------------- 1 | import Logger from "common/logger"; 2 | 3 | const DISCORD_APP_DOMAINS = ["discord.com", "canary.discord.com", "ptb.discord.com"]; 4 | const NET_REQUEST_RULE_IDS = [1, 2]; 5 | 6 | // Rule blocks any Discord webhook being accessed/called from within Discord's domains. 7 | // Same thing renderer/src/secure.js from BetterDiscord does but via dNR. 8 | function getBlockWebhookRule() { 9 | return { 10 | id: NET_REQUEST_RULE_IDS[0], 11 | priority: 100, 12 | condition: { 13 | initiatorDomains: DISCORD_APP_DOMAINS.concat([location.hostname]), 14 | regexFilter: "(http|https)://discord.com/api/webhooks/.*" 15 | }, 16 | action: { 17 | type: "block" 18 | } 19 | }; 20 | } 21 | 22 | // Rule removes/eases the Content Security Policy from Discord's domains. 23 | // Same thing injector/src/modules/csp.js from BetterDiscord does but via dNR. 24 | function getAlterContentSecurityPolicyRule(cspHeaderValue) { 25 | return { 26 | id: NET_REQUEST_RULE_IDS[1], 27 | priority: 100, 28 | condition: { 29 | requestDomains: DISCORD_APP_DOMAINS, 30 | // resourceTypes needs to be set for this to work! 31 | resourceTypes: ["main_frame"] 32 | }, 33 | action: { 34 | type: "modifyHeaders", 35 | responseHeaders: [{ 36 | header: "Content-Security-Policy", 37 | operation: ((cspHeaderValue !== undefined) ? "set" : "remove"), 38 | value: cspHeaderValue 39 | }] 40 | } 41 | }; 42 | } 43 | 44 | function installOrUpdateRules() { 45 | // Remove the rules before trying to work with the CSP headers! 46 | // Otherwise, we potentially work with pre-tainted headers... 47 | chrome.declarativeNetRequest.updateSessionRules({removeRuleIds: NET_REQUEST_RULE_IDS}).then(() => { 48 | const netRequestRulesList = []; 49 | netRequestRulesList.push(getBlockWebhookRule()); 50 | netRequestRulesList.push(getAlterContentSecurityPolicyRule()); 51 | 52 | Logger.log("Service", "Installing/updating Declarative Net Request rules..."); 53 | chrome.declarativeNetRequest.updateSessionRules({addRules: netRequestRulesList}).then(() => { 54 | Logger.log("Service", "Declarative Net Request rules updated!"); 55 | }); 56 | }); 57 | } 58 | 59 | function removeRules() { 60 | Logger.log("Service", "Removing Declarative Net Request rules."); 61 | chrome.declarativeNetRequest.updateSessionRules({removeRuleIds: NET_REQUEST_RULE_IDS}); 62 | } 63 | 64 | function registerEvents() { 65 | chrome.runtime.onInstalled.addListener(installOrUpdateRules); 66 | chrome.runtime.onStartup.addListener(installOrUpdateRules); 67 | chrome.runtime.onSuspend.addListener(removeRules); 68 | chrome.runtime.onSuspendCanceled.addListener(installOrUpdateRules); 69 | } 70 | 71 | export default { 72 | registerEvents 73 | }; 74 | -------------------------------------------------------------------------------- /service/src/modules/dnrdebug.js: -------------------------------------------------------------------------------- 1 | import Logger from "common/logger"; 2 | 3 | function registerEvents() { 4 | chrome.permissions.contains({permissions: ["declarativeNetRequestFeedback"]}, enableOnRuleMatchedDebug); 5 | } 6 | 7 | function enableOnRuleMatchedDebug(hasPermission) { 8 | if (hasPermission) { 9 | Logger.log("Service", "Registering onRuleMatchedDebug listener."); 10 | chrome.declarativeNetRequest.onRuleMatchedDebug.addListener(processOnRuleMatchedDebug); 11 | } 12 | } 13 | 14 | function processOnRuleMatchedDebug(matchedRuleInfo) { 15 | Logger.log("Debug", `Matched rule, Initiator: ${matchedRuleInfo.request.initiator}, Requested URL ${matchedRuleInfo.request.url}`); 16 | } 17 | 18 | export default { 19 | registerEvents 20 | }; 21 | -------------------------------------------------------------------------------- /service/src/modules/fetch.js: -------------------------------------------------------------------------------- 1 | import Logger from "common/logger"; 2 | 3 | function registerEvents() { 4 | Logger.log("Service", "Registering Message events."); 5 | chrome.runtime.onMessage.addListener( 6 | function(request, sender, sendResponse) { 7 | if (request.operation === "fetch") { 8 | processFetchMessage(request) 9 | .then(result => sendResponse(result)); 10 | return true; 11 | } 12 | } 13 | ); 14 | } 15 | 16 | async function processFetchMessage(request) { 17 | let returnValue; 18 | try { 19 | const fetchOptions = request.parameters.options || {}; 20 | const fetchResponse = await fetch(request.parameters.url, fetchOptions); 21 | const fetchBody = await fetchResponse.arrayBuffer(); 22 | 23 | const defaultStatusMessages = { 24 | 200: "OK", 25 | 201: "Created", 26 | 204: "No Content", 27 | 400: "Bad Request", 28 | 401: "Unauthorized", 29 | 403: "Forbidden", 30 | 404: "Not Found", 31 | 500: "Internal Server Error", 32 | }; 33 | 34 | returnValue = { 35 | ok: fetchResponse.ok, 36 | redirected: fetchResponse.redirected, 37 | status: fetchResponse.status, 38 | statusText: fetchResponse.statusText || defaultStatusMessages[fetchResponse.status], 39 | type: fetchResponse.type, 40 | body: Array.from(new Uint8Array(fetchBody)), 41 | headers: Object.fromEntries(fetchResponse.headers), 42 | url: fetchResponse.url 43 | }; 44 | } 45 | catch (err) { 46 | returnValue = { 47 | error: err.toString() 48 | }; 49 | } 50 | 51 | return returnValue; 52 | } 53 | 54 | export default { 55 | registerEvents 56 | }; 57 | -------------------------------------------------------------------------------- /service/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = (env, argv) => ({ 4 | mode: "development", 5 | target: "node", 6 | devtool: argv.mode === "production" ? undefined : "eval-source-map", 7 | entry: path.resolve(__dirname, "src", "index.js"), 8 | optimization: { 9 | minimize: false 10 | }, 11 | output: { 12 | filename: "service.js", 13 | path: path.resolve(__dirname, "..", "dist", "js") 14 | }, 15 | resolve: { 16 | extensions: [".js", ".jsx"], 17 | modules: [ 18 | path.resolve(__dirname, "src", "modules") 19 | ], 20 | alias: { 21 | common: path.join(__dirname, "..", "common"), 22 | assets: path.join(__dirname, "..", "assets") 23 | } 24 | } 25 | }); 26 | --------------------------------------------------------------------------------