├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml ├── config.yml └── workflows │ └── release.yml ├── .gitignore ├── .releaserc.js ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── logo │ ├── logo-dark.svg │ └── logo-light.svg └── wordmark │ ├── wordmark+slogan-dark.svg │ └── wordmark+slogan-light.svg ├── docs ├── 2_building.md └── README.md ├── eslint.config.mjs ├── package.json ├── pnpm-lock.yaml ├── scripts ├── adb.mjs ├── build.mjs ├── serve.mjs └── util.mjs ├── shims ├── asyncIteratorSymbol.js ├── depsModule.ts ├── emptyModule.ts ├── jsxRuntime.ts └── promiseAllSettled.js ├── src ├── assets │ └── icons │ │ └── revenge.png ├── core │ ├── commands │ │ ├── debug.ts │ │ ├── eval.ts │ │ └── plugins.ts │ ├── debug │ │ ├── patches │ │ │ └── patchErrorBoundary.tsx │ │ └── safeMode.ts │ ├── fixes.ts │ ├── i18n │ │ ├── default.json │ │ └── index.ts │ ├── plugins │ │ ├── badges │ │ │ └── index.tsx │ │ ├── index.ts │ │ └── quickinstall │ │ │ ├── forumPost.tsx │ │ │ ├── index.ts │ │ │ └── url.tsx │ ├── ui │ │ ├── components │ │ │ ├── AddonCard.tsx │ │ │ └── AddonPage.tsx │ │ ├── hooks │ │ │ └── useFS.ts │ │ ├── reporter │ │ │ ├── components │ │ │ │ ├── ErrorBoundaryScreen.tsx │ │ │ │ ├── ErrorCard.tsx │ │ │ │ ├── ErrorComponentStackCard.tsx │ │ │ │ ├── ErrorDetailsActionSheet.tsx │ │ │ │ └── ErrorStackCard.tsx │ │ │ └── utils │ │ │ │ ├── isStack.tsx │ │ │ │ ├── parseComponentStack.tsx │ │ │ │ └── parseErrorStack.ts │ │ └── settings │ │ │ ├── index.ts │ │ │ └── pages │ │ │ ├── Developer │ │ │ ├── AssetBrowser.tsx │ │ │ ├── AssetDisplay.tsx │ │ │ └── index.tsx │ │ │ ├── Fonts │ │ │ ├── FontCard.tsx │ │ │ ├── FontEditor.tsx │ │ │ └── index.tsx │ │ │ ├── General │ │ │ ├── About.tsx │ │ │ ├── Version.tsx │ │ │ └── index.tsx │ │ │ ├── PluginBrowser │ │ │ └── index.tsx │ │ │ ├── Plugins │ │ │ ├── components │ │ │ │ └── PluginCard.tsx │ │ │ ├── index.tsx │ │ │ ├── models │ │ │ │ ├── bunny.ts │ │ │ │ ├── index.ts │ │ │ │ └── vendetta.ts │ │ │ ├── sheets │ │ │ │ ├── PluginInfoActionSheet.tsx │ │ │ │ ├── TitleComponent.tsx │ │ │ │ ├── VdPluginInfoActionSheet.tsx │ │ │ │ └── common.ts │ │ │ └── usePluginCardStyles.ts │ │ │ └── Themes │ │ │ ├── ThemeCard.tsx │ │ │ └── index.tsx │ └── vendetta │ │ ├── Emitter.ts │ │ ├── alerts.ts │ │ ├── api.tsx │ │ ├── plugins.ts │ │ └── storage.ts ├── entry.ts ├── global.d.ts ├── index.ts ├── lib │ ├── addons │ │ ├── fonts │ │ │ └── index.ts │ │ ├── plugins │ │ │ ├── api.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── themes │ │ │ ├── colors │ │ │ │ ├── index.ts │ │ │ │ ├── parser.ts │ │ │ │ ├── patches │ │ │ │ │ ├── background.tsx │ │ │ │ │ ├── resolver.ts │ │ │ │ │ └── storage.ts │ │ │ │ ├── preferences.ts │ │ │ │ ├── types.ts │ │ │ │ └── updater.ts │ │ │ └── index.ts │ │ └── types.ts │ ├── api │ │ ├── assets │ │ │ ├── index.ts │ │ │ └── patches.ts │ │ ├── commands │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── debug.ts │ │ ├── flux │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── native │ │ │ ├── fs.ts │ │ │ ├── index.ts │ │ │ ├── loader.ts │ │ │ └── modules │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ ├── patcher.ts │ │ ├── react │ │ │ ├── index.ts │ │ │ └── jsx.ts │ │ ├── settings.ts │ │ └── storage │ │ │ └── index.ts │ ├── index.ts │ ├── ui │ │ ├── alerts.ts │ │ ├── color.ts │ │ ├── components │ │ │ ├── Codeblock.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── InputAlert.tsx │ │ │ ├── Search.tsx │ │ │ ├── Summary.tsx │ │ │ ├── index.ts │ │ │ └── wrappers │ │ │ │ ├── AlertModal.tsx │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── settings │ │ │ ├── index.tsx │ │ │ └── patches │ │ │ │ ├── shared.tsx │ │ │ │ └── tabs.tsx │ │ ├── sheets.ts │ │ ├── styles.ts │ │ ├── toasts.ts │ │ └── types.ts │ └── utils │ │ ├── constants.ts │ │ ├── cyrb64.ts │ │ ├── findInReactTree.ts │ │ ├── findInTree.ts │ │ ├── hookDefineProperty.ts │ │ ├── index.ts │ │ ├── invariant.ts │ │ ├── isValidHttpUrl.ts │ │ ├── lazy.ts │ │ ├── logger.ts │ │ ├── safeFetch.ts │ │ └── types.ts ├── metro │ ├── common │ │ ├── components.ts │ │ ├── index.ts │ │ ├── stores.ts │ │ └── types │ │ │ ├── components.ts │ │ │ └── flux.ts │ ├── factories.ts │ ├── filters.ts │ ├── finders.ts │ ├── index.ts │ ├── internals │ │ ├── caches.ts │ │ ├── enums.ts │ │ └── modules.ts │ ├── lazy.ts │ ├── polyfills │ │ └── redesign.ts │ ├── types.ts │ └── wrappers.ts └── modules.d.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report 2 | description: Report a bug or an issue. 3 | title: 'bug: ' 4 | labels: ['Bug report'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | # Revenge bug report 10 | 11 | Before creating a new bug report, please keep the following in mind: 12 | 13 | - **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/revenge-mod/revenge-bundle/issues?q=label%3A%22Bug+report%22). 14 | - **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/revenge-mod/revenge-bundle/blob/main/CONTRIBUTING.md). 15 | - **Do not use the issue page for support**: If you need help or have questions, join us on [Discord](https://discord.gg/ddcQf3s2Uq). 16 | - type: textarea 17 | attributes: 18 | label: Bug description 19 | description: | 20 | - Describe your bug in detail 21 | - Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...) 22 | - Add images and videos if possible 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Stack trace 28 | description: If this bug causes a JS crash, please paste the stack trace here. 29 | render: shell 30 | - type: textarea 31 | attributes: 32 | label: Component stack trace 33 | description: If this bug causes a JS crash, please paste the component stack trace here. 34 | render: shell 35 | - type: textarea 36 | attributes: 37 | label: Native crash trace 38 | description: If this bug causes a native crash, please paste the crash trace here. On Android, this can be accessed by doing `logcat | grep AndroidRuntime`. 39 | render: shell 40 | - type: textarea 41 | attributes: 42 | label: Solution 43 | description: If applicable, add a possible solution to the bug. 44 | - type: textarea 45 | attributes: 46 | label: Additional context 47 | description: Add additional context here. 48 | - type: checkboxes 49 | id: acknowledgements 50 | attributes: 51 | label: Acknowledgements 52 | description: Your bug report will be closed if you don't follow the checklist below. 53 | options: 54 | - label: I have checked all open and closed bug reports and this is not a duplicate. 55 | required: true 56 | - label: I have chosen an appropriate title. 57 | required: true 58 | - label: All requested information has been provided properly. 59 | required: true 60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🗨 Discussions 4 | url: https://discord.gg/ddcQf3s2Uq 5 | about: Have something unspecific to Revenge in mind? Join us on Discord! 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: ⭐ Feature request 2 | description: Create a detailed request for a new feature. 3 | title: 'feat: ' 4 | labels: ['Feature request'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | # Revenge feature request 10 | 11 | Before creating a new feature request, please keep the following in mind: 12 | 13 | - **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/revenge-mod/revenge-bundle/issues?q=label%3A%22Feature+request%22). 14 | - **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/revenge-mod/revenge-bundle/blob/main/CONTRIBUTING.md). 15 | - **Do not use the issue page for support**: If you need help or have questions, join us on [Discord](https://discord.gg/ddcQf3s2Uq). 16 | - type: textarea 17 | attributes: 18 | label: Feature description 19 | description: | 20 | - Describe your feature in detail 21 | - Add images, videos, links, examples, references, etc. if possible 22 | - type: textarea 23 | attributes: 24 | label: Motivation 25 | description: | 26 | A strong motivation is necessary for a feature request to be considered. 27 | 28 | - Why should this feature be implemented? 29 | - What is the explicit use case? 30 | - What are the benefits? 31 | - What makes this feature important? 32 | validations: 33 | required: true 34 | - type: checkboxes 35 | id: acknowledgements 36 | attributes: 37 | label: Acknowledgements 38 | description: Your feature request will be closed if you don't follow the checklist below. 39 | options: 40 | - label: I have checked all open and closed feature requests and this is not a duplicate 41 | required: true 42 | - label: I have chosen an appropriate title. 43 | required: true 44 | - label: All requested information has been provided properly. 45 | required: true 46 | -------------------------------------------------------------------------------- /.github/config.yml: -------------------------------------------------------------------------------- 1 | firstPRMergeComment: > 2 | Thank you for contributing to Revenge. Join us on [Discord](https://discord.gg/ddcQf3s2Uq) to receive a role for your contribution. -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [main, dev] 5 | 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Install pnpm 18 | uses: pnpm/action-setup@v4 19 | with: 20 | version: latest 21 | 22 | - name: Install Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: 22 26 | cache: pnpm 27 | 28 | - name: Install dependencies 29 | run: pnpm install --frozen-lockfile 30 | 31 | - name: Release 32 | run: pnpx semantic-release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | RELEASE_BRANCH: ${{ github.ref_name }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | package-lock.json 4 | yarn.lock 5 | .DS_Store 6 | lib-dist/ 7 | /packages/types -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | branches: [ 3 | "main", 4 | { 5 | name: "dev", 6 | prerelease: true, 7 | }, 8 | ], 9 | plugins: [ 10 | [ 11 | "@semantic-release/commit-analyzer", 12 | { 13 | releaseRules: [ 14 | { type: "build", scope: "Needs bump", release: "patch" }, 15 | ], 16 | }, 17 | ], 18 | "@semantic-release/release-notes-generator", 19 | "@semantic-release/changelog", 20 | [ 21 | "@semantic-release/git", 22 | { 23 | assets: ["README.md", "CHANGELOG.md"], 24 | message: 25 | "chore: Release ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}", 26 | }, 27 | ], 28 | [ 29 | "@semantic-release/exec", 30 | { 31 | prepareCmd: `pnpm build --release-branch ${process.env.RELEASE_BRANCH} --build-minify`, 32 | }, 33 | ], 34 | [ 35 | "@semantic-release/github", 36 | { 37 | assets: [ 38 | { 39 | path: "dist/*.js", 40 | }, 41 | ], 42 | successComment: false, 43 | }, 44 | ], 45 | [ 46 | "@saithodev/semantic-release-backmerge", 47 | { 48 | backmergeBranches: [{ from: "main", to: "dev" }], 49 | clearWorkspace: true, 50 | }, 51 | ], 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "[typescript]": { 6 | "editor.defaultFormatter": "vscode.typescript-language-features" 7 | }, 8 | "[typescriptreact]": { 9 | "editor.defaultFormatter": "vscode.typescript-language-features" 10 | }, 11 | "[json]": { 12 | "editor.formatOnSave": false 13 | }, 14 | "javascript.format.semicolons": "insert", 15 | "typescript.format.semicolons": "insert", 16 | "typescript.preferences.quoteStyle": "double", 17 | "javascript.preferences.quoteStyle": "double" 18 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 👋 Contribution guidelines 2 | 3 | This document describes how to contribute to Revenge bundle. 4 | 5 | ## 📖 Resources to help you get started 6 | 7 | * [Issues](https://github.com/revenge-mod/revenge-bundle/issues) are where we keep track of bugs and feature requests 8 | 9 | ## 🙏 Submitting a feature request 10 | 11 | Features can be requested by opening an issue using the 12 | [Feature request issue template](https://github.com/revenge-mod/revenge-bundle/issues/new?assignees=&labels=Feature+request&projects=&template=feature_request.yml&title=feat%3A+). 13 | 14 | > **Note** 15 | > Requests can be accepted or rejected at the discretion of maintainers of Revenge bundle. 16 | > Good motivation has to be provided for a request to be accepted. 17 | 18 | ## 🐞 Submitting a bug report 19 | 20 | If you encounter a bug while using Revenge bundle, open an issue using the 21 | [Bug report issue template](https://github.com/revenge-mod/revenge-bundle/issues/new?assignees=&labels=Bug+report&projects=&template=bug_report.yml&title=bug%3A+). 22 | 23 | ## 📝 How to contribute 24 | 25 | 1. Before contributing, it is recommended to open an issue to discuss your change 26 | with the maintainers of Revenge bundle. This will help you determine whether your change is acceptable 27 | and whether it is worth your time to implement it 28 | 2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev` 29 | 3. Commit your changes 30 | 4. Submit a pull request to the `dev` branch of the repository and reference issues 31 | that your pull request closes in the description of your pull request 32 | 5. Our team will review your pull request and provide feedback. Once your pull request is approved, 33 | it will be merged into the `dev` branch and will be included in the next release of Revenge bundle 34 | 35 | ❤️ Thank you for considering contributing to Revenge bundle 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Team Vendetta 4 | Copyright (c) 2024, pylixonly 5 | Copyright (c) 2024, Team Revenge 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 8 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 |        20 | 21 | 22 | 23 | 24 | 25 |        26 | 27 | 28 | 29 | 30 | 31 |        32 | 33 | 34 |

35 | 36 | # 👊 Revenge 37 | 38 | ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/revenge-mod/revenge-bundle/release.yml) 39 | [![3-Clause BSD License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) 40 | 41 | A client modification for Discord mobile, a continuation of [Bunny](https://github.com/pyoncord/Bunny). 42 | 43 | ## 💪 Features 44 | 45 | - **🔌 Plugins**: Extend Discord with custom features 46 | - **🎨 Themes & Fonts**: Customize Discord's appearance to your liking 47 | - **🧪 Experiments**: Try out Discord's new features before they're rolled out 48 | 49 | ## ⬇️ Download 50 | 51 | This repository releases JavaScript bundles for loaders to execute. These are the official Revenge loaders: 52 | 53 | ### Android 54 | 55 | - **🩹 Root with Xposed**: [RevengeXposed](https://github.com/revenge-mod/revenge-xposed/releases/latest) 56 | - **📵 Non-root**: [Revenge Manager](https://github.com/revenge-mod/revenge-manager/releases/latest) 57 | 58 | ### iOS 59 | 60 | - [**RevengeTweak**](https://github.com/revenge-mod/revenge-tweak): Prebuilt rootful and rootless `.deb` files or the prepatched `.ipa` 61 | 62 | ## 📚 Everything else 63 | 64 | ### 📙 Contributing 65 | 66 | Thank you for considering contributing to Revenge. 67 | You can find the contribution guidelines [here](CONTRIBUTING.md). 68 | 69 | ### 🛠️ Building 70 | 71 | To build Revenge bundle, you can follow the [documentation](/docs). 72 | 73 | ### 📃 Documentation 74 | 75 | You can find the documentation of Revenge bundle [here](/docs). 76 | 77 | ## 📜 Licence 78 | 79 | Revenge bundle is licensed under the 3-Clause BSD license. Please see the [license file](LICENSE) for more information. 80 | -------------------------------------------------------------------------------- /assets/logo/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /assets/logo/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/2_building.md: -------------------------------------------------------------------------------- 1 | ## Building 2 | 1. Install a Revenge loader with config support (any mentioned in the [Installing](#installing) section). 3 | 2. Go to **Settings** > **General** and enable **Developer Settings**. 4 | 3. Clone the repository 5 | 6 | ```sh 7 | git clone https://github.com/revenge-mod/revenge-bundle.git 8 | ``` 9 | 10 | 4. Install dependencies 11 | 12 | ``` 13 | pnpm i 14 | ``` 15 | 16 | 5. Build Revenge's code 17 | 18 | ``` 19 | pnpm build 20 | ``` 21 | 22 | 6. In the newly created `dist` directory, run a HTTP server. I recommend [http-server](https://www.npmjs.com/package/http-server). 23 | 7. Go to **Settings** > **Developer** enabled earlier. Enable `Load from custom URL` and input the IP address and port of the server (e.g. `http://192.168.1.236:4040/revenge.js`) in the new input box labeled `Revenge URL`. 24 | 8. Restart Discord. Upon reload, you should notice that your device will download Revenge's bundled code from your server, rather than GitHub. 25 | 9. Make your changes, rebuild, reload, go wild! 26 | 27 | Alternatively, you can directly *serve* the bundled code by running `pnpm serve`. `revenge.js` will be served on your local address under the port 4040. You will then insert `http://:4040/revenge.js` as a custom URL and reload. Whenever you restart your mobile client, the script will rebuild the bundle as your client fetches it. 28 | 29 | If the bundle keeps being cached and not updated, you can instead tap the **Settings** > **Developer** > **Clear JS bundle** option which will prompt you to reload. 30 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 👊 Revenge 2 | 3 | This documentation explains how to use [Revenge](https://github.com/revenge-mod/revenge-bundle). 4 | 5 | ## 📖 Table of contents 6 | 7 | TODO. 8 | 9 | ## ⏭️ Start here 10 | 11 | The next page will tell you about the prerequisites for using Revenge. 12 | 13 | Continue: [💼 Prerequisites](0_prerequisites.md) -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import simpleImportSort from "eslint-plugin-simple-import-sort"; 3 | import unusedImports from "eslint-plugin-unused-imports"; 4 | import importAlias from "eslint-plugin-import-alias"; 5 | import tsParser from "@typescript-eslint/parser"; 6 | 7 | export default [{ 8 | ignores: ["**/dist", "**/browser"], 9 | }, { 10 | plugins: { 11 | "@typescript-eslint": typescriptEslint, 12 | "simple-import-sort": simpleImportSort, 13 | "unused-imports": unusedImports, 14 | "import-alias": importAlias, 15 | }, 16 | 17 | languageOptions: { 18 | parser: tsParser, 19 | }, 20 | 21 | rules: { 22 | "no-restricted-syntax": ["error", { 23 | selector: "AwaitExpression:not(:function *)", 24 | message: "Hermes does not support top-level await, and SWC cannot transform it.", 25 | }], 26 | 27 | quotes: ["error", "double", { 28 | avoidEscape: true, 29 | }], 30 | 31 | "jsx-quotes": ["error", "prefer-double"], 32 | "no-mixed-spaces-and-tabs": "error", 33 | 34 | indent: ["error", 4, { 35 | SwitchCase: 1, 36 | }], 37 | 38 | "arrow-parens": ["error", "as-needed"], 39 | "eol-last": ["error", "always"], 40 | "func-call-spacing": ["error", "never"], 41 | "no-multi-spaces": "error", 42 | "no-trailing-spaces": "error", 43 | "no-whitespace-before-property": "error", 44 | semi: ["error", "always"], 45 | "semi-style": ["error", "last"], 46 | "space-in-parens": ["error", "never"], 47 | "block-spacing": ["error", "always"], 48 | "object-curly-spacing": ["error", "always"], 49 | 50 | eqeqeq: ["error", "always", { 51 | null: "ignore", 52 | }], 53 | 54 | "spaced-comment": ["error", "always", { 55 | markers: ["!"], 56 | }], 57 | 58 | yoda: "error", 59 | 60 | "prefer-destructuring": ["error", { 61 | object: true, 62 | array: false, 63 | }], 64 | 65 | "operator-assignment": ["error", "always"], 66 | "no-useless-computed-key": "error", 67 | 68 | "no-unneeded-ternary": ["error", { 69 | defaultAssignment: false, 70 | }], 71 | 72 | "no-invalid-regexp": "error", 73 | 74 | "no-constant-condition": ["error", { 75 | checkLoops: false, 76 | }], 77 | 78 | "no-duplicate-imports": "error", 79 | "no-extra-semi": "error", 80 | "dot-notation": "error", 81 | "no-useless-escape": ["error"], 82 | "no-fallthrough": "error", 83 | "for-direction": "error", 84 | "no-async-promise-executor": "error", 85 | "no-cond-assign": "error", 86 | "no-dupe-else-if": "error", 87 | "no-duplicate-case": "error", 88 | "no-irregular-whitespace": "error", 89 | "no-loss-of-precision": "error", 90 | "no-misleading-character-class": "error", 91 | "no-prototype-builtins": "error", 92 | "no-regex-spaces": "error", 93 | "no-shadow-restricted-names": "error", 94 | "no-unexpected-multiline": "error", 95 | "no-unsafe-optional-chaining": "error", 96 | "no-useless-backreference": "error", 97 | "use-isnan": "error", 98 | "prefer-const": "error", 99 | "prefer-spread": "error", 100 | "simple-import-sort/imports": "error", 101 | "simple-import-sort/exports": "error", 102 | "unused-imports/no-unused-imports": "error", 103 | 104 | "import-alias/import-alias": ["error", { 105 | relativeDepth: 0, 106 | 107 | aliases: [{ 108 | alias: "@metro", 109 | matcher: "^src/lib/metro", 110 | }, { 111 | alias: "@core", 112 | matcher: "^src/core", 113 | }, { 114 | alias: "@ui", 115 | matcher: "^src/lib/ui", 116 | }, { 117 | alias: "@types", 118 | matcher: "^src/lib/utils/types.ts", 119 | }, { 120 | alias: "@lib", 121 | matcher: "^src/lib", 122 | }], 123 | }], 124 | }, 125 | }]; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@revenge-mod/revenge", 3 | "exports": { 4 | "types": "./dist/types/index.d.ts" 5 | }, 6 | "author": { 7 | "name": "Revenge Team", 8 | "url": "https://github.com/revenge-mod" 9 | }, 10 | "scripts": { 11 | "build": "node scripts/build.mjs", 12 | "serve": "node scripts/serve.mjs", 13 | "serve:adb": "pnpm run serve --adb", 14 | "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx" 15 | }, 16 | "license": "BSD-3-Clause", 17 | "devDependencies": { 18 | "@saithodev/semantic-release-backmerge": "^4.0.1", 19 | "@semantic-release/changelog": "^6.0.3", 20 | "@semantic-release/exec": "^6.0.3", 21 | "@semantic-release/git": "^10.0.1", 22 | "semantic-release": "^24.1.3", 23 | "@swc/core": "^1.7.0", 24 | "@swc/helpers": "^0.5.12", 25 | "@types/node": "^20.14.11", 26 | "@types/yargs-parser": "^21.0.3", 27 | "@typescript-eslint/eslint-plugin": "^7.16.1", 28 | "@typescript-eslint/parser": "^7.16.1", 29 | "@typescript-eslint/typescript-estree": "^7.16.1", 30 | "chalk": "^5.3.0", 31 | "esbuild": "^0.20.2", 32 | "esbuild-plugin-globals": "^0.2.0", 33 | "eslint": "^8.57.0", 34 | "eslint-plugin-import-alias": "^1.2.0", 35 | "eslint-plugin-react": "^7.35.0", 36 | "eslint-plugin-simple-import-sort": "^12.1.1", 37 | "eslint-plugin-unused-imports": "^3.2.0", 38 | "typescript": "^5.5.3", 39 | "yargs-parser": "^21.1.1" 40 | }, 41 | "dependencies": { 42 | "@gullerya/object-observer": "^6.1.3", 43 | "@shopify/react-native-skia": "^1.3.8", 44 | "@tanstack/react-query": "^5.51.16", 45 | "@types/chroma-js": "~2.4.4", 46 | "@types/lodash": "~4.17.7", 47 | "@types/react": "18.2.60", 48 | "@types/react-native": "0.72.3", 49 | "es-toolkit": "^1.13.1", 50 | "fuzzysort": "^3.0.2", 51 | "intl-messageformat": "^10.5.14", 52 | "moment": "2.22.2", 53 | "react-native-reanimated": "^3.6.2", 54 | "spitroast": "^1.4.4", 55 | "type-fest": "^4.22.1" 56 | }, 57 | "pnpm": { 58 | "peerDependencyRules": { 59 | "ignoreMissing": [ 60 | "react", 61 | "react-native" 62 | ] 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /scripts/adb.mjs: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { execSync } from "child_process"; 3 | 4 | const packageName = process.env.DISCORD_PACKAGE_NAME ?? "com.discord"; 5 | 6 | export function getPackageName() { 7 | return packageName; 8 | } 9 | 10 | export function isADBAvailableAndAppInstalled() { 11 | try { 12 | const out = execSync(`adb shell pm list packages ${packageName}`); 13 | return out.toString().trimEnd() === `package:${packageName}`; 14 | } catch { 15 | return false; 16 | } 17 | } 18 | export async function restartAppFromADB(reversePort) { 19 | if (typeof reversePort === "number") { 20 | await execSync(`adb reverse tcp:${reversePort} tcp:${reversePort}`); 21 | } 22 | 23 | await forceStopAppFromADB(); 24 | await execSync(`adb shell am start ${packageName}/com.discord.main.MainActivity`); 25 | } 26 | 27 | export async function forceStopAppFromADB() { 28 | await execSync(`adb shell am force-stop ${packageName}`); 29 | } 30 | -------------------------------------------------------------------------------- /scripts/serve.mjs: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import chalk from "chalk"; 3 | import { readFile } from "fs/promises"; 4 | import http from "http"; 5 | import os from "os"; 6 | import readline from "readline"; 7 | import url from "url"; 8 | import yargs from "yargs-parser"; 9 | 10 | import { forceStopAppFromADB, getPackageName, isADBAvailableAndAppInstalled, restartAppFromADB } from "./adb.mjs"; 11 | import { buildBundle } from "./build.mjs"; 12 | import { printBuildSuccess } from "./util.mjs"; 13 | 14 | const args = yargs(process.argv.slice(2)); 15 | 16 | export function serve(options) { 17 | // @ts-ignore 18 | const server = http.createServer(async (req, res) => { 19 | const { pathname } = url.parse(req.url || "", true); 20 | if (pathname?.endsWith(".js")) { 21 | try { 22 | const { config, context, timeTook } = await buildBundle(); 23 | 24 | printBuildSuccess( 25 | context.hash, 26 | args.production, 27 | timeTook 28 | ); 29 | 30 | res.writeHead(200, { "Content-Type": "application/javascript" }); 31 | res.end(await readFile(config.outfile, "utf-8")); 32 | } catch { 33 | res.writeHead(500); 34 | res.end(); 35 | } 36 | } else { 37 | res.writeHead(404); 38 | res.end(); 39 | } 40 | }, options); 41 | 42 | server.listen(args.port ?? 4040); 43 | 44 | console.info(chalk.bold.yellowBright("Serving Revenge bundle, available on:")); 45 | 46 | const netInterfaces = os.networkInterfaces(); 47 | for (const netinterfaces of Object.values(netInterfaces)) { 48 | for (const details of netinterfaces || []) { 49 | if (details.family !== "IPv4") continue; 50 | const port = chalk.green(server.address()?.port.toString()); 51 | console.info(` http://${details.address}:${port}/bundle.js`); 52 | } 53 | } 54 | 55 | return server; 56 | } 57 | 58 | const server = serve(); 59 | 60 | console.log("\nPress Q key or Ctrl+C to exit."); 61 | 62 | if (args.adb && isADBAvailableAndAppInstalled()) { 63 | const packageName = getPackageName(); 64 | 65 | console.log(`Press R key to reload Discord ${chalk.bold.blue(`(${packageName})`)}.`); 66 | console.log(`Press S key to force stop Discord ${chalk.bold.blue(`(${packageName})`)}.`); 67 | 68 | readline.emitKeypressEvents(process.stdin); 69 | 70 | if (process.stdin.isTTY) { 71 | process.stdin.setRawMode(true); 72 | } 73 | 74 | process.stdin.on("keypress", (ch, key) => { 75 | if (!key) return; 76 | 77 | if (key.name === "q" || key.ctrl && key.name === "c") { 78 | process.exit(0); 79 | } 80 | 81 | if (key.name === "r") { 82 | console.info(chalk.yellow(`${chalk.bold("↻ Reloading")} ${packageName}`)); 83 | restartAppFromADB(server.address().port) 84 | .then(() => console.info(chalk.greenBright(`${chalk.bold("✔ Executed")} reload command`))) 85 | .catch(e => console.error(e)); 86 | } 87 | 88 | if (key.name === "s") { 89 | console.info(chalk.yellow(`${chalk.bold("⎊ Force stopping")} ${packageName}`)); 90 | forceStopAppFromADB() 91 | .then(() => console.info(chalk.greenBright(`${chalk.bold("✔ Executed")} force stop command`))) 92 | .catch(e => console.error(e)); 93 | } 94 | }); 95 | } else if (args.adb) { 96 | console.warn("ADB option enabled but failed to connect to device!"); 97 | } 98 | -------------------------------------------------------------------------------- /scripts/util.mjs: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import chalk from "chalk"; 3 | 4 | export function printBuildSuccess( 5 | hash, 6 | branch, 7 | timeTook, 8 | minified 9 | ) { 10 | console.info([ 11 | chalk.bold.greenBright("✔ Built bundle" + (minified ? " (minified)" : "")), 12 | hash && chalk.bold.blueBright(`(${hash})`), 13 | !branch && chalk.bold.cyanBright("(local)"), 14 | timeTook && chalk.gray(`in ${timeTook.toFixed(3)}ms`) 15 | ].filter(Boolean).join(" ")); 16 | } 17 | -------------------------------------------------------------------------------- /shims/asyncIteratorSymbol.js: -------------------------------------------------------------------------------- 1 | // babel/swc async generator implementation for some reason does not include assigning Symbol.asyncIterator (and won't work out of the box???) 2 | const asyncIteratorSymbol = Symbol("Symbol.asyncIterator"); 3 | 4 | // your editor may yell at you for invalid syntax here, but this is a valid JS syntax! 5 | export { asyncIteratorSymbol as "Symbol.asyncIterator" }; -------------------------------------------------------------------------------- /shims/depsModule.ts: -------------------------------------------------------------------------------- 1 | import { findByPropsLazy } from "@metro/wrappers"; 2 | 3 | module.exports = { 4 | "react": findByPropsLazy("createElement"), 5 | "react-native": findByPropsLazy("AppRegistry"), 6 | "util": findByPropsLazy("inspect", "isNullOrUndefined"), 7 | "moment": findByPropsLazy("isMoment"), 8 | "chroma-js": findByPropsLazy("brewer"), 9 | "lodash": findByPropsLazy("forEachRight"), 10 | "@shopify/react-native-skia": findByPropsLazy("useFont") 11 | }; 12 | -------------------------------------------------------------------------------- /shims/emptyModule.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /shims/jsxRuntime.ts: -------------------------------------------------------------------------------- 1 | import { getProxyFactory } from "@lib/utils/lazy"; 2 | import { findByPropsLazy } from "@metro/wrappers"; 3 | 4 | const jsxRuntime = findByPropsLazy("jsx", "jsxs", "Fragment"); 5 | 6 | function unproxyFirstArg(args: T[]) { 7 | if (!args[0]) { 8 | throw new Error("The first argument (Component) is falsy. Ensure that you are passing a valid component."); 9 | } 10 | 11 | const factory = getProxyFactory(args[0]); 12 | if (factory) args[0] = factory(); 13 | return args; 14 | } 15 | 16 | export const Fragment = Symbol.for("react.fragment"); 17 | export const jsx = (...args: any[]) => jsxRuntime.jsx(...unproxyFirstArg(args)); 18 | export const jsxs = (...args: any[]) => jsxRuntime.jsxs(...unproxyFirstArg(args)); 19 | -------------------------------------------------------------------------------- /shims/promiseAllSettled.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | const allSettledFulfill = value => ({ status: "fulfilled", value }); 4 | const allSettledReject = reason => ({ status: "rejected", reason }); 5 | const mapAllSettled = item => Promise.resolve(item).then(allSettledFulfill, allSettledReject); 6 | 7 | const allSettled = Promise.allSettled ??= iterator => { 8 | return Promise.all(Array.from(iterator).map(mapAllSettled)); 9 | }; 10 | 11 | // Your editor may yell at you for this, but this is alright! It's a valid JS syntax 12 | export { allSettled as "Promise.allSettled" }; 13 | -------------------------------------------------------------------------------- /src/assets/icons/revenge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revenge-mod/revenge-bundle/c263de39d0f8786d2a9225621e2db667f0141edb/src/assets/icons/revenge.png -------------------------------------------------------------------------------- /src/core/commands/debug.ts: -------------------------------------------------------------------------------- 1 | import { Strings } from "@core/i18n"; 2 | import { ApplicationCommand, ApplicationCommandOptionType } from "@lib/api/commands/types"; 3 | import { getDebugInfo } from "@lib/api/debug"; 4 | import { messageUtil } from "@metro/common"; 5 | 6 | export default () => { 7 | name: "debug", 8 | description: Strings.COMMAND_DEBUG_DESC, 9 | options: [ 10 | { 11 | name: "ephemeral", 12 | type: ApplicationCommandOptionType.BOOLEAN, 13 | description: Strings.COMMAND_DEBUG_OPT_EPHEMERALLY, 14 | } 15 | ], 16 | execute([ephemeral], ctx) { 17 | const info = getDebugInfo(); 18 | const content = [ 19 | "**Revenge Debug Info**", 20 | `> Revenge: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`, 21 | `> Discord: ${info.discord.version} (${info.discord.build})`, 22 | `> React: ${info.react.version} (RN ${info.react.nativeVersion})`, 23 | `> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`, 24 | `> System: ${info.os.name} ${info.os.version} ${info.os.sdk ? `(SDK ${info.os.sdk})` : ""}`.trimEnd(), 25 | `> Device: ${info.device.model} (${info.device.codename})`, 26 | ].join("\n"); 27 | 28 | if (ephemeral?.value) { 29 | messageUtil.sendBotMessage(ctx.channel.id, content); 30 | } else { 31 | messageUtil.sendMessage(ctx.channel.id, { content }); 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/core/commands/eval.ts: -------------------------------------------------------------------------------- 1 | import { Strings } from "@core/i18n"; 2 | import { ApplicationCommand, ApplicationCommandOptionType } from "@lib/api/commands/types"; 3 | import { settings } from "@lib/api/settings"; 4 | import { messageUtil } from "@metro/common"; 5 | import { findByPropsLazy } from "@metro/wrappers"; 6 | 7 | const util = findByPropsLazy("inspect"); 8 | const AsyncFunction = (async () => void 0).constructor; 9 | 10 | const ZERO_WIDTH_SPACE_CHARACTER = "\u200B"; 11 | 12 | function wrapInJSCodeblock(resString: string) { 13 | return "```js\n" + resString.replaceAll("`", "`" + ZERO_WIDTH_SPACE_CHARACTER) + "\n```"; 14 | } 15 | 16 | export default () => { 17 | name: "eval", 18 | description: Strings.COMMAND_EVAL_DESC, 19 | shouldHide: () => settings.enableEvalCommand === true, 20 | options: [ 21 | { 22 | name: "code", 23 | type: ApplicationCommandOptionType.STRING, 24 | description: Strings.COMMAND_EVAL_OPT_CODE, 25 | required: true 26 | }, 27 | { 28 | name: "async", 29 | type: ApplicationCommandOptionType.BOOLEAN, 30 | description: Strings.COMMAND_EVAL_OPT_ASYNC, 31 | } 32 | ], 33 | async execute([code, async], ctx) { 34 | try { 35 | const res = util.inspect(async?.value ? await AsyncFunction(code.value)() : eval?.(code.value)); 36 | const trimmedRes = res.length > 2000 ? res.slice(0, 2000) + "..." : res; 37 | 38 | messageUtil.sendBotMessage(ctx.channel.id, wrapInJSCodeblock(trimmedRes)); 39 | } catch (err: any) { 40 | messageUtil.sendBotMessage(ctx.channel.id, wrapInJSCodeblock(err?.stack ?? err)); 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/core/commands/plugins.ts: -------------------------------------------------------------------------------- 1 | import { Strings } from "@core/i18n"; 2 | import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins"; 3 | import { ApplicationCommand, ApplicationCommandOptionType } from "@lib/api/commands/types"; 4 | import { messageUtil } from "@metro/common"; 5 | 6 | export default () => { 7 | name: "plugins", 8 | description: Strings.COMMAND_PLUGINS_DESC, 9 | options: [ 10 | { 11 | name: "ephemeral", 12 | displayName: "ephemeral", 13 | type: ApplicationCommandOptionType.BOOLEAN, 14 | description: Strings.COMMAND_DEBUG_OPT_EPHEMERALLY, 15 | } 16 | ], 17 | execute([ephemeral], ctx) { 18 | const plugins = Object.values(VdPluginManager.plugins).filter(Boolean) as unknown as VendettaPlugin[]; 19 | plugins.sort((a, b) => a.manifest.name.localeCompare(b.manifest.name)); 20 | 21 | const enabled = plugins.filter(p => p.enabled).map(p => p.manifest.name); 22 | const disabled = plugins.filter(p => !p.enabled).map(p => p.manifest.name); 23 | 24 | const content = [ 25 | `**Installed Plugins (${plugins.length}):**`, 26 | ...(enabled.length > 0 ? [ 27 | `Enabled (${enabled.length}):`, 28 | "> " + enabled.join(", "), 29 | ] : []), 30 | ...(disabled.length > 0 ? [ 31 | `Disabled (${disabled.length}):`, 32 | "> " + disabled.join(", "), 33 | ] : []), 34 | ].join("\n"); 35 | 36 | if (ephemeral?.value) { 37 | messageUtil.sendBotMessage(ctx.channel.id, content); 38 | } else { 39 | messageUtil.sendMessage(ctx.channel.id, { content }); 40 | } 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /src/core/debug/patches/patchErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import ErrorBoundaryScreen from "@core/ui/reporter/components/ErrorBoundaryScreen"; 2 | import { after } from "@lib/api/patcher"; 3 | import { _lazyContextSymbol } from "@metro/lazy"; 4 | import { LazyModuleContext } from "@metro/types"; 5 | import { findByNameLazy } from "@metro/wrappers"; 6 | 7 | function getErrorBoundaryContext() { 8 | const ctxt: LazyModuleContext = findByNameLazy("ErrorBoundary")[_lazyContextSymbol]; 9 | return new Promise(resolve => ctxt.getExports(exp => resolve(exp.prototype))); 10 | } 11 | 12 | export default function patchErrorBoundary() { 13 | return after.await("render", getErrorBoundaryContext(), function (this: any) { 14 | if (!this.state.error) return; 15 | 16 | return this.setState({ info: null, error: null })} 19 | />; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/core/debug/safeMode.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentTheme, writeThemeToNative } from "@lib/addons/themes"; 2 | import { BundleUpdaterManager } from "@lib/api/native/modules"; 3 | import { settings } from "@lib/api/settings"; 4 | 5 | export function isSafeMode() { 6 | return settings.safeMode?.enabled === true; 7 | } 8 | 9 | export async function toggleSafeMode({ 10 | to = !isSafeMode(), 11 | reload = true 12 | } = {}) { 13 | const enabled = (settings.safeMode ??= { enabled: to }).enabled = to; 14 | const currentColor = getCurrentTheme(); 15 | await writeThemeToNative(enabled ? {} : currentColor?.data ?? {}); 16 | if (reload) setTimeout(() => BundleUpdaterManager.reload(), 500); 17 | } 18 | -------------------------------------------------------------------------------- /src/core/fixes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Why do we need this exactly? It's simple, to fight the side effects caused by when you 3 | * initialize modules not in the right time or when you're not supposed to. 4 | * Back then, we had major bugs like the mixing of light and dark themes and AMOLED option getting ignored. 5 | * Nowadays, there are only three which I can spot: Hindi timestamps, crashing on some native components and slower startup time. 6 | * - @pylixonly 7 | */ 8 | 9 | import { logger } from "@lib/utils/logger"; 10 | import { FluxDispatcher } from "@metro/common"; 11 | import moment from "moment"; 12 | 13 | function onDispatch({ locale }: { locale: string; }) { 14 | // Timestamps 15 | try { 16 | moment.locale(locale.toLowerCase()); 17 | } catch (e) { 18 | logger.error("Failed to fix timestamps...", e); 19 | } 20 | 21 | // We're done here! 22 | FluxDispatcher.unsubscribe("I18N_LOAD_SUCCESS", onDispatch); 23 | } 24 | 25 | export default () => { 26 | FluxDispatcher.subscribe("I18N_LOAD_SUCCESS", onDispatch); 27 | }; 28 | -------------------------------------------------------------------------------- /src/core/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { FluxDispatcher } from "@metro/common"; 2 | import { findByNameLazy } from "@metro/wrappers"; 3 | import { PrimitiveType } from "intl-messageformat"; 4 | 5 | import langDefault from "./default.json"; 6 | 7 | const IntlMessageFormat = findByNameLazy("MessageFormat") as typeof import("intl-messageformat").default; 8 | 9 | type I18nKey = keyof typeof langDefault; 10 | 11 | let _currentLocale: string | null = null; 12 | let _lastSetLocale: string | null = null; 13 | 14 | const _loadedLocale = new Set(); 15 | const _loadedStrings = {} as Record; 16 | 17 | export const Strings = new Proxy({}, { 18 | get: (_t, prop: keyof typeof langDefault) => { 19 | if (_currentLocale && _loadedStrings[_currentLocale]?.[prop]) { 20 | return _loadedStrings[_currentLocale]?.[prop]; 21 | } 22 | return langDefault[prop]; 23 | } 24 | }) as Record; 25 | 26 | export function initFetchI18nStrings() { 27 | const cb = ({ locale }: { locale: string; }) => { 28 | const languageMap = { 29 | "es-ES": "es", 30 | "es-419": "es_419", 31 | "zh-TW": "zh-Hant", 32 | "zh-CN": "zh-Hans", 33 | "pt-PT": "pt", 34 | "pt-BR": "pt_BR", 35 | "sv-SE": "sv" 36 | } as Record; 37 | 38 | const resolvedLocale = _lastSetLocale = languageMap[locale] ?? locale; 39 | 40 | if (resolvedLocale.startsWith("en-")) { 41 | _currentLocale = null; 42 | return; 43 | } 44 | 45 | if (!_loadedLocale.has(resolvedLocale)) { 46 | _loadedLocale.add(resolvedLocale); 47 | 48 | fetch(`https://raw.githubusercontent.com/pyoncord/i18n/main/resources/${resolvedLocale}/bunny.json`) 49 | .then(r => r.json()) 50 | .then(strings => _loadedStrings[resolvedLocale] = strings) 51 | .then(() => resolvedLocale === _lastSetLocale && (_currentLocale = resolvedLocale)) 52 | .catch(e => console.error(`An error occured while fetching strings for ${resolvedLocale}: ${e}`)); 53 | } else { 54 | _currentLocale = resolvedLocale; 55 | } 56 | }; 57 | 58 | FluxDispatcher.subscribe("I18N_LOAD_SUCCESS", cb); 59 | return () => FluxDispatcher.unsubscribe("I18N_LOAD_SUCCESS", cb); 60 | } 61 | 62 | type FormatStringRet = T extends PrimitiveType ? string : string | T | (string | T)[]; 63 | 64 | export function formatString(key: I18nKey, val: Record): FormatStringRet { 65 | const str = Strings[key]; 66 | // @ts-ignore 67 | return new IntlMessageFormat(str).format(val); 68 | } 69 | -------------------------------------------------------------------------------- /src/core/plugins/badges/index.tsx: -------------------------------------------------------------------------------- 1 | import { after } from "@lib/api/patcher"; 2 | import { onJsxCreate } from "@lib/api/react/jsx"; 3 | import { findByName } from "@metro"; 4 | import { useEffect, useState } from "react"; 5 | 6 | import { defineCorePlugin } from ".."; 7 | 8 | interface BunnyBadge { 9 | label: string; 10 | url: string; 11 | } 12 | 13 | const useBadgesModule = findByName("useBadges", false); 14 | 15 | export default defineCorePlugin({ 16 | manifest: { 17 | id: "bunny.badges", 18 | name: "Badges", 19 | version: "1.0.0", 20 | description: "Adds badges to user's profile", 21 | authors: [{ name: "pylixonly" }] 22 | }, 23 | start() { 24 | const propHolder = {} as Record; 25 | const badgeCache = {} as Record; 26 | 27 | onJsxCreate("RenderedBadge", (_, ret) => { 28 | if (ret.props.id.match(/bunny-\d+-\d+/)) { 29 | Object.assign(ret.props, propHolder[ret.props.id]); 30 | } 31 | }); 32 | 33 | after("default", useBadgesModule, ([user], r) => { 34 | const [badges, setBadges] = useState(user ? badgeCache[user.userId] ??= [] : []); 35 | 36 | useEffect(() => { 37 | if (user) { 38 | fetch(`https://raw.githubusercontent.com/pyoncord/badges/refs/heads/main/${user.userId}.json`) 39 | .then(r => r.json()) 40 | .then(badges => setBadges(badgeCache[user.userId] = badges)); 41 | } 42 | }, [user]); 43 | 44 | if (user) { 45 | badges.forEach((badges, i) => { 46 | propHolder[`bunny-${user.userId}-${i}`] = { 47 | source: { uri: badges.url }, 48 | id: `bunny-${i}`, 49 | label: badges.label 50 | }; 51 | 52 | r.push({ 53 | id: `bunny-${user.userId}-${i}`, 54 | description: badges.label, 55 | icon: "_", 56 | }); 57 | }); 58 | } 59 | }); 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /src/core/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import { PluginInstanceInternal } from "@lib/addons/plugins/types"; 2 | 3 | interface CorePlugin { 4 | default: PluginInstanceInternal; 5 | preenabled: boolean; 6 | } 7 | 8 | // Called from @lib/plugins 9 | export const getCorePlugins = (): Record => ({ 10 | "bunny.quickinstall": require("./quickinstall"), 11 | "bunny.badges": require("./badges") 12 | }); 13 | 14 | /** 15 | * @internal 16 | */ 17 | export function defineCorePlugin(instance: PluginInstanceInternal): PluginInstanceInternal { 18 | // @ts-expect-error 19 | instance[Symbol.for("bunny.core.plugin")] = true; 20 | return instance; 21 | } 22 | -------------------------------------------------------------------------------- /src/core/plugins/quickinstall/index.ts: -------------------------------------------------------------------------------- 1 | import { defineCorePlugin } from ".."; 2 | import patchForumPost from "./forumPost"; 3 | import patchUrl from "./url"; 4 | 5 | let patches = [] as (() => unknown)[]; 6 | 7 | export default defineCorePlugin({ 8 | manifest: { 9 | id: "bunny.quickinstall", 10 | name: "QuickInstall", 11 | version: "1.0.0", 12 | description: "Quickly install Vendetta plugins and themes", 13 | authors: [{ name: "Vendetta Team" }] 14 | }, 15 | start() { 16 | patches = [patchForumPost(), patchUrl()]; 17 | }, 18 | stop() { 19 | patches.forEach(p => p()); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /src/core/plugins/quickinstall/url.tsx: -------------------------------------------------------------------------------- 1 | import { formatString, Strings } from "@core/i18n"; 2 | import { showConfirmationAlert } from "@core/vendetta/alerts"; 3 | import { VdPluginManager } from "@core/vendetta/plugins"; 4 | import { installTheme } from "@lib/addons/themes"; 5 | import { findAssetId } from "@lib/api/assets"; 6 | import { isThemeSupported } from "@lib/api/native/loader"; 7 | import { after, instead } from "@lib/api/patcher"; 8 | import { VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID } from "@lib/utils/constants"; 9 | import { lazyDestructure } from "@lib/utils/lazy"; 10 | import { channels } from "@metro/common"; 11 | import { byMutableProp } from "@metro/filters"; 12 | import { findExports } from "@metro/finders"; 13 | import { findByProps, findByPropsLazy } from "@metro/wrappers"; 14 | import { showToast } from "@ui/toasts"; 15 | import { Linking } from "react-native"; 16 | 17 | const showSimpleActionSheet = findExports(byMutableProp("showSimpleActionSheet")); 18 | const handleClick = findByPropsLazy("handleClick"); 19 | const { getChannelId } = lazyDestructure(() => channels); 20 | const { getChannel } = lazyDestructure(() => findByProps("getChannel")); 21 | 22 | function typeFromUrl(url: string) { 23 | if (url.startsWith(VD_PROXY_PREFIX)) { 24 | return "plugin"; 25 | } else if (url.endsWith(".json") && isThemeSupported()) { 26 | return "theme"; 27 | } 28 | } 29 | 30 | function installWithToast(type: "plugin" | "theme", url: string) { 31 | (type === "plugin" ? VdPluginManager.installPlugin.bind(VdPluginManager) : installTheme)(url) 32 | .then(() => { 33 | showToast(Strings.SUCCESSFULLY_INSTALLED, findAssetId("Check")); 34 | }) 35 | .catch((e: Error) => { 36 | showToast(e.message, findAssetId("Small")); 37 | }); 38 | } 39 | 40 | export default () => { 41 | const patches = new Array(); 42 | 43 | patches.push( 44 | after("showSimpleActionSheet", showSimpleActionSheet, args => { 45 | if (args[0].key !== "LongPressUrl") return; 46 | const { 47 | header: { title: url }, 48 | options, 49 | } = args[0]; 50 | 51 | const urlType = typeFromUrl(url); 52 | if (!urlType) return; 53 | 54 | options.push({ 55 | label: Strings.INSTALL_ADDON, 56 | onPress: () => installWithToast(urlType, url), 57 | }); 58 | }) 59 | ); 60 | 61 | patches.push( 62 | instead("handleClick", handleClick, async function (this: any, args, orig) { 63 | const { href: url } = args[0]; 64 | 65 | const urlType = typeFromUrl(url); 66 | if (!urlType) return orig.apply(this, args); 67 | 68 | // Make clicking on theme links only work in #themes, should there be a theme proxy in the future, this can be removed. 69 | if (urlType === "theme" && getChannel(getChannelId())?.parent_id !== VD_THEMES_CHANNEL_ID) return orig.apply(this, args); 70 | 71 | showConfirmationAlert({ 72 | title: Strings.HOLD_UP, 73 | content: formatString("CONFIRMATION_LINK_IS_A_TYPE", { urlType }), 74 | onConfirm: () => installWithToast(urlType, url), 75 | confirmText: Strings.INSTALL, 76 | cancelText: Strings.CANCEL, 77 | secondaryConfirmText: Strings.OPEN_IN_BROWSER, 78 | onConfirmSecondary: () => Linking.openURL(url), 79 | }); 80 | }) 81 | ); 82 | 83 | return () => patches.forEach(p => p()); 84 | }; 85 | -------------------------------------------------------------------------------- /src/core/ui/hooks/useFS.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "@lib/api/native/fs"; 2 | import { useEffect, useMemo, useState } from "react"; 3 | 4 | export enum CheckState { 5 | FALSE, 6 | TRUE, 7 | LOADING, 8 | ERROR 9 | } 10 | 11 | export function useFileExists(path: string, prefix?: string): [CheckState, typeof fs] { 12 | const [state, setState] = useState(CheckState.LOADING); 13 | 14 | const check = () => fs.fileExists(path, { prefix }) 15 | .then(exists => setState(exists ? CheckState.TRUE : CheckState.FALSE)) 16 | .catch(() => setState(CheckState.ERROR)); 17 | 18 | const customFS = useMemo(() => new Proxy(fs, { 19 | get(target, p, receiver) { 20 | const val = Reflect.get(target, p, receiver); 21 | if (typeof val !== "function") return; 22 | 23 | return (...args: any[]) => { 24 | const promise = (check(), val(...args)); 25 | if (promise?.constructor?.name === "Promise") { 26 | setState(CheckState.LOADING); 27 | promise.finally(check); 28 | } 29 | return promise; 30 | }; 31 | }, 32 | }), []); 33 | 34 | useEffect(() => void check(), []); 35 | return [state, customFS]; 36 | } 37 | -------------------------------------------------------------------------------- /src/core/ui/reporter/components/ErrorBoundaryScreen.tsx: -------------------------------------------------------------------------------- 1 | import { hasStack, isComponentStack } from "@core/ui/reporter/utils/isStack"; 2 | import { getDebugInfo, toggleSafeMode } from "@lib/api/debug"; 3 | import { BundleUpdaterManager } from "@lib/api/native/modules"; 4 | import { settings } from "@lib/api/settings"; 5 | import { Codeblock, ErrorBoundary } from "@lib/ui/components"; 6 | import { createStyles } from "@lib/ui/styles"; 7 | import { tokens } from "@metro/common"; 8 | import { Button, Card, SafeAreaProvider, SafeAreaView, Text } from "@metro/common/components"; 9 | import { ScrollView, View } from "react-native"; 10 | 11 | import ErrorComponentStackCard from "./ErrorComponentStackCard"; 12 | import ErrorStackCard from "./ErrorStackCard"; 13 | 14 | const useStyles = createStyles({ 15 | container: { 16 | flex: 1, 17 | backgroundColor: tokens.colors.BG_BASE_SECONDARY, 18 | paddingHorizontal: 16, 19 | height: "100%", 20 | gap: 12 21 | } 22 | }); 23 | 24 | export default function ErrorBoundaryScreen(props: { 25 | error: Error; 26 | rerender: () => void; 27 | }) { 28 | const styles = useStyles(); 29 | const debugInfo = getDebugInfo(); 30 | 31 | return 32 | 33 | 34 | 35 | Uh oh. 36 | A crash occurred while rendering a component. This could be caused by a plugin, Revenge, or Discord itself. 37 | {debugInfo.os.name}; {debugInfo.discord.build} ({debugInfo.discord.version}); {debugInfo.bunny.version} 38 | 39 | 40 | {props.error.message} 41 | {hasStack(props.error) && } 42 | {isComponentStack(props.error) ? : null} 43 | 44 | 45 |