chore: merge dev to main (#52)

This commit is contained in:
Palm 2024-10-21 01:05:03 +07:00 • committed by GitHub
commit f10a413af6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
102 changed files with 9849 additions and 3106 deletions

View file

@ -1,79 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"ignorePatterns": [ "dist", "browser" ],
"plugins": [
"@typescript-eslint",
"simple-import-sort",
"unused-imports",
"import-alias"
],
"rules": {
"no-restricted-syntax": [ "error", {
"selector": "AwaitExpression:not(:function *)",
"message": "Hermes does not support top-level await, and SWC cannot transform it."
} ],
"quotes": [ "error", "double", { "avoidEscape": true } ],
"jsx-quotes": [ "error", "prefer-double" ],
"no-mixed-spaces-and-tabs": "error",
"indent": [ "error", 4, { "SwitchCase": 1 } ],
"arrow-parens": [ "error", "as-needed" ],
"eol-last": [ "error", "always" ],
"func-call-spacing": [ "error", "never" ],
"no-multi-spaces": "error",
"no-trailing-spaces": "error",
"no-whitespace-before-property": "error",
"semi": [ "error", "always" ],
"semi-style": [ "error", "last" ],
"space-in-parens": [ "error", "never" ],
"block-spacing": [ "error", "always" ],
"object-curly-spacing": [ "error", "always" ],
"eqeqeq": [ "error", "always", { "null": "ignore" } ],
"spaced-comment": [ "error", "always", { "markers": [ "!" ] } ],
"yoda": "error",
"prefer-destructuring": [ "error", { "object": true, "array": false } ],
"operator-assignment": [ "error", "always" ],
"no-useless-computed-key": "error",
"no-unneeded-ternary": [ "error", { "defaultAssignment": false } ],
"no-invalid-regexp": "error",
"no-constant-condition": [ "error", { "checkLoops": false } ],
"no-duplicate-imports": "error",
"no-extra-semi": "error",
"dot-notation": "error",
"no-useless-escape": [ "error" ],
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-spread": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
"import-alias/import-alias": [
"error",
{
"relativeDepth": 0,
"aliases": [
{ "alias": "@metro", "matcher": "^src/lib/metro" },
{ "alias": "@core", "matcher": "^src/core" },
{ "alias": "@ui", "matcher": "^src/lib/ui" },
{ "alias": "@types", "matcher": "^src/lib/utils/types.ts" },
{ "alias": "@lib", "matcher": "^src/lib" }
]
}
]
}
}

1
.github/FUNDING.yml vendored
View file

@ -1 +0,0 @@
github: [pylixonly]

View file

@ -1,35 +1,59 @@
---
name: Bug report
about: Create a report to help us improve
name: 🐞 Bug report
description: Report a bug or an issue.
title: 'bug: '
labels: bug
assignees: ''
labels: ['Bug report']
body:
- type: markdown
attributes:
value: |
# Revenge bug report
---
Before creating a new bug report, please keep the following in mind:
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Device Information (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Discord Version [e.g. 222111]
- Bunny Version [e.g e19e89c-main]
**Checklists**
- [ ] I understand that this is a bug report and not a place to request new features or plugins
- [ ] I understand that this is no place to report bugs produced by a plugin
- [ ] I can reproduce this bug in Bunny's safe mode AND unable to reproduce it in vanilla/unmodded Discord
- **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).
- **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).
- **Do not use the issue page for support**: If you need help or have questions, join us on [Discord](https://discord.gg/ddcQf3s2Uq).
- type: textarea
attributes:
label: Bug description
description: |
- Describe your bug in detail
- Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...)
- Add images and videos if possible
validations:
required: true
- type: textarea
attributes:
label: Stack trace
description: If this bug causes a JS crash, please paste the stack trace here.
render: shell
- type: textarea
attributes:
label: Component stack trace
description: If this bug causes a JS crash, please paste the component stack trace here.
render: shell
- type: textarea
attributes:
label: Native crash trace
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`.
render: shell
- type: textarea
attributes:
label: Solution
description: If applicable, add a possible solution to the bug.
- type: textarea
attributes:
label: Additional context
description: Add additional context here.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your bug report will be closed if you don't follow the checklist below.
options:
- label: I have checked all open and closed bug reports and this is not a duplicate.
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: 🗨 Discussions
url: https://discord.gg/ddcQf3s2Uq
about: Have something unspecific to Revenge in mind? Join us on Discord!

View file

@ -0,0 +1,45 @@
name: ⭐ Feature request
description: Create a detailed request for a new feature.
title: 'feat: '
labels: ['Feature request']
body:
- type: markdown
attributes:
value: |
# Revenge feature request
Before creating a new feature request, please keep the following in mind:
- **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).
- **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).
- **Do not use the issue page for support**: If you need help or have questions, join us on [Discord](https://discord.gg/ddcQf3s2Uq).
- type: textarea
attributes:
label: Feature description
description: |
- Describe your feature in detail
- Add images, videos, links, examples, references, etc. if possible
- type: textarea
attributes:
label: Motivation
description: |
A strong motivation is necessary for a feature request to be considered.
- Why should this feature be implemented?
- What is the explicit use case?
- What are the benefits?
- What makes this feature important?
validations:
required: true
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your feature request will be closed if you don't follow the checklist below.
options:
- label: I have checked all open and closed feature requests and this is not a duplicate
required: true
- label: I have chosen an appropriate title.
required: true
- label: All requested information has been provided properly.
required: true

2
.github/config.yml vendored Normal file
View file

@ -0,0 +1,2 @@
firstPRMergeComment: >
Thank you for contributing to Revenge. Join us on [Discord](https://discord.gg/ddcQf3s2Uq) to receive a role for your contribution.

View file

@ -1,84 +0,0 @@
name: Build
on:
push:
tags:
- "v*"
jobs:
build:
name: Build and release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Checkout builds
uses: actions/checkout@v4
with:
repository: "revenge-mod/ci-builds"
path: "builds"
token: ${{ secrets.BUILDS_PUSH_TOKEN }}
# TODO: fix this
ref: main
- name: Set env
id: env
run: |
echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
echo "hash=$GITHUB_SHA" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install pnpm and dependencies
run: |
npm i -g pnpm
pnpm i
- name: Build
run: pnpm build --release-branch=${{ github.ref_name }} --build-minify
- name: Create draft release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.env.outputs.tag }}
release_name: Revenge ${{ steps.env.outputs.tag }}
draft: true
prerelease: false
- name: Upload dist
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/revenge.js
asset_name: revenge.js
asset_content_type: text/javascript
- name: Publish release
uses: eregon/publish-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
release_id: ${{ steps.create_release.outputs.id }}
- name: Push build
run: |
rm $GITHUB_WORKSPACE/builds/* || true
cp -r dist/* $GITHUB_WORKSPACE/builds || true
cd $GITHUB_WORKSPACE/builds
git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions"
git add .
git commit -m "Build $GITHUB_SHA" || exit 0
git push
env:
GITHUB_TOKEN: ${{ secrets.BUILDS_PUSH_TOKEN }}

View file

@ -1,32 +0,0 @@
name: Sync with I18n repository
on:
push:
branches:
- 'main'
paths:
- 'src/core/i18n/default.json'
workflow_dispatch:
jobs:
sync:
name: Synchronize strings
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v3
with:
repository: "pyoncord/i18n"
path: "main"
token: ${{ secrets.PYLIX_TOKEN }}
- name: Push
run: |
cp src/core/i18n/default.json $GITHUB_WORKSPACE/main/base/bunny.json
cd $GITHUB_WORKSPACE/main
git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions"
git add .
git commit -m "Strings update from Bunny" || exit 0
git push

35
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: Release
on:
push:
branches: [main, dev]
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Release
run: pnpx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_BRANCH: ${{ github.ref_name }}

53
.releaserc.js Normal file
View file

@ -0,0 +1,53 @@
export default {
branches: [
"main",
{
name: "dev",
prerelease: true,
},
],
plugins: [
[
"@semantic-release/commit-analyzer",
{
releaseRules: [
{ type: "build", scope: "Needs bump", release: "patch" },
],
},
],
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
[
"@semantic-release/git",
{
assets: ["README.md", "CHANGELOG.md"],
message:
"chore: Release ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}",
},
],
[
"@semantic-release/exec",
{
prepareCmd: `pnpm build --release-branch ${process.env.RELEASE_BRANCH} --build-minify`,
},
],
[
"@semantic-release/github",
{
assets: [
{
path: "dist/*.js",
},
],
successComment: false,
},
],
[
"@saithodev/semantic-release-backmerge",
{
backmergeBranches: [{ from: "main", to: "dev" }],
clearWorkspace: true,
},
],
],
};

36
CHANGELOG.md Normal file
View file

@ -0,0 +1,36 @@
# [1.2.0-dev.3](https://github.com/revenge-mod/revenge-bundle/compare/v1.2.0-dev.2...v1.2.0-dev.3) (2024-10-19)
### Bug Fixes
* downgrade dependencies to fix build issue ([b803053](https://github.com/revenge-mod/revenge-bundle/commit/b803053e28c77529d62179b75c22c2365869d020))
# [1.2.0-dev.2](https://github.com/revenge-mod/revenge-bundle/compare/v1.2.0-dev.1...v1.2.0-dev.2) (2024-10-19)
### Bug Fixes
* wrong imports ([87ba67e](https://github.com/revenge-mod/revenge-bundle/commit/87ba67e8c2a3cec6c9b03312937dfa3f5adde0f8))
# [1.2.0-dev.1](https://github.com/revenge-mod/revenge-bundle/compare/v1.1.2...v1.2.0-dev.1) (2024-10-19)
### Bug Fixes
* **badges:** check if user is null ([a0c2ca7](https://github.com/revenge-mod/revenge-bundle/commit/a0c2ca7837c4585ead0e630d4475c354671fb8fd))
* Fix old settings menu and make some improvements to it ([#90](https://github.com/revenge-mod/revenge-bundle/issues/90)) ([e240509](https://github.com/revenge-mod/revenge-bundle/commit/e2405093eea5268d0afe88ded8735db7b5dfef43))
* load core plugins ([ed52f33](https://github.com/revenge-mod/revenge-bundle/commit/ed52f332da8a189eb1141e1f2c17f50c50562d1a))
* remove debug traces ([863466e](https://github.com/revenge-mod/revenge-bundle/commit/863466e48a741ede369a7032554191eac81e0905))
* remove parent from emitter callback ([fe937a4](https://github.com/revenge-mod/revenge-bundle/commit/fe937a4ab5631a0bb23706794511598770015e09))
* **types:** fix typescript [skip ci] ([61f9b65](https://github.com/revenge-mod/revenge-bundle/commit/61f9b6510b18032e4e91b7b2c87315d8ce6dea85))
### Features
* **coreplugins:** add badges ([e3df56d](https://github.com/revenge-mod/revenge-bundle/commit/e3df56dc58c8c6078269ef800d6ad899ddfa52d3))
* **metro:** expose common components & custom jsx runtime ([019fdc2](https://github.com/revenge-mod/revenge-bundle/commit/019fdc28b345e8b6f07cb96562f5e26c112da4cf))
* Rebrand to Revenge ([#51](https://github.com/revenge-mod/revenge-bundle/issues/51)) ([0a61404](https://github.com/revenge-mod/revenge-bundle/commit/0a614041327fa1232331676c40a8cd60839276ba))
* **storage:** port storage api from dev ([122c3a1](https://github.com/revenge-mod/revenge-bundle/commit/122c3a11e9f173c9c2ad9cd20d18926220428e8b))
* **ui:** ponyfill AlertModal's `extraContent` prop for older versions ([ad65247](https://github.com/revenge-mod/revenge-bundle/commit/ad6524772b9dd98f36b640b8fba2d36e2d6a7f2b)), closes [#91](https://github.com/revenge-mod/revenge-bundle/issues/91)
* **ui:** simplify some components ([4d88183](https://github.com/revenge-mod/revenge-bundle/commit/4d881835a63a64602dc0edc0332ee0097d28a601))
* **ui:** simplify unproxied notice ([1d4dfd3](https://github.com/revenge-mod/revenge-bundle/commit/1d4dfd311f4818d961b8667811bce7ecc0989152))

35
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,35 @@
# 👋 Contribution guidelines
This document describes how to contribute to Revenge bundle.
## 📖 Resources to help you get started
* [Issues](https://github.com/revenge-mod/revenge-bundle/issues) are where we keep track of bugs and feature requests
## 🙏 Submitting a feature request
Features can be requested by opening an issue using the
[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+).
> **Note**
> Requests can be accepted or rejected at the discretion of maintainers of Revenge bundle.
> Good motivation has to be provided for a request to be accepted.
## 🐞 Submitting a bug report
If you encounter a bug while using Revenge bundle, open an issue using the
[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+).
## 📝 How to contribute
1. Before contributing, it is recommended to open an issue to discuss your change
with the maintainers of Revenge bundle. This will help you determine whether your change is acceptable
and whether it is worth your time to implement it
2. Development happens on the `dev` branch. Fork the repository and create your branch from `dev`
3. Commit your changes
4. Submit a pull request to the `dev` branch of the repository and reference issues
that your pull request closes in the description of your pull request
5. Our team will review your pull request and provide feedback. Once your pull request is approved,
it will be merged into the `dev` branch and will be included in the next release of Revenge bundle
❤️ Thank you for considering contributing to Revenge bundle

View file

@ -1,42 +1,79 @@
# Revenge [![Discord](https://img.shields.io/discord/1205207689832038522?style=social&logo=discord&label=Revenge)]([https://discord.gg/24Ue3pzaum])
A modification for Discord's mobile apps, a fork of [Bunny](https://github.com/pyoncord/Bunny).
<p align="center">
<picture>
<source
width="512px"
media="(prefers-color-scheme: dark)"
srcset="assets/wordmark/wordmark+slogan-dark.svg"
>
<img
width="512px"
src="assets/wordmark/wordmark+slogan-light.svg"
>
</picture>
<br>
<a href="https://revenge-mod.github.io/">
<picture>
<source height="32px" media="(prefers-color-scheme: dark)" srcset="assets/logo/logo-dark.svg" />
<img height="32px" src="assets/logo/logo-light.svg" />
</picture>
</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://discord.com/invite/ddcQf3s2Uq">
<picture>
<source height="32px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
<img height="32px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://github.com/revenge-mod">
<picture>
<source height="32px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
<img height="32px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
</picture>
</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
## Installing
</a>
</p>
# 👊 Revenge
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/revenge-mod/revenge-bundle/release.yml)
[![3-Clause BSD License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
A client modification for Discord mobile, a continuation of [Bunny](https://github.com/pyoncord/Bunny).
## 💪 Features
- **🔌 Plugins**: Extend Discord with custom features
- **🎨 Themes & Fonts**: Customize Discord's appearance to your liking
- **🧪 Experiments**: Try out Discord's new features before they're rolled out
## ⬇️ Download
This repository releases JavaScript bundles for loaders to execute. These are the official Revenge loaders:
### Android
- **Root** with Xposed - [RevengeXposed](https://github.com/revenge-mod/RevengeXposed/releases/latest)
- **Non-root** - [Revenge Manager](https://github.com/revenge-mod/RevengeManager/releases/latest)
- **🩹 Root with Xposed**: [RevengeXposed](https://github.com/revenge-mod/revenge-xposed/releases/latest)
- **📵 Non-root**: [Revenge Manager](https://github.com/revenge-mod/revenge-manager/releases/latest)
### iOS
- [**RevengeTweak**](https://github.com/revenge-mod/RevengeTweak) - Get prebuilt rootful and rootless `.deb` files or the prepatched `.ipa `
## Building
1. Install a Revenge loader with config support (any mentioned in the [Installing](#installing) section).
2. Go to **Settings** > **General** and enable **Developer Settings**.
3. Clone the repository
- [**RevengeTweak**](https://github.com/revenge-mod/revenge-tweak): Prebuilt rootful and rootless `.deb` files or the prepatched `.ipa`
```sh
git clone https://github.com/revenge-mod/Revenge.git
```
## 📚 Everything else
4. Install dependencies
### 📙 Contributing
```
pnpm i
```
Thank you for considering contributing to Revenge.
You can find the contribution guidelines [here](CONTRIBUTING.md).
5. Build Revenge's code
### 🛠️ Building
```
pnpm build
```
To build Revenge bundle, you can follow the [documentation](/docs).
6. In the newly created `dist` directory, run a HTTP server. I recommend [http-server](https://www.npmjs.com/package/http-server).
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`.
8. Restart Discord. Upon reload, you should notice that your device will download Revenge's bundled code from your server, rather than GitHub.
9. Make your changes, rebuild, reload, go wild!
### 📃 Documentation
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://<local ip address>: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.
You can find the documentation of Revenge bundle [here](/docs).
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.
## 📜 Licence
Revenge bundle is licensed under the 3-Clause BSD license. Please see the [license file](LICENSE) for more information.

View file

@ -0,0 +1,4 @@
<svg width="320" height="320" viewBox="0 0 320 320" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.26979 99H294.723L312.234 215.273C312.723 222.597 310.021 229.993 304.839 235.184L265.514 274.935C260.741 279.717 254.27 283.273 247.514 283.273H182.785C168.723 283.273 152.981 270.126 152.981 256.073V255.61C152.981 250.056 156.895 244.851 160.088 240.605L160.088 240.605L160.288 240.339L75.9944 236.224C73.9944 236.135 69.0077 236.055 62.9899 235.984C43.5233 235.762 27.1144 221.37 24.4033 202.09L8.74994 103.006C8.84706 101.648 9.02178 100.311 9.26979 99ZM84.5458 137.592L139.983 152.446C139.994 152.788 140 153.132 140 153.477C140 170.045 126.568 183.477 110 183.477C93.4314 183.477 79.9999 170.045 79.9999 153.477C79.9999 147.643 81.665 142.198 84.5458 137.592ZM180.019 152.4L235.429 137.553C238.325 142.168 240 147.627 240 153.477C240 170.045 226.568 183.477 210 183.477C193.431 183.477 180 170.045 180 153.477C180 153.116 180.006 152.757 180.019 152.4Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M288.328 82.0258C288.569 83.4188 289.198 84.7073 290.018 85.8593C292.71 89.6425 294.372 94.0776 294.688 98.766L295.899 106.809C296.628 111.647 292.881 116 287.988 116H17.6382C13.7018 116 10.3504 113.136 9.73617 109.248L8.89299 103.911C8.79798 103.31 8.76815 102.697 8.82742 102.091C10.4271 85.7347 23.3039 72.6711 39.7278 70.9082L87.1856 63.328C89.9772 62.8821 92.7967 63.9426 94.6025 66.1175L105.915 79.7431C107.719 81.9152 110.533 82.9759 113.322 82.5343L203.656 68.2267C205.851 67.8792 207.802 66.6347 209.042 64.7913L222.878 44.2296C224.116 42.3892 226.063 41.1457 228.253 40.7958L247.639 37.6994C265.683 35.7705 282.181 48.0371 285.523 65.8683L288.328 82.0258Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,4 @@
<svg width="320" height="320" viewBox="0 0 320 320" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.26979 99H294.723L312.234 215.273C312.723 222.597 310.021 229.993 304.839 235.184L265.514 274.935C260.741 279.717 254.27 283.273 247.514 283.273H182.785C168.723 283.273 152.981 270.126 152.981 256.073V255.61C152.981 250.056 156.895 244.851 160.088 240.605L160.088 240.605L160.288 240.339L75.9944 236.224C73.9944 236.135 69.0077 236.055 62.9899 235.984C43.5233 235.762 27.1144 221.37 24.4033 202.09L8.74994 103.006C8.84706 101.648 9.02178 100.311 9.26979 99ZM84.5458 137.592L139.983 152.446C139.994 152.788 140 153.132 140 153.477C140 170.045 126.568 183.477 110 183.477C93.4314 183.477 79.9999 170.045 79.9999 153.477C79.9999 147.643 81.665 142.198 84.5458 137.592ZM180.019 152.4L235.429 137.553C238.325 142.168 240 147.627 240 153.477C240 170.045 226.568 183.477 210 183.477C193.431 183.477 180 170.045 180 153.477C180 153.116 180.006 152.757 180.019 152.4Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M288.328 82.0258C288.569 83.4188 289.198 84.7073 290.018 85.8593C292.71 89.6425 294.372 94.0776 294.688 98.766L295.899 106.809C296.628 111.647 292.881 116 287.988 116H17.6382C13.7018 116 10.3504 113.136 9.73617 109.248L8.89299 103.911C8.79798 103.31 8.76815 102.697 8.82742 102.091C10.4271 85.7347 23.3039 72.6711 39.7278 70.9082L87.1856 63.328C89.9772 62.8821 92.7967 63.9426 94.6025 66.1175L105.915 79.7431C107.719 81.9152 110.533 82.9759 113.322 82.5343L203.656 68.2267C205.851 67.8792 207.802 66.6347 209.042 64.7913L222.878 44.2296C224.116 42.3892 226.063 41.1457 228.253 40.7958L247.639 37.6994C265.683 35.7705 282.181 48.0371 285.523 65.8683L288.328 82.0258Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

29
docs/2_building.md Normal file
View file

@ -0,0 +1,29 @@
## Building
1. Install a Revenge loader with config support (any mentioned in the [Installing](#installing) section).
2. Go to **Settings** > **General** and enable **Developer Settings**.
3. Clone the repository
```sh
git clone https://github.com/revenge-mod/revenge-bundle.git
```
4. Install dependencies
```
pnpm i
```
5. Build Revenge's code
```
pnpm build
```
6. In the newly created `dist` directory, run a HTTP server. I recommend [http-server](https://www.npmjs.com/package/http-server).
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`.
8. Restart Discord. Upon reload, you should notice that your device will download Revenge's bundled code from your server, rather than GitHub.
9. Make your changes, rebuild, reload, go wild!
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://<local ip address>: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.
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.

13
docs/README.md Normal file
View file

@ -0,0 +1,13 @@
# 👊 Revenge
This documentation explains how to use [Revenge](https://github.com/revenge-mod/revenge-bundle).
## 📖 Table of contents
TODO.
## ⏭️ Start here
The next page will tell you about the prerequisites for using Revenge.
Continue: [💼 Prerequisites](0_prerequisites.md)

125
eslint.config.mjs Normal file
View file

@ -0,0 +1,125 @@
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import simpleImportSort from "eslint-plugin-simple-import-sort";
import unusedImports from "eslint-plugin-unused-imports";
import importAlias from "eslint-plugin-import-alias";
import tsParser from "@typescript-eslint/parser";
export default [{
ignores: ["**/dist", "**/browser"],
}, {
plugins: {
"@typescript-eslint": typescriptEslint,
"simple-import-sort": simpleImportSort,
"unused-imports": unusedImports,
"import-alias": importAlias,
},
languageOptions: {
parser: tsParser,
},
rules: {
"no-restricted-syntax": ["error", {
selector: "AwaitExpression:not(:function *)",
message: "Hermes does not support top-level await, and SWC cannot transform it.",
}],
quotes: ["error", "double", {
avoidEscape: true,
}],
"jsx-quotes": ["error", "prefer-double"],
"no-mixed-spaces-and-tabs": "error",
indent: ["error", 4, {
SwitchCase: 1,
}],
"arrow-parens": ["error", "as-needed"],
"eol-last": ["error", "always"],
"func-call-spacing": ["error", "never"],
"no-multi-spaces": "error",
"no-trailing-spaces": "error",
"no-whitespace-before-property": "error",
semi: ["error", "always"],
"semi-style": ["error", "last"],
"space-in-parens": ["error", "never"],
"block-spacing": ["error", "always"],
"object-curly-spacing": ["error", "always"],
eqeqeq: ["error", "always", {
null: "ignore",
}],
"spaced-comment": ["error", "always", {
markers: ["!"],
}],
yoda: "error",
"prefer-destructuring": ["error", {
object: true,
array: false,
}],
"operator-assignment": ["error", "always"],
"no-useless-computed-key": "error",
"no-unneeded-ternary": ["error", {
defaultAssignment: false,
}],
"no-invalid-regexp": "error",
"no-constant-condition": ["error", {
checkLoops: false,
}],
"no-duplicate-imports": "error",
"no-extra-semi": "error",
"dot-notation": "error",
"no-useless-escape": ["error"],
"no-fallthrough": "error",
"for-direction": "error",
"no-async-promise-executor": "error",
"no-cond-assign": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-shadow-restricted-names": "error",
"no-unexpected-multiline": "error",
"no-unsafe-optional-chaining": "error",
"no-useless-backreference": "error",
"use-isnan": "error",
"prefer-const": "error",
"prefer-spread": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"unused-imports/no-unused-imports": "error",
"import-alias/import-alias": ["error", {
relativeDepth: 0,
aliases: [{
alias: "@metro",
matcher: "^src/lib/metro",
}, {
alias: "@core",
matcher: "^src/core",
}, {
alias: "@ui",
matcher: "^src/lib/ui",
}, {
alias: "@types",
matcher: "^src/lib/utils/types.ts",
}, {
alias: "@lib",
matcher: "^src/lib",
}],
}],
},
}];

View file

@ -1,6 +1,8 @@
{
"name": "revenge",
"private": true,
"name": "@revenge-mod/revenge",
"exports": {
"types": "./dist/types/index.d.ts"
},
"author": {
"name": "Revenge Team",
"url": "https://github.com/revenge-mod"
@ -13,6 +15,11 @@
},
"license": "BSD-3-Clause",
"devDependencies": {
"@saithodev/semantic-release-backmerge": "^4.0.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"semantic-release": "^24.1.3",
"@swc/core": "^1.7.0",
"@swc/helpers": "^0.5.12",
"@types/node": "^20.14.11",
@ -32,6 +39,7 @@
"yargs-parser": "^21.1.1"
},
"dependencies": {
"@gullerya/object-observer": "^6.1.3",
"@shopify/react-native-skia": "^1.3.8",
"@tanstack/react-query": "^5.51.16",
"@types/chroma-js": "~2.4.4",

10176
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -41,7 +41,7 @@ export function serve(options) {
server.listen(args.port ?? 4040);
console.info(chalk.bold.yellowBright("Serving Bunny bundle, available on:"));
console.info(chalk.bold.yellowBright("Serving Revenge bundle, available on:"));
const netInterfaces = os.networkInterfaces();
for (const netinterfaces of Object.values(netInterfaces)) {

View file

@ -4,6 +4,10 @@ import { findByPropsLazy } from "@metro/wrappers";
const jsxRuntime = findByPropsLazy("jsx", "jsxs", "Fragment");
function unproxyFirstArg<T>(args: T[]) {
if (!args[0]) {
throw new Error("The first argument (Component) is falsy. Ensure that you are passing a valid component.");
}
const factory = getProxyFactory(args[0]);
if (factory) args[0] = factory();
return args;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View file

@ -16,8 +16,8 @@ export default () => <ApplicationCommand>{
execute([ephemeral], ctx) {
const info = getDebugInfo();
const content = [
"**Bunny Debug Info**",
`> Bunny: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
"**Revenge Debug Info**",
`> Revenge: ${info.bunny.version} (${info.bunny.loader.name} ${info.bunny.loader.version})`,
`> Discord: ${info.discord.version} (${info.discord.build})`,
`> React: ${info.react.version} (RN ${info.react.nativeVersion})`,
`> Hermes: ${info.hermes.version} (bcv${info.hermes.bytecodeVersion})`,

View file

@ -0,0 +1,61 @@
import { after } from "@lib/api/patcher";
import { onJsxCreate } from "@lib/api/react/jsx";
import { findByName } from "@metro";
import { useEffect, useState } from "react";
import { defineCorePlugin } from "..";
interface BunnyBadge {
label: string;
url: string;
}
const useBadgesModule = findByName("useBadges", false);
export default defineCorePlugin({
manifest: {
id: "bunny.badges",
name: "Badges",
version: "1.0.0",
description: "Adds badges to user's profile",
authors: [{ name: "pylixonly" }]
},
start() {
const propHolder = {} as Record<string, any>;
const badgeCache = {} as Record<string, BunnyBadge[]>;
onJsxCreate("RenderedBadge", (_, ret) => {
if (ret.props.id.match(/bunny-\d+-\d+/)) {
Object.assign(ret.props, propHolder[ret.props.id]);
}
});
after("default", useBadgesModule, ([user], r) => {
const [badges, setBadges] = useState<BunnyBadge[]>(user ? badgeCache[user.userId] ??= [] : []);
useEffect(() => {
if (user) {
fetch(`https://raw.githubusercontent.com/pyoncord/badges/refs/heads/main/${user.userId}.json`)
.then(r => r.json())
.then(badges => setBadges(badgeCache[user.userId] = badges));
}
}, [user]);
if (user) {
badges.forEach((badges, i) => {
propHolder[`bunny-${user.userId}-${i}`] = {
source: { uri: badges.url },
id: `bunny-${i}`,
label: badges.label
};
r.push({
id: `bunny-${user.userId}-${i}`,
description: badges.label,
icon: "_",
});
});
}
});
}
});

View file

@ -1,4 +1,4 @@
import { PluginInstanceInternal } from "@lib/plugins/types";
import { PluginInstanceInternal } from "@lib/addons/plugins/types";
interface CorePlugin {
default: PluginInstanceInternal;
@ -7,7 +7,8 @@ interface CorePlugin {
// Called from @lib/plugins
export const getCorePlugins = (): Record<string, CorePlugin> => ({
"bunny.quickinstall": require("./quickinstall")
"bunny.quickinstall": require("./quickinstall"),
"bunny.badges": require("./badges")
});
/**

View file

@ -1,11 +1,11 @@
import { Strings } from "@core/i18n";
import { VdPluginManager } from "@core/vendetta/plugins";
import { useProxy } from "@core/vendetta/storage";
import { installTheme, removeTheme, themes } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { isThemeSupported } from "@lib/api/native/loader";
import { after } from "@lib/api/patcher";
import { useProxy } from "@lib/api/storage";
import { installTheme, removeTheme, themes } from "@lib/themes";
import { DISCORD_SERVER_ID, HTTP_REGEX_MULTI, PLUGINS_CHANNEL_ID, THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { HTTP_REGEX_MULTI, VD_DISCORD_SERVER_ID, VD_PLUGINS_CHANNEL_ID, VD_PROXY_PREFIX, VD_THEMES_CHANNEL_ID } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
import { Button } from "@metro/common/components";
import { findByProps, findByPropsLazy } from "@metro/wrappers";
@ -39,13 +39,13 @@ const postMap = {
};
function useExtractThreadContent(thread: any, _firstMessage = null, actionSheet = false): ([PostType, string]) | void {
if (thread.guild_id !== DISCORD_SERVER_ID) return;
if (thread.guild_id !== VD_DISCORD_SERVER_ID) return;
// Determine what type of addon this is.
let postType: PostType;
if (thread.parent_id === PLUGINS_CHANNEL_ID) {
if (thread.parent_id === VD_PLUGINS_CHANNEL_ID) {
postType = "Plugin";
} else if (thread.parent_id === THEMES_CHANNEL_ID && isThemeSupported()) {
} else if (thread.parent_id === VD_THEMES_CHANNEL_ID && isThemeSupported()) {
postType = "Theme";
} else return;

View file

@ -10,7 +10,7 @@ export default defineCorePlugin({
name: "QuickInstall",
version: "1.0.0",
description: "Quickly install Vendetta plugins and themes",
authors: ["pyoncord"]
authors: [{ name: "Vendetta Team" }]
},
start() {
patches = [patchForumPost(), patchUrl()];

View file

@ -1,16 +1,16 @@
import { formatString, Strings } from "@core/i18n";
import { showConfirmationAlert } from "@core/vendetta/alerts";
import { VdPluginManager } from "@core/vendetta/plugins";
import { installTheme } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { isThemeSupported } from "@lib/api/native/loader";
import { after, instead } from "@lib/api/patcher";
import { installTheme } from "@lib/themes";
import { THEMES_CHANNEL_ID, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { VD_PROXY_PREFIX,VD_THEMES_CHANNEL_ID } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
import { channels, url } from "@metro/common";
import { byMutableProp } from "@metro/filters";
import { findExports } from "@metro/finders";
import { findByProps, findByPropsLazy } from "@metro/wrappers";
import { showConfirmationAlert } from "@ui/alerts";
import { showToast } from "@ui/toasts";
const showSimpleActionSheet = findExports(byMutableProp("showSimpleActionSheet"));
@ -66,7 +66,7 @@ export default () => {
if (!urlType) return orig.apply(this, args);
// Make clicking on theme links only work in #themes, should there be a theme proxy in the future, this can be removed.
if (urlType === "theme" && getChannel(getChannelId())?.parent_id !== THEMES_CHANNEL_ID) return orig.apply(this, args);
if (urlType === "theme" && getChannel(getChannelId())?.parent_id !== VD_THEMES_CHANNEL_ID) return orig.apply(this, args);
showConfirmationAlert({
title: Strings.HOLD_UP,

View file

@ -1,7 +1,8 @@
import { CardWrapper } from "@core/ui/components/AddonCard";
import { useProxy } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import AlertModal, { AlertActionButton } from "@lib/ui/components/wrappers/AlertModal";
import isValidHttpUrl from "@lib/utils/isValidHttpUrl";
import { lazyDestructure } from "@lib/utils/lazy";
import { findByProps } from "@metro";
@ -14,7 +15,6 @@ import { Image, ScrollView, View } from "react-native";
const { showSimpleActionSheet, hideActionSheet } = lazyDestructure(() => findByProps("showSimpleActionSheet"));
const { openAlert, dismissAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
type SearchKeywords = Array<string | ((obj: any & {}) => string)>;
@ -27,19 +27,19 @@ interface AddonPageProps<T extends object, I = any> {
safeModeHint?: {
message?: string;
footer?: ReactNode;
}
};
installAction?: {
label?: string;
// Ignored when onPress is defined!
fetchFn?: (url: string) => Promise<void>;
onPress?: () => void;
}
};
CardComponent: ComponentType<CardWrapper<T>>;
ListHeaderComponent?: ComponentType<any>;
ListFooterComponent?: ComponentType<any>;
}
function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<void> }) {
function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<void>; }) {
const [value, setValue] = React.useState("");
const [error, setError] = React.useState("");
const [isFetching, setIsFetching] = React.useState(false);
@ -55,7 +55,7 @@ function InputAlert(props: { label: string, fetchFn: (url: string) => Promise<vo
return <AlertModal
title={props.label}
content="Type in the source URL you want to install from:"
content="Enter the URL of the source you want to install from:"
extraContent={
<Stack style={{ marginTop: -12 }}>
<TextInput
@ -122,7 +122,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
}, [props.items, sortFn, search]);
const onInstallPress = useCallback(() => {
if (!props.installAction) return () => {};
if (!props.installAction) return () => { };
const { label, onPress, fetchFn } = props.installAction;
if (fetchFn) {
openAlert("AddonInputAlert", <InputAlert label={label ?? "Install"} fetchFn={fetchFn} />);
@ -175,7 +175,7 @@ export default function AddonPage<T extends object>({ CardComponent, ...props }:
})}
/>}
</View>
{props.ListHeaderComponent && <props.ListHeaderComponent />}
{props.ListHeaderComponent && !search && <props.ListHeaderComponent />}
</View>
);

View file

@ -1,9 +1,9 @@
import PyoncordIcon from "@assets/icons/revenge.png";
import { Strings } from "@core/i18n";
import { useProxy } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { isFontSupported, isThemeSupported } from "@lib/api/native/loader";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { registerSection } from "@ui/settings";
import { version } from "bunny-build-info";
@ -19,9 +19,7 @@ export default function initSettings() {
title: () => Strings.BUNNY,
icon: { uri: PyoncordIcon },
render: () => import("@core/ui/settings/pages/General"),
rawTabsConfig: {
useTrailing: () => `(${version})`
}
useTrailing: () => `(${version})`
},
{
key: "BUNNY_PLUGINS",

View file

@ -1,11 +1,11 @@
import { Strings } from "@core/i18n";
import { CheckState, useFileExists } from "@core/ui/hooks/useFS";
import AssetBrowser from "@core/ui/settings/pages/Developer/AssetBrowser";
import { useProxy } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { connectToDebugger } from "@lib/api/debug";
import { getReactDevToolsProp, getReactDevToolsVersion, isLoaderConfigSupported, isReactDevToolsPreloaded, isVendettaLoader } from "@lib/api/native/loader";
import { loaderConfig, settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { lazyDestructure } from "@lib/utils/lazy";
import { NavigationNative } from "@metro/common";
import { Button, LegacyFormText, Stack, TableRow, TableRowGroup, TableSwitchRow, TextInput } from "@metro/common/components";
@ -21,7 +21,7 @@ const { showSimpleActionSheet } = lazyDestructure(() => findByProps("showSimpleA
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
const RDT_EMBED_LINK = "https://raw.githubusercontent.com/amsyarasyiq/rdt-embedder/main/dist.js";
const RDT_EMBED_LINK = "https://github.com/revenge-mod/react-devtools-core/releases/latest/download/rdtc.js";
const useStyles = createStyles({
leadingText: {

View file

@ -1,16 +1,15 @@
import { Strings } from "@core/i18n";
import { CardWrapper } from "@core/ui/components/AddonCard";
import { showConfirmationAlert } from "@core/vendetta/alerts";
import { useProxy } from "@core/vendetta/storage";
import { FontDefinition, fonts, selectFont } from "@lib/addons/fonts";
import { findAssetId } from "@lib/api/assets";
import { BundleUpdaterManager } from "@lib/api/native/modules";
import { useProxy } from "@lib/api/storage";
import { FontDefinition, fonts, selectFont } from "@lib/fonts";
import { lazyDestructure } from "@lib/utils/lazy";
import { ButtonColors } from "@lib/utils/types";
import { findByProps } from "@metro";
import { NavigationNative, tokens } from "@metro/common";
import { Button, Card, IconButton, Stack, Text } from "@metro/common/components";
import * as Skia from "@shopify/react-native-skia";
import { showConfirmationAlert } from "@ui/alerts";
import { TextStyleSheet } from "@ui/styles";
import { useMemo } from "react";
import { View } from "react-native";
@ -104,7 +103,7 @@ export default function FontCard({ item: font }: CardWrapper<FontDefinition>) {
content: "Reload Discord to apply changes?",
confirmText: Strings.RELOAD,
cancelText: Strings.CANCEL,
confirmColor: ButtonColors.RED,
confirmColor: "red",
onConfirm: BundleUpdaterManager.reload
});
}}

View file

@ -1,8 +1,8 @@
import { formatString, Strings } from "@core/i18n";
import { createProxy, useProxy } from "@core/vendetta/storage";
import { FontDefinition, fonts, removeFont, saveFont, validateFont } from "@lib/addons/fonts";
import { getCurrentTheme } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { createProxy, useProxy } from "@lib/api/storage";
import { FontDefinition, fonts, removeFont, saveFont, validateFont } from "@lib/fonts";
import { getCurrentTheme } from "@lib/themes";
import { safeFetch } from "@lib/utils";
import { NavigationNative } from "@metro/common";
import { ActionSheet, BottomSheetTitleHeader, Button, IconButton, Stack, TableRow, TableRowGroup, Text, TextInput } from "@metro/common/components";

View file

@ -1,9 +1,9 @@
import { Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import FontEditor from "@core/ui/settings/pages/Fonts/FontEditor";
import { useProxy } from "@core/vendetta/storage";
import { FontDefinition, fonts } from "@lib/addons/fonts";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { FontDefinition, fonts } from "@lib/fonts";
import { NavigationNative } from "@metro/common";
import FontCard from "./FontCard";

View file

@ -1,8 +1,8 @@
import { Strings } from "@core/i18n";
import Version from "@core/ui/settings/pages/General/Version";
import { useProxy } from "@core/vendetta/storage";
import { getDebugInfo } from "@lib/api/debug";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { Stack, TableRowGroup } from "@metro/common/components";
import { Platform, ScrollView } from "react-native";
@ -72,7 +72,7 @@ export default function About() {
{
label: Platform.select({ android: Strings.CODENAME, ios: Strings.MACHINE_ID })!,
version: debugInfo.device.codename,
icon: "ic_compose_24px"
icon: "ic_copy_id"
}
];

View file

@ -1,10 +1,10 @@
import { Strings } from "@core/i18n";
import { PyoncordIcon } from "@core/ui/settings";
import About from "@core/ui/settings/pages/General/About";
import { useProxy } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { getDebugInfo, toggleSafeMode } from "@lib/api/debug";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { DISCORD_SERVER, GITHUB } from "@lib/utils/constants";
import { NavigationNative, url } from "@metro/common";
import { Stack, TableRow, TableRowGroup, TableSwitchRow } from "@metro/common/components";

View file

@ -1,6 +1,6 @@
import { installPlugin, isPluginInstalled, uninstallPlugin } from "@lib/addons/plugins";
import { BunnyPluginManifest } from "@lib/addons/plugins/types";
import { findAssetId } from "@lib/api/assets";
import { installPlugin, isPluginInstalled, uninstallPlugin } from "@lib/plugins";
import { BunnyPluginManifest } from "@lib/plugins/types";
import { showToast } from "@lib/ui/toasts";
import { safeFetch } from "@lib/utils";
import { OFFICIAL_PLUGINS_REPO_URL } from "@lib/utils/constants";
@ -41,7 +41,7 @@ async function* getManifests(repoUrl: string) {
function InstallButton(props: { id: string; }) {
const [installed, setInstalled] = useState(isPluginInstalled(props.id));
const installationState = useMutation<void, Error, { install: boolean }>({
const installationState = useMutation<void, Error, { install: boolean; }>({
mutationFn: async ({ install }) => {
await (install ? installPlugin : uninstallPlugin)(props.id, true);
},
@ -63,11 +63,11 @@ function InstallButton(props: { id: string; }) {
/>;
}
function TrailingButtons(props: { id: string }) {
function TrailingButtons(props: { id: string; }) {
return <Stack spacing={8} direction="horizontal">
<IconButton
size="sm"
onPress={() => {}}
onPress={() => { }}
variant="secondary"
icon={findAssetId("CircleInformationIcon")}
/>

View file

@ -5,12 +5,12 @@ import { NavigationNative, tokens } from "@metro/common";
import { Card, IconButton, Stack, TableSwitch, Text } from "@metro/common/components";
import { showSheet } from "@ui/sheets";
import chroma from "chroma-js";
import { createContext, memo, useContext, useMemo } from "react";
import { createContext, useContext, useMemo } from "react";
import { Image, View } from "react-native";
import { UnifiedPluginModel } from "..";
const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult<UnifiedPluginModel> }>(null!);
const CardContext = createContext<{ plugin: UnifiedPluginModel, result: Fuzzysort.KeysResult<UnifiedPluginModel>; }>(null!);
const useCardContext = () => useContext(CardContext);
function getHighlightColor(): import("react-native").ColorValue {
@ -92,7 +92,7 @@ function Description() {
</Text>;
}
const Actions = memo(() => {
const Actions = () => {
const { plugin } = useCardContext();
const navigation = NavigationNative.useNavigation();
@ -118,7 +118,7 @@ const Actions = memo(() => {
)}
/>
</View>;
});
};
export default function PluginCard({ result, item: plugin }: CardWrapper<UnifiedPluginModel>) {
plugin.usePluginState();

View file

@ -2,26 +2,29 @@ import { Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import PluginCard from "@core/ui/settings/pages/Plugins/components/PluginCard";
import { VdPluginManager } from "@core/vendetta/plugins";
import { useProxy } from "@core/vendetta/storage";
import { isCorePlugin, isPluginInstalled, pluginSettings, registeredPlugins } from "@lib/addons/plugins";
import { Author } from "@lib/addons/types";
import { findAssetId } from "@lib/api/assets";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { useObservable } from "@lib/api/storage";
import { showToast } from "@lib/ui/toasts";
import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { lazyDestructure } from "@lib/utils/lazy";
import { Author } from "@lib/utils/types";
import { findByProps } from "@metro";
import { NavigationNative } from "@metro/common";
import { Button, Card, FlashList, Text } from "@metro/common/components";
import { Card, FlashList, IconButton, Text } from "@metro/common/components";
import { ComponentProps } from "react";
import { View } from "react-native";
import unifyBunnyPlugin from "./models/bunny";
import unifyVdPlugin from "./models/vendetta";
export interface UnifiedPluginModel {
id: string;
name: string;
description?: string;
authors?: Array<Author | string>;
authors?: Author[];
icon?: string;
isEnabled(): boolean;
@ -35,13 +38,6 @@ export interface UnifiedPluginModel {
const { openAlert } = lazyDestructure(() => findByProps("openAlert", "dismissAlert"));
const { AlertModal, AlertActions, AlertActionButton } = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
function navigateToPluginBrowser(navigation: any) {
navigation.push("BUNNY_CUSTOM_PAGE", {
title: "Plugin Browser",
render: React.lazy(() => import("../PluginBrowser")),
});
}
interface PluginPageProps extends Partial<ComponentProps<typeof AddonPage<UnifiedPluginModel>>> {
useItems: () => unknown[];
}
@ -74,40 +70,54 @@ export default function Plugins() {
const navigation = NavigationNative.useNavigation();
return <PluginPage
useItems={() => useProxy(VdPluginManager.plugins) && Object.values(VdPluginManager.plugins)}
resolveItem={unifyVdPlugin}
useItems={() => {
useProxy(VdPluginManager.plugins);
useObservable([pluginSettings]);
const vdPlugins = Object.values(VdPluginManager.plugins).map(unifyVdPlugin);
const bnPlugins = [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id) && !isCorePlugin(p.id)).map(unifyBunnyPlugin);
return [...vdPlugins, ...bnPlugins];
}}
ListHeaderComponent={() => {
const unproxiedPlugins = Object.values(VdPluginManager.plugins).filter(p => !p.id.startsWith(VD_PROXY_PREFIX) && !p.id.startsWith(BUNNY_PROXY_PREFIX));
if (!unproxiedPlugins.length) return null;
// TODO: Make this dismissable
return <Card style={{ marginVertical: 8, gap: 4 }}>
<Text variant="heading-lg/bold">Unproxied Plugins Detected</Text>
<Text variant="text-md/medium">Installed plugins from unproxied sources may execute unreviewed code in this app without your knowledge.</Text>
<View style={{ marginTop: 8, flexDirection: "row" }}>
<Button
style={{ flexShrink: 1 }}
size="sm"
text="Review"
variant="secondary"
onPress={() => {
navigation.push("BUNNY_CUSTOM_PAGE", {
title: "Unproxied Plugins",
render: () => {
return <FlashList
data={unproxiedPlugins}
contentContainerStyle={{ padding: 8 }}
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
renderItem={({ item: p }: any) => <Card>
<Text variant="heading-md/semibold">{p.id}</Text>
</Card>}
/>;
}
});
}}
/>
</View>
</Card>;
return <View style={{ marginVertical: 12, marginHorizontal: 10 }}>
<Card border="strong">
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", flexDirection: "row" }}>
<View style={{ gap: 6, flexShrink: 1 }}>
<Text variant="heading-md/bold">Unproxied Plugins Found</Text>
<Text variant="text-sm/medium" color="text-muted">
Plugins installed from unproxied sources may run unverified code in this app without your awareness.
</Text>
</View>
<View style={{ marginLeft: "auto" }}>
<IconButton
size="sm"
variant="secondary"
icon={findAssetId("CircleInformationIcon-primary")}
style={{ marginLeft: 8 }}
onPress={() => {
navigation.push("BUNNY_CUSTOM_PAGE", {
title: "Unproxied Plugins",
render: () => {
return <FlashList
data={unproxiedPlugins}
contentContainerStyle={{ padding: 8 }}
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
renderItem={({ item: p }: any) => <Card>
<Text variant="heading-md/semibold">{p.id}</Text>
</Card>}
/>;
}
});
}}
/>
</View>
</View>
</Card>
</View>;
}}
installAction={{
label: "Install a plugin",
@ -137,61 +147,4 @@ export default function Plugins() {
}
}}
/>;
// const navigation = NavigationNative.useNavigation();
// const { width: pageWidth } = useWindowDimensions();
// const state = useSegmentedControlState({
// pageWidth,
// items: [
// {
// label: "Vendetta",
// id: "vendetta-plugins",
// page: (
// <PluginPage
// useItems={() => useProxy(VdPluginManager.plugins) && Object.values(VdPluginManager.plugins)}
// resolveItem={unifyVdPlugin}
// fetchFunction={(url: string) => VdPluginManager.installPlugin(url)}
// />
// )
// },
// {
// label: "Bunny",
// id: "bunny-plugins",
// page: (
// <PluginPage
// useItems={() => (useNewProxy(pluginSettings), [...registeredPlugins.values()].filter(p => isPluginInstalled(p.id)))}
// resolveItem={unifyBunnyPlugin}
// ListHeaderComponent={() => (
// <View style={{ marginBottom: 10 }}>
// <HelpMessage messageType={0}>
// Bunny plugin system is in no way ready, try not getting yourself burnt ⚠️
// </HelpMessage>
// </View>
// )}
// ListFooterComponent={() => (
// <View style={{ alignItems: "center", justifyContent: "center", paddingTop: 16, gap: 12 }}>
// <Text variant="heading-lg/bold">{"Looking for more?"}</Text>
// <Button
// size="lg"
// text="Browse plugins"
// icon={findAssetId("discover")}
// onPress={() => navigateToPluginBrowser(navigation)}
// />
// </View>
// )}
// />
// )
// },
// ]
// });
// return (
// <View style={{ alignItems: "center", justifyContent: "center", height: "100%" }}>
// <View style={{ padding: 8, paddingBottom: 0 }}>
// <SegmentedControl state={state} />
// </View>
// <SegmentedControlPages state={state} />
// </View>
// );
}

View file

@ -1,6 +1,6 @@
import { useProxy } from "@lib/api/storage/new";
import { disablePlugin, enablePlugin, getId, getPluginSettingsComponent,isPluginEnabled, pluginSettings } from "@lib/plugins";
import { BunnyPluginManifest } from "@lib/plugins/types";
import { disablePlugin, enablePlugin, getId, getPluginSettingsComponent, isPluginEnabled, pluginSettings } from "@lib/addons/plugins";
import { BunnyPluginManifest } from "@lib/addons/plugins/types";
import { useObservable } from "@lib/api/storage";
import { UnifiedPluginModel } from "..";
@ -13,7 +13,7 @@ export default function unifyBunnyPlugin(manifest: BunnyPluginManifest): Unified
isEnabled: () => isPluginEnabled(getId(manifest)),
isInstalled: () => manifest.id in pluginSettings,
usePluginState() {
useProxy(pluginSettings);
useObservable([pluginSettings]);
},
toggle(start: boolean) {
start

View file

@ -1,5 +1,5 @@
import { VdPluginManager, VendettaPlugin } from "@core/vendetta/plugins";
import { useProxy } from "@lib/api/storage";
import { useProxy } from "@core/vendetta/storage";
import { UnifiedPluginModel } from "..";

View file

@ -9,11 +9,16 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
plugin.usePluginState();
return <ActionSheet>
<ScrollView>
<ScrollView contentContainerStyle={{ gap: 8, marginBottom: 12 }}>
<View style={{ flexDirection: "row", alignItems: "center", paddingVertical: 24 }}>
<Text variant="heading-xl/semibold">
{plugin.name}
</Text>
<View style={{ gap: 4 }}>
<Text variant="heading-xl/semibold">
{plugin.name}
</Text>
<Text variant="text-md/medium" color="text-muted">
{plugin.description}
</Text>
</View>
<View style={{ marginLeft: "auto" }}>
{plugin.getPluginSettingsComponent() && <Button
size="md"
@ -30,6 +35,11 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
/>}
</View>
</View>
<View style={{ flexDirection: "row", justifyContent: "center", alignContent: "center" }}>
<Text variant="text-lg/medium">
Oops, you shouldn't see this!
</Text>
</View>
</ScrollView>
</ActionSheet>;
}

View file

@ -1,11 +1,10 @@
import { formatString, Strings } from "@core/i18n";
import { showConfirmationAlert } from "@core/vendetta/alerts";
import { VdPluginManager } from "@core/vendetta/plugins";
import { purgeStorage } from "@core/vendetta/storage";
import { findAssetId } from "@lib/api/assets";
import { purgeStorage } from "@lib/api/storage";
import { ButtonColors } from "@lib/utils/types";
import { clipboard } from "@metro/common";
import { ActionSheet, ActionSheetRow, Button, TableRow, Text } from "@metro/common/components";
import { showConfirmationAlert } from "@ui/alerts";
import { hideSheet } from "@ui/sheets";
import { showToast } from "@ui/toasts";
import { ScrollView, View } from "react-native";
@ -85,7 +84,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
content: formatString("ARE_YOU_SURE_TO_CLEAR_DATA", { name: plugin.name }),
confirmText: Strings.CLEAR,
cancelText: Strings.CANCEL,
confirmColor: ButtonColors.RED,
confirmColor: "red",
onConfirm: async () => {
if (vdPlugin.enabled) VdPluginManager.stopPlugin(plugin.id, false);
@ -122,7 +121,7 @@ export default function PluginInfoActionSheet({ plugin, navigation }: PluginInfo
content: formatString("ARE_YOU_SURE_TO_DELETE_PLUGIN", { name: plugin.name }),
confirmText: Strings.DELETE,
cancelText: Strings.CANCEL,
confirmColor: ButtonColors.RED,
confirmColor: "red",
onConfirm: () => {
try {
VdPluginManager.removePlugin(plugin.id);

View file

@ -1,12 +1,11 @@
import { formatString, Strings } from "@core/i18n";
import AddonCard, { CardWrapper } from "@core/ui/components/AddonCard";
import { showConfirmationAlert } from "@core/vendetta/alerts";
import { useProxy } from "@core/vendetta/storage";
import { applyTheme, fetchTheme, removeTheme, selectTheme, Theme, themes } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { applyTheme, fetchTheme, removeTheme, selectTheme, Theme, themes } from "@lib/themes";
import { ButtonColors } from "@lib/utils/types";
import { clipboard } from "@metro/common";
import { showConfirmationAlert } from "@ui/alerts";
import { showToast } from "@ui/toasts";
function selectAndApply(value: boolean, theme: Theme) {
@ -68,7 +67,7 @@ export default function ThemeCard({ item: theme }: CardWrapper<Theme>) {
content: formatString("ARE_YOU_SURE_TO_DELETE_THEME", { name: theme.data.name }),
confirmText: Strings.DELETE,
cancelText: Strings.CANCEL,
confirmColor: ButtonColors.RED,
confirmColor: "red",
onConfirm: () => {
removeTheme(theme.id).then(wasSelected => {
setRemoved(true);

View file

@ -1,10 +1,10 @@
import { formatString, Strings } from "@core/i18n";
import AddonPage from "@core/ui/components/AddonPage";
import ThemeCard from "@core/ui/settings/pages/Themes/ThemeCard";
import { useProxy } from "@core/vendetta/storage";
import { installTheme, Theme, themes } from "@lib/addons/themes";
import { Author } from "@lib/addons/types";
import { settings } from "@lib/api/settings";
import { useProxy } from "@lib/api/storage";
import { installTheme, Theme, themes } from "@lib/themes";
import { Author } from "@lib/utils/types";
import { Button } from "@metro/common/components";
export default function Themes() {

View file

@ -8,8 +8,7 @@ export type EmitterEvent = "SET" | "GET" | "DEL";
export interface EmitterListenerData {
path: string[];
value: any;
parent: any;
value?: any;
}
export type EmitterListener = (
@ -17,7 +16,7 @@ export type EmitterListener = (
data: EmitterListenerData | any
) => any;
export type EmitterListeners = Record<string, Set<EmitterListener>>
export type EmitterListeners = Record<string, Set<EmitterListener>>;
export class Emitter {
listeners = Object.values(Events).reduce<EmitterListeners>(

View file

@ -1,4 +1,3 @@
import { ButtonColors } from "@lib/utils/types";
import { findByPropsLazy } from "@metro/wrappers";
import InputAlert, { InputAlertProps } from "@ui/components/InputAlert";
@ -13,7 +12,7 @@ export interface ConfirmationAlertOptions {
title?: string;
content: string | JSX.Element | (string | JSX.Element)[];
confirmText?: string;
confirmColor?: ButtonColors;
confirmColor?: string;
onConfirm: () => void;
secondaryConfirmText?: string;
onConfirmSecondary?: () => void;

View file

@ -1,20 +1,20 @@
import * as alerts from "@core/vendetta/alerts";
import * as storage from "@core/vendetta/storage";
import { createStorage } from "@core/vendetta/storage";
import * as themes from "@lib/addons/themes";
import * as assets from "@lib/api/assets";
import * as commands from "@lib/api/commands";
import * as debug from "@lib/api/debug";
import { getVendettaLoaderIdentity, isPyonLoader } from "@lib/api/native/loader";
import patcher from "@lib/api/patcher";
import { loaderConfig, settings } from "@lib/api/settings";
import * as storage from "@lib/api/storage";
import { createStorage } from "@lib/api/storage";
import * as themes from "@lib/themes";
import * as utils from "@lib/utils";
import { cyrb64Hash } from "@lib/utils/cyrb64";
import { DiscordLogger } from "@lib/utils/logger";
import { LoggerClass } from "@lib/utils/logger";
import * as metro from "@metro";
import * as common from "@metro/common";
import { Forms } from "@metro/common/components";
import * as commonComponents from "@metro/common/components";
import * as alerts from "@ui/alerts";
import * as color from "@ui/color";
import * as components from "@ui/components";
import { createThemedStyleSheet } from "@ui/styles";
@ -34,7 +34,7 @@ export async function createVdPluginObject(plugin: VendettaPlugin) {
// Wrapping this with wrapSync is NOT an option.
storage: await createStorage<Record<string, any>>(storage.createMMKVBackend(plugin.id)),
},
logger: new DiscordLogger(`Bunny » ${plugin.manifest.name}`),
logger: new LoggerClass(`Revenge » ${plugin.manifest.name}`),
};
}

View file

@ -1,9 +1,9 @@
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@core/vendetta/storage";
import { Author } from "@lib/addons/types";
import { settings } from "@lib/api/settings";
import { awaitStorage, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/api/storage";
import { safeFetch } from "@lib/utils";
import { BUNNY_PROXY_PREFIX, VD_PROXY_PREFIX } from "@lib/utils/constants";
import { DiscordLogger, logger } from "@lib/utils/logger";
import { Author } from "@lib/utils/types";
import { logger,LoggerClass } from "@lib/utils/logger";
type EvaledPlugin = {
onLoad?(): void;
@ -39,7 +39,9 @@ export const VdPluginManager = {
plugins,
async pluginFetch(url: string) {
if (url.startsWith(VD_PROXY_PREFIX)) {
url = url.replace(VD_PROXY_PREFIX, BUNNY_PROXY_PREFIX);
url = url
.replace("https://bunny-mod.github.io/plugins-proxy", BUNNY_PROXY_PREFIX)
.replace(VD_PROXY_PREFIX, BUNNY_PROXY_PREFIX);
}
return await safeFetch(url, { cache: "no-store" });
@ -61,7 +63,7 @@ export const VdPluginManager = {
if (existingPlugin?.manifest.hash !== pluginManifest.hash) {
try {
// by polymanifest spec, plugins should always specify their main file, but just in case
// by polymanifest spec, plugins should always specify their main file, but just in case
pluginJs = await (await this.pluginFetch(id + (pluginManifest.main || "index.js"))).text();
} catch { } // Empty catch, checked below
}
@ -96,7 +98,7 @@ export const VdPluginManager = {
// Wrapping this with wrapSync is NOT an option.
storage: await createStorage<Record<string, any>>(createMMKVBackend(plugin.id)),
},
logger: new DiscordLogger(`Bunny » ${plugin.manifest.name}`),
logger: new LoggerClass(`Revenge » ${plugin.manifest.name}`),
};
const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.id}`;
@ -166,7 +168,7 @@ export const VdPluginManager = {
const allIds = Object.keys(plugins);
if (!settings.safeMode?.enabled) {
// Loop over any plugin that is enabled, update it if allowed, then start it.
// Loop over any plugin that is enabled, update it if allowed, then start it.
await Promise.allSettled(allIds.filter(pl => plugins[pl].enabled).map(async pl => (plugins[pl].update && await this.fetchPlugin(pl).catch((e: Error) => logger.error(e.message)), await this.startPlugin(pl))));
// Wait for the above to finish, then update all disabled plugins that are allowed to.
allIds.filter(pl => !plugins[pl].enabled && plugins[pl].update).forEach(pl => this.fetchPlugin(pl));

View file

@ -0,0 +1,238 @@
import { Emitter, EmitterEvent, EmitterListener, EmitterListenerData } from "@core/vendetta/Emitter";
import { FileManager, MMKVManager } from "@lib/api/native/modules";
import { Platform } from "react-native";
const emitterSymbol = Symbol.for("vendetta.storage.emitter");
const syncAwaitSymbol = Symbol.for("vendetta.storage.accessor");
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
const emitter = new Emitter();
const childrens = new WeakMap<any, any>();
const proxiedChildrenSet = new WeakSet<any>();
function createProxy(target: any, path: string[]): any {
return new Proxy(target, {
get(target, prop: string) {
if ((prop as unknown) === emitterSymbol) return emitter;
const newPath = [...path, prop];
const value: any = target[prop];
if (value !== undefined && value !== null) {
emitter.emit("GET", {
path: newPath,
value,
});
if (typeof value === "object") {
if (proxiedChildrenSet.has(value)) return value;
if (childrens.has(value)) return childrens.get(value);
const childrenProxy = createProxy(value, newPath);
childrens.set(value, childrenProxy);
return childrenProxy;
}
return value;
}
return value;
},
set(target, prop: string, value) {
if (typeof value === "object") {
if (childrens.has(value)) {
target[prop] = childrens.get(value);
} else {
const childrenProxy = createProxy(value, [...path, prop]);
childrens.set(value, childrenProxy);
proxiedChildrenSet.add(value);
target[prop] = childrenProxy;
}
} else {
target[prop] = value;
}
emitter.emit("SET", {
path: [...path, prop],
value: target[prop],
});
// we do not care about success, if this actually does fail we have other problems
return true;
},
deleteProperty(target, prop: string) {
const value = typeof target[prop] === "object" ? childrens.get(target[prop])! : target[prop];
const success = delete target[prop];
if (success)
emitter.emit("DEL", {
value,
path: [...path, prop],
});
return success;
},
});
}
return {
proxy: createProxy(target, []),
emitter,
};
}
export function useProxy<T>(storage: T): T {
const emitter = (storage as any)?.[emitterSymbol] as Emitter;
if (!emitter) throw new Error("storage?.[emitterSymbol] is undefined");
const [, forceUpdate] = React.useReducer(n => ~n, 0);
React.useEffect(() => {
const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => {
if (event === "DEL" && data.value === storage) return;
forceUpdate();
};
emitter.on("SET", listener);
emitter.on("DEL", listener);
return () => {
emitter.off("SET", listener);
emitter.off("DEL", listener);
};
}, []);
return storage;
}
export async function createStorage<T>(backend: StorageBackend): Promise<Awaited<T>> {
const data = await backend.get();
const { proxy, emitter } = createProxy(data);
const handler = () => backend.set(proxy);
emitter.on("SET", handler);
emitter.on("DEL", handler);
return proxy;
}
export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
let awaited: any = undefined;
const awaitQueue: (() => void)[] = [];
const awaitInit = (cb: () => void) => (awaited ? cb() : awaitQueue.push(cb));
store.then(v => {
awaited = v;
awaitQueue.forEach(cb => cb());
});
return new Proxy({} as Awaited<T>, {
...Object.fromEntries(
Object.getOwnPropertyNames(Reflect)
// @ts-expect-error
.map(k => [k, (t: T, ...a: any[]) => Reflect[k](awaited ?? t, ...a)])
),
get(target, prop, recv) {
if (prop === syncAwaitSymbol) return awaitInit;
return Reflect.get(awaited ?? target, prop, recv);
},
});
}
export function awaitStorage(...stores: any[]) {
return Promise.all(
stores.map(store => new Promise<void>(res => store[syncAwaitSymbol](res)))
);
}
export interface StorageBackend {
get: () => unknown | Promise<unknown>;
set: (data: unknown) => void | Promise<void>;
}
const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g;
const filePathFixer = (file: string): string => Platform.select({
default: file,
ios: FileManager.saveFileToGallery ? file : `Documents/${file}`,
});
const getMMKVPath = (name: string): string => {
if (ILLEGAL_CHARS_REGEX.test(name)) {
// Replace forbidden characters with hyphens
name = name.replace(ILLEGAL_CHARS_REGEX, "-").replace(/-+/g, "-");
}
return `vd_mmkv/${name}`;
};
export const purgeStorage = async (store: string) => {
if (await MMKVManager.getItem(store)) {
MMKVManager.removeItem(store);
}
const mmkvPath = getMMKVPath(store);
if (await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`)) {
await FileManager.removeFile?.("documents", mmkvPath);
}
};
export const createMMKVBackend = (store: string, defaultData = {}) => {
const mmkvPath = getMMKVPath(store);
const defaultStr = JSON.stringify(defaultData);
return createFileBackend(mmkvPath, defaultData, (async () => {
const path = `${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`;
if (await FileManager.fileExists(path)) return;
let oldData = await MMKVManager.getItem(store) ?? defaultStr;
// From the testing on Android, it seems to return this if the data is too large
if (oldData === "!!LARGE_VALUE!!") {
const cachePath = `${FileManager.getConstants().CacheDirPath}/mmkv/${store}`;
if (await FileManager.fileExists(cachePath)) {
oldData = await FileManager.readFile(cachePath, "utf8");
} else {
console.log(`${store}: Experienced data loss :(`);
oldData = defaultStr;
}
}
try {
JSON.parse(oldData);
} catch {
console.error(`${store} had an unparseable data while migrating`);
oldData = defaultStr;
}
await FileManager.writeFile("documents", filePathFixer(mmkvPath), oldData, "utf8");
if (await MMKVManager.getItem(store) !== null) {
MMKVManager.removeItem(store);
console.log(`Successfully migrated ${store} store from MMKV storage to fs`);
}
})());
};
export const createFileBackend = (file: string, defaultData = {}, migratePromise?: Promise<void>): StorageBackend => {
return {
get: async () => {
await migratePromise;
const path = `${FileManager.getConstants().DocumentsDirPath}/${file}`;
if (await FileManager.fileExists(path)) {
const content = await FileManager.readFile(path, "utf8");
try {
return JSON.parse(content);
} catch {
// Corrupted content, ignore
}
}
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(defaultData), "utf8");
return JSON.parse(await FileManager.readFile(path, "utf8"));
},
set: async data => {
await migratePromise;
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8");
}
};
};

View file

@ -17,9 +17,9 @@ async function initializeBunny() {
console.log(stack ?? e?.toString?.() ?? e);
alert([
"Failed to load Bunny!\n",
"Failed to load Revenge!\n",
`Build Number: ${ClientInfoManager.Build}`,
`Bunny: ${version}`,
`Revenge: ${version}`,
stack || e?.toString?.(),
].join("\n"));
}

View file

@ -3,37 +3,36 @@ import { initFetchI18nStrings } from "@core/i18n";
import initSettings from "@core/ui/settings";
import { initVendettaObject } from "@core/vendetta/api";
import { VdPluginManager } from "@core/vendetta/plugins";
import { updateFonts } from "@lib/addons/fonts";
import { initPlugins, updatePlugins } from "@lib/addons/plugins";
import { initThemes, patchChatBackground } from "@lib/addons/themes";
import { patchCommands } from "@lib/api/commands";
import { patchLogHook } from "@lib/api/debug";
import { injectFluxInterceptor } from "@lib/api/flux";
import { removeFile, writeFile } from "@lib/api/native/fs";
import { writeFile } from "@lib/api/native/fs";
import { isPyonLoader, isThemeSupported } from "@lib/api/native/loader";
import { FileManager } from "@lib/api/native/modules";
import { updateFonts } from "@lib/fonts";
import { initPlugins } from "@lib/plugins";
import { initThemes, patchChatBackground } from "@lib/themes";
import { patchJsx } from "@lib/api/react/jsx";
import { logger } from "@lib/utils/logger";
import initSafeMode from "@ui/safeMode";
import { patchSettings } from "@ui/settings";
import * as lib from "./lib";
export default async () => {
// Themes
if (isThemeSupported()) {
try {
if (isPyonLoader()) {
if (FileManager.removeFile != null) {
removeFile("vendetta_theme.json", "");
} else {
writeFile("vendetta_theme.json", "null", "");
}
}
initThemes();
} catch (e) {
console.error("[Bunny] Failed to initialize themes...", e);
function maybeLoadThemes() {
if (!isThemeSupported()) return;
try {
if (isPyonLoader()) {
writeFile("../vendetta_theme.json", "null");
}
initThemes();
} catch (e) {
console.error("Failed to initialize themes", e);
}
}
export default async () => {
maybeLoadThemes();
// Load everything in parallel
await Promise.all([
@ -42,11 +41,13 @@ export default async () => {
patchLogHook(),
patchCommands(),
patchChatBackground(),
patchJsx(),
initVendettaObject(),
initFetchI18nStrings(),
initSettings(),
initFixes(),
initSafeMode()
initSafeMode(),
updatePlugins()
]).then(
// Push them all to unloader
u => u.forEach(f => f && lib.unload.push(f))
@ -60,11 +61,12 @@ export default async () => {
.then(u => lib.unload.push(u))
.catch(() => alert("Failed to initialize Vendetta plugins"));
// And then, load Bunny plugins
initPlugins();
// Update the fonts
updateFonts();
// We good :)
logger.log("Bunny is ready!");
logger.log("Revenge is ready!");
};

View file

@ -1,5 +1,5 @@
import { awaitStorage, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
import { clearFolder, downloadFile, fileExists, removeFile, writeFile } from "@lib/api/native/fs";
import { awaitStorage, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
import { safeFetch } from "@lib/utils";
type FontMap = Record<string, string>;

View file

@ -1,6 +1,6 @@
import { patcher } from "@lib/api";
import { registerCommand } from "@lib/api/commands";
import { createStorage } from "@lib/api/storage/new";
import { createStorage } from "@lib/api/storage";
import { logger } from "@lib/utils/logger";
import { registeredPlugins } from ".";
@ -39,7 +39,7 @@ export function createBunnyPluginAPI(id: string) {
},
// Added something in here? Make sure to also update BunnyPluginProperty in ./types
plugin: {
createStorage: () => createStorage(`plugins/storage/${id}.json`),
createStorage: <T extends object = any>() => createStorage<T>(`plugins/storage/${id}.json`),
manifest: registeredPlugins.get(id),
logger
}

View file

@ -1,6 +1,6 @@
import { getCorePlugins } from "@core/plugins";
import { readFile, removeFile, writeFile } from "@lib/api/native/fs";
import { awaitStorage, createStorage, getPreloadedStorage, preloadStorageIfExists, updateStorageAsync } from "@lib/api/storage/new";
import { awaitStorage, createStorage, getPreloadedStorage, preloadStorageIfExists, updateStorage } from "@lib/api/storage";
import { safeFetch } from "@lib/utils";
import { OFFICIAL_PLUGINS_REPO_URL } from "@lib/utils/constants";
import { semver } from "@metro/common";
@ -51,6 +51,10 @@ function isExternalPlugin(manifest: t.BunnyPluginManifest): manifest is t.BunnyP
return "parentRepository" in manifest;
}
export function isCorePlugin(id: string) {
return corePluginInstances.has(id);
}
export function getId<T extends t.BunnyPluginManifest>(manifest: T): string {
const id = manifestToId.get(manifest);
assert(id, manifest?.name ?? "unknown", "getting ID from an unregistered/invalid manifest");
@ -93,7 +97,7 @@ export async function updateAndWritePlugin(repoUrl: string, id: string, fetchScr
await writeFile(manifest.jsPath, js);
}
await updateStorageAsync(`plugins/manifests/${id}.json`, manifest);
await updateStorage(`plugins/manifests/${id}.json`, manifest);
if (registeredPlugins.has(id)) {
const existingManifest = registeredPlugins.get(id)!;
@ -347,9 +351,24 @@ export function stopPlugin(id: string) {
pluginInstances.delete(id);
}
export async function checkAndRegisterUpdates() {
async function updateAllRepository() {
try {
await updateRepository(OFFICIAL_PLUGINS_REPO_URL);
} catch (error) {
console.error("Failed to update official plugins repository", error);
}
await Promise.allSettled(Object.keys(pluginRepositories).map(async repo => {
if (repo !== OFFICIAL_PLUGINS_REPO_URL) {
await updateRepository(repo);
}
}));
}
export async function updatePlugins() {
await awaitStorage(pluginRepositories, pluginSettings);
// Register core plugins
const corePlugins = getCorePlugins();
for (const id in corePlugins) {
const {
@ -367,12 +386,7 @@ export async function checkAndRegisterUpdates() {
corePluginInstances.set(id, instance);
}
// await updateRepository(OFFICIAL_PLUGINS_REPO_URL);
// await Promise.allSettled(Object.keys(pluginRepositories).map(async repo => {
// if (repo !== OFFICIAL_PLUGINS_REPO_URL) {
// await updateRepository(repo);
// }
// }));
await updateAllRepository();
}
/**

View file

@ -1,6 +1,6 @@
import { createStorage } from "@lib/api/storage";
import { createStorage } from "@core/vendetta/storage";
import { Author } from "@lib/addons/types";
import { Logger } from "@lib/utils/logger";
import { Author } from "@lib/utils/types";
export interface PluginRepo {
[id: string]: {
@ -26,7 +26,7 @@ export interface BunnyPluginManifest {
readonly name: string;
readonly description: string;
readonly version: string;
readonly authors: (Author | string)[];
readonly authors: Author[];
}
export interface BunnyPluginManifestInternal extends BunnyPluginManifest {

View file

@ -2,13 +2,13 @@
* Theming system in Bunny is currently a prototype, expect an unreadable theme implementation below
*/
import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
import { Author } from "@lib/addons/types";
import { getStoredTheme, getThemeFilePath } from "@lib/api/native/loader";
import { ThemeManager } from "@lib/api/native/modules";
import { after, before, instead } from "@lib/api/patcher";
import { awaitStorage, createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
import { findInReactTree, safeFetch } from "@lib/utils";
import { lazyDestructure, proxyLazy } from "@lib/utils/lazy";
import { Author } from "@lib/utils/types";
import { byMutableProp } from "@metro/filters";
import { createLazyModule } from "@metro/lazy";
import { findByNameLazy, findByProps, findByPropsLazy, findByStoreNameLazy } from "@metro/wrappers";

4
src/lib/addons/types.ts Normal file
View file

@ -0,0 +1,4 @@
export interface Author {
name: string;
id: string;
}

View file

@ -4,6 +4,7 @@ import { getImportingModuleId, requireModule } from "@metro/internals/modules";
// TODO: Deprecate this map, make another that maps to an array of assets (Asset[]) instead
/**
* @internal
* Pitfall: Multiple assets may have the same name, this is fine if we require the asset only for display,\
* but not when used to get the registered id/index. In some condition, this would break some plugins like HideGiftButton that gets id by name for patching.\
*/

View file

@ -1,14 +1,13 @@
import { getThemeFromLoader, selectTheme, themes } from "@lib/addons/themes";
import { findAssetId } from "@lib/api/assets";
import { getLoaderName, getLoaderVersion, isThemeSupported } from "@lib/api/native/loader";
import { BundleUpdaterManager, ClientInfoManager, DeviceManager } from "@lib/api/native/modules";
import { after } from "@lib/api/patcher";
import { settings } from "@lib/api/settings";
import { getThemeFromLoader, selectTheme, themes } from "@lib/themes";
import { logger } from "@lib/utils/logger";
import { showToast } from "@ui/toasts";
import { version } from "bunny-build-info";
import { Platform, type PlatformConstants } from "react-native";
export let socket: WebSocket;
export interface RNConstants extends PlatformConstants {
// Android
@ -44,6 +43,7 @@ export async function toggleSafeMode() {
setTimeout(BundleUpdaterManager.reload, 400);
}
let socket: WebSocket;
export function connectToDebugger(url: string) {
if (socket !== undefined && socket.readyState !== WebSocket.CLOSED) socket.close();

View file

@ -4,5 +4,6 @@ export * as debug from "./debug";
export * as flux from "./flux";
export * as native from "./native";
export * as patcher from "./patcher";
export * as react from "./react";
export * as settings from "./settings";
export * as storage from "./storage";

View file

@ -4,7 +4,7 @@ import { FileManager } from "./modules";
* Removes all files in a directory from the path given
* @param path Path to the targeted directory
*/
export async function clearFolder(path: string, prefix = "pyoncord/") {
export async function clearFolder(path: string, { prefix = "pyoncord/" } = {}) {
if (typeof FileManager.clearFolder !== "function") throw new Error("'fs.clearFolder' is not supported");
return void await FileManager.clearFolder("documents", `${prefix}${path}`);
}
@ -13,7 +13,7 @@ export async function clearFolder(path: string, prefix = "pyoncord/") {
* Remove file from given path, currently no check for any failure
* @param path Path to the file
*/
export async function removeFile(path: string, prefix = "pyoncord/") {
export async function removeFile(path: string, { prefix = "pyoncord/" } = {}) {
if (typeof FileManager.removeFile !== "function") throw new Error("'fs.removeFile' is not supported");
return void await FileManager.removeFile("documents", `${prefix}${path}`);
}
@ -31,7 +31,7 @@ export async function removeCacheFile(path: string, prefix = "pyoncord/") {
* Check if the file or directory given by the path exists
* @param path Path to the file
*/
export async function fileExists(path: string, prefix = "pyoncord/") {
export async function fileExists(path: string, { prefix = "pyoncord/" } = {}) {
return await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${prefix}${path}`);
}
@ -40,7 +40,7 @@ export async function fileExists(path: string, prefix = "pyoncord/") {
* @param path Path to the file
* @param data String data to write to the file
*/
export async function writeFile(path: string, data: string, prefix = "pyoncord/"): Promise<void> {
export async function writeFile(path: string, data: string, { prefix = "pyoncord/" } = {}): Promise<void> {
if (typeof data !== "string") throw new Error("Argument 'data' must be a string");
return void await FileManager.writeFile("documents", `${prefix}${path}`, data, "utf8");
}
@ -50,7 +50,7 @@ export async function writeFile(path: string, data: string, prefix = "pyoncord/"
* @param path Path to the file
* @param fallback Fallback data to return if the file doesn't exist, and will be written to the file
*/
export async function readFile(path: string, prefix = "pyoncord/"): Promise<string> {
export async function readFile(path: string, { prefix = "pyoncord/" } = {}): Promise<string> {
try {
return await FileManager.readFile(`${FileManager.getConstants().DocumentsDirPath}/${prefix}${path}`, "utf8");
} catch (err) {
@ -58,30 +58,19 @@ export async function readFile(path: string, prefix = "pyoncord/"): Promise<stri
}
}
export async function downloadFile(url: string, path: string, prefix = "pyoncord/") {
const blob = await fetch(url).then(r => r.blob());
const dataURL: string | null = await new Promise(r => {
const reader = new FileReader();
reader.onload = () => r(reader.result as unknown as string);
reader.readAsDataURL(blob);
});
let data: string;
if (dataURL == null) {
throw new Error("Failed to convert blob to data URL");
} else {
const index = dataURL.indexOf("base64,");
if (index === -1) throw new Error("dataURL does not contain base64");
data = dataURL.slice(index + 7);
/**
* Download a file from the given URL and save it to the path given
* @param url URL to download the file from
* @param path Path to save the file to
*/
export async function downloadFile(url: string, path: string, { prefix = "pyoncord/" } = {}) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download file from ${url}: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
const data = Buffer.from(arrayBuffer).toString("base64");
return void await FileManager.writeFile("documents", `${prefix}${path}`, data, "base64");
}
// // thanks Rosie, doesnt work though
// export async function downloadFile(url: string, path: string, prefix = "pyoncord/") {
// const arrayBuffer = await fetch(url).then(r => r.arrayBuffer());
// const data = Buffer.from(arrayBuffer).toString("base64");
// return void await FileManager.writeFile("documents", `${prefix}${path}`, data, "base64");
// }

View file

@ -1,3 +1 @@
export * as fs from "./fs";
export * as loader from "./loader";
export * as modules from "./modules";

View file

@ -1,4 +1,4 @@
import { Theme } from "@lib/themes";
import { Theme } from "@lib/addons/themes";
import { removeCacheFile } from "./fs";
// @ts-ignore
@ -59,7 +59,7 @@ function polyfillVendettaLoaderIdentity() {
const id = getStoredTheme()?.id;
if (!id) return null;
const { themes } = require("@lib/themes");
const { themes } = require("@lib/addons/themes");
return themes[id] ?? getStoredTheme() ?? null;
},
configurable: true

View file

@ -1,97 +0,0 @@
const nmp = window.nativeModuleProxy;
/**
* A key-value storage based upon `SharedPreferences` on Android.
*
* These types are based on Android though everything should be the same between
* platforms.
*/
export interface MMKVManager {
/**
* Get the value for the given `key`, or null
* @param key The key to fetch
*/
getItem: (key: string) => Promise<string | null>;
/**
* Deletes the value for the given `key`
* @param key The key to delete
*/
removeItem: (key: string) => void;
/**
* Sets the value of `key` to `value`
*/
setItem: (key: string, value: string) => void;
/**
* Goes through every item in storage and returns it, excluding the
* keys specified in `exclude`.
* @param exclude A list of items to exclude from result
*/
refresh: (exclude: string[]) => Promise<Record<string, string>>;
/**
* You will be murdered if you use this function.
* Clears ALL of Discord's settings.
*/
clear: () => void;
}
export interface FileManager {
/**
* @param path **Full** path to file
*/
fileExists: (path: string) => Promise<boolean>;
/**
* Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3))
*/
getSize: (uri: string) => Promise<boolean>;
/**
* @param path **Full** path to file
* @param encoding Set to `base64` in order to encode response
*/
readFile(path: string, encoding: "base64" | "utf8"): Promise<string>;
saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise<string>;
/**
* @param storageDir Either `cache` or `documents`.
* @param path Path in `storageDir`, parents are recursively created.
* @param data The data to write to the file
* @param encoding Set to `base64` if `data` is base64 encoded.
* @returns Promise that resolves to path of the file once it got written
*/
writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise<string>;
/**
* Removes a file from the path given.
* (!) On Android, this always returns false, regardless if it fails or not!
* @param storageDir Either `cache` or `documents`
* @param path Path to the file to be removed
*/
removeFile(storageDir: "cache" | "documents", path: string): Promise<unknown>;
/**
* Clear the folder from the path given
* (!) On Android, this only clears all *files* and not subdirectories!
* @param storageDir Either `cache` or `documents`
* @param path Path to the folder to be cleared
* @returns Whether the clearance succeeded
*/
clearFolder(storageDir: "cache" | "documents", path: string): Promise<boolean>;
getConstants: () => {
/**
* The path the `documents` storage dir (see {@link writeFile}) represents.
*/
DocumentsDirPath: string;
CacheDirPath: string;
};
/**
* Will apparently cease to exist some time in the future so please use {@link getConstants} instead.
* @deprecated
*/
DocumentsDirPath: string;
}
export const MMKVManager = nmp.MMKVManager as MMKVManager;
//! 173.10 renamed this to RTNFileManager.
export const FileManager = (nmp.DCDFileManager ?? nmp.RTNFileManager) as FileManager;
//! 173.13 renamed this to RTNClientInfoManager.
export const ClientInfoManager = nmp.InfoDictionaryManager ?? nmp.RTNClientInfoManager;
//! 173.14 renamed this to RTNDeviceManager.
export const DeviceManager = nmp.DCDDeviceManager ?? nmp.RTNDeviceManager;
export const { BundleUpdaterManager } = nmp;
export const ThemeManager = nmp.RTNThemeManager ?? nmp.DCDTheme;

View file

@ -0,0 +1,11 @@
import { RNModules } from "./types";
const nmp = window.nativeModuleProxy;
export const MMKVManager = nmp.MMKVManager as RNModules.MMKVManager;
export const FileManager = (nmp.DCDFileManager ?? nmp.RTNFileManager) as RNModules.FileManager;
export const ClientInfoManager = nmp.InfoDictionaryManager ?? nmp.RTNClientInfoManager;
export const DeviceManager = nmp.DCDDeviceManager ?? nmp.RTNDeviceManager;
export const { BundleUpdaterManager } = nmp;
export const ThemeManager = nmp.RTNThemeManager ?? nmp.DCDTheme;

View file

@ -0,0 +1,89 @@
export namespace RNModules {
/**
* A key-value storage based upon `SharedPreferences` on Android.
*
* These types are based on Android though everything should be the same between
* platforms.
*/
export interface MMKVManager {
/**
* Get the value for the given `key`, or null
* @param key The key to fetch
*/
getItem: (key: string) => Promise<string | null>;
/**
* Deletes the value for the given `key`
* @param key The key to delete
*/
removeItem: (key: string) => void;
/**
* Sets the value of `key` to `value`
*/
setItem: (key: string, value: string) => void;
/**
* Goes through every item in storage and returns it, excluding the
* keys specified in `exclude`.
* @param exclude A list of items to exclude from result
*/
refresh: (exclude: string[]) => Promise<Record<string, string>>;
/**
* You will be murdered if you use this function.
* Clears ALL of Discord's settings.
*/
clear: () => void;
}
export interface FileManager {
/**
* @param path **Full** path to file
*/
fileExists: (path: string) => Promise<boolean>;
/**
* Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3))
*/
getSize: (uri: string) => Promise<boolean>;
/**
* @param path **Full** path to file
* @param encoding Set to `base64` in order to encode response
*/
readFile(path: string, encoding: "base64" | "utf8"): Promise<string>;
saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise<string>;
/**
* @param storageDir Either `cache` or `documents`.
* @param path Path in `storageDir`, parents are recursively created.
* @param data The data to write to the file
* @param encoding Set to `base64` if `data` is base64 encoded.
* @returns Promise that resolves to path of the file once it got written
*/
writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise<string>;
/**
* Removes a file from the path given.
* (!) On Android, this always returns false, regardless if it fails or not!
* @param storageDir Either `cache` or `documents`
* @param path Path to the file to be removed
*/
removeFile(storageDir: "cache" | "documents", path: string): Promise<unknown>;
/**
* Clear the folder from the path given
* (!) On Android, this only clears all *files* and not subdirectories!
* @param storageDir Either `cache` or `documents`
* @param path Path to the folder to be cleared
* @returns Whether the clearance succeeded
*/
clearFolder(storageDir: "cache" | "documents", path: string): Promise<boolean>;
getConstants: () => {
/**
* The path the `documents` storage dir (see {@link writeFile}) represents.
*/
DocumentsDirPath: string;
CacheDirPath: string;
};
/**
* Will apparently cease to exist some time in the future so please use {@link getConstants} instead.
* @deprecated
*/
DocumentsDirPath: string;
}
}

View file

@ -0,0 +1 @@
export * as jsx from "./jsx";

43
src/lib/api/react/jsx.ts Normal file
View file

@ -0,0 +1,43 @@
import { after } from "@lib/api/patcher";
import { findByPropsLazy } from "@metro";
type Callback = (Component: any, ret: JSX.Element) => JSX.Element;
const callbacks = new Map<string, Callback[]>();
const jsxRuntime = findByPropsLazy("jsx", "jsxs");
export function onJsxCreate(Component: string, callback: Callback) {
if (!callbacks.has(Component)) callbacks.set(Component, []);
callbacks.get(Component)!.push(callback);
}
export function deleteJsxCreate(Component: string, callback: Callback) {
if (!callbacks.has(Component)) return;
const cbs = callbacks.get(Component)!;
cbs.splice(cbs.indexOf(callback), 1);
if (cbs.length === 0) callbacks.delete(Component);
}
/**
* @internal
*/
export function patchJsx() {
const callback = ([Component]: unknown[], ret: JSX.Element) => {
// The check could be more complex, but this is fine for now to avoid overhead
if (typeof Component === "function" && callbacks.has(Component.name)) {
const cbs = callbacks.get(Component.name)!;
for (const cb of cbs) {
const _ret = cb(Component, ret);
if (_ret !== undefined) ret = _ret;
}
return ret;
}
};
const patches = [
after("jsx", jsxRuntime, callback),
after("jsxs", jsxRuntime, callback)
];
return () => patches.forEach(unpatch => unpatch());
}

View file

@ -1,5 +1,5 @@
import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@core/vendetta/storage";
import { getLoaderConfigPath } from "@lib/api/native/loader";
import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/api/storage";
export interface Settings {
debuggerUrl: string;

View file

@ -1,95 +0,0 @@
import { FileManager, MMKVManager } from "@lib/api/native/modules";
import { Platform } from "react-native";
export interface StorageBackend {
get: () => unknown | Promise<unknown>;
set: (data: unknown) => void | Promise<void>;
}
const ILLEGAL_CHARS_REGEX = /[<>:"/\\|?*]/g;
const filePathFixer = (file: string): string => Platform.select({
default: file,
ios: FileManager.saveFileToGallery ? file : `Documents/${file}`,
});
const getMMKVPath = (name: string): string => {
if (ILLEGAL_CHARS_REGEX.test(name)) {
// Replace forbidden characters with hyphens
name = name.replace(ILLEGAL_CHARS_REGEX, "-").replace(/-+/g, "-");
}
return `vd_mmkv/${name}`;
};
export const purgeStorage = async (store: string) => {
if (await MMKVManager.getItem(store)) {
MMKVManager.removeItem(store);
}
const mmkvPath = getMMKVPath(store);
if (await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`)) {
await FileManager.removeFile?.("documents", mmkvPath);
}
};
export const createMMKVBackend = (store: string, defaultData = {}) => {
const mmkvPath = getMMKVPath(store);
const defaultStr = JSON.stringify(defaultData);
return createFileBackend(mmkvPath, defaultData, (async () => {
const path = `${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`;
if (await FileManager.fileExists(path)) return;
let oldData = await MMKVManager.getItem(store) ?? defaultStr;
// From the testing on Android, it seems to return this if the data is too large
if (oldData === "!!LARGE_VALUE!!") {
const cachePath = `${FileManager.getConstants().CacheDirPath}/mmkv/${store}`;
if (await FileManager.fileExists(cachePath)) {
oldData = await FileManager.readFile(cachePath, "utf8");
} else {
console.log(`${store}: Experienced data loss :(`);
oldData = defaultStr;
}
}
try {
JSON.parse(oldData);
} catch {
console.error(`${store} had an unparseable data while migrating`);
oldData = defaultStr;
}
await FileManager.writeFile("documents", filePathFixer(mmkvPath), oldData, "utf8");
if (await MMKVManager.getItem(store) !== null) {
MMKVManager.removeItem(store);
console.log(`Successfully migrated ${store} store from MMKV storage to fs`);
}
})());
};
export const createFileBackend = (file: string, defaultData = {}, migratePromise?: Promise<void>): StorageBackend => {
return {
get: async () => {
await migratePromise;
const path = `${FileManager.getConstants().DocumentsDirPath}/${file}`;
if (await FileManager.fileExists(path)) {
const content = await FileManager.readFile(path, "utf8");
try {
return JSON.parse(content);
} catch {
// Corrupted content, ignore
}
}
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(defaultData), "utf8");
return JSON.parse(await FileManager.readFile(path, "utf8"));
},
set: async data => {
await migratePromise;
await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8");
}
};
};

View file

@ -1,156 +1,190 @@
import { StorageBackend } from "@lib/api/storage/backends";
import { Emitter, EmitterEvent, EmitterListener, EmitterListenerData } from "@lib/utils/Emitter";
import { Emitter } from "@core/vendetta/Emitter";
import { Observable, ObserverOptions } from "@gullerya/object-observer";
import { fileExists, readFile, removeFile, writeFile } from "@lib/api/native/fs";
import { debounce } from "es-toolkit";
const emitterSymbol = Symbol.for("vendetta.storage.emitter");
const syncAwaitSymbol = Symbol.for("vendetta.storage.accessor");
const storageInitErrorSymbol = Symbol.for("bunny.storage.initError");
const storagePromiseSymbol = Symbol.for("bunny.storage.promise");
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
const emitter = new Emitter();
const parentTarget = target;
const _loadedStorage = {} as Record<string, any>;
const childrens = new WeakMap<any, any>();
const proxiedChildrenSet = new WeakSet<any>();
function createProxy(target: any, path: string[]): any {
return new Proxy(target, {
get(target, prop: string) {
if ((prop as unknown) === emitterSymbol) return emitter;
const newPath = [...path, prop];
const value: any = target[prop];
if (value !== undefined && value !== null) {
emitter.emit("GET", {
parent: parentTarget,
path: newPath,
value,
});
if (typeof value === "object") {
if (proxiedChildrenSet.has(value)) return value;
if (childrens.has(value)) return childrens.get(value);
const childrenProxy = createProxy(value, newPath);
childrens.set(value, childrenProxy);
return childrenProxy;
}
return value;
}
return value;
},
set(target, prop: string, value) {
if (typeof value === "object") {
if (childrens.has(value)) {
target[prop] = childrens.get(value);
} else {
const childrenProxy = createProxy(value, [...path, prop]);
childrens.set(value, childrenProxy);
proxiedChildrenSet.add(value);
target[prop] = childrenProxy;
}
} else {
target[prop] = value;
}
emitter.emit("SET", {
parent: parentTarget,
path: [...path, prop],
value: target[prop],
});
// we do not care about success, if this actually does fail we have other problems
return true;
},
deleteProperty(target, prop: string) {
const value = typeof target[prop] === "object" ? childrens.get(target[prop])! : target[prop];
const success = delete target[prop];
if (success)
emitter.emit("DEL", {
value,
parent: parentTarget,
path: [...path, prop],
});
return success;
},
});
}
function createFileBackend<T extends object>(filePath: string) {
const write = debounce((data: T) => {
writeFile(filePath, JSON.stringify(data));
}, 500);
return {
proxy: createProxy(target, []),
emitter,
get: async () => {
try {
return JSON.parse(await readFile(filePath)) as T;
} catch (e) {
throw new Error(`Failed to parse storage from '${filePath}'`, { cause: e });
}
},
set: async (data: T) => {
if (!data || typeof data !== "object") {
throw new Error("data needs to be an object");
}
write(data);
},
exists: async () => {
return await fileExists(filePath);
}
};
}
export function useProxy<T>(storage: T): T {
const emitter = (storage as any)?.[emitterSymbol] as Emitter;
if (!emitter) throw new Error("storage?.[emitterSymbol] is undefined");
export function useObservable(observables: Observable[], opts?: ObserverOptions) {
if (observables.some((o: any) => o?.[storageInitErrorSymbol])) throw new Error(
"An error occured while initializing the storage",
);
if (observables.some(o => !Observable.isObservable(o))) {
throw new Error("Argument passed isn't an Observable");
}
const [, forceUpdate] = React.useReducer(n => ~n, 0);
React.useEffect(() => {
const listener: EmitterListener = (event: EmitterEvent, data: EmitterListenerData) => {
if (event === "DEL" && data.value === storage) return;
forceUpdate();
};
const listener = () => forceUpdate();
emitter.on("SET", listener);
emitter.on("DEL", listener);
observables.forEach(o => Observable.observe(o, listener, opts));
return () => {
emitter.off("SET", listener);
emitter.off("DEL", listener);
observables.forEach(o => Observable.unobserve(o, listener));
};
}, []);
return storage;
}
export async function createStorage<T>(backend: StorageBackend): Promise<Awaited<T>> {
const data = await backend.get();
const { proxy, emitter } = createProxy(data);
const handler = () => backend.set(proxy);
emitter.on("SET", handler);
emitter.on("DEL", handler);
return proxy;
export async function updateStorage<T extends object = {}>(path: string, value: T): Promise<void> {
_loadedStorage[path] = value;
createFileBackend<T>(path).set(value);
}
export function wrapSync<T extends Promise<any>>(store: T): Awaited<T> {
let awaited: any = undefined;
export function createStorageAndCallback<T extends object = {}>(
path: string,
cb: (proxy: T) => void,
{
dflt = {} as T,
nullIfEmpty = false
} = {}
) {
let emitter: Emitter;
const awaitQueue: (() => void)[] = [];
const awaitInit = (cb: () => void) => (awaited ? cb() : awaitQueue.push(cb));
const callback = (data: any) => {
const proxy = new Proxy(Observable.from(data), {
get(target, prop, receiver) {
if (prop === Symbol.for("vendetta.storage.emitter")) {
if (emitter) return emitter;
emitter = new Emitter();
store.then(v => {
awaited = v;
awaitQueue.forEach(cb => cb());
});
Observable.observe(target, changes => {
for (const change of changes) {
emitter.emit(change.type !== "delete" ? "SET" : "DEL", {
path: change.path,
value: change.value
});
}
});
return new Proxy({} as Awaited<T>, {
return emitter;
}
return Reflect.get(target, prop, receiver);
},
});
const handler = () => backend.set(proxy);
Observable.observe(proxy, handler);
cb(proxy);
};
const backend = createFileBackend<T>(path);
if (_loadedStorage[path]) {
callback(_loadedStorage[path]);
} else {
backend.exists().then(async exists => {
if (!exists) {
if (nullIfEmpty) {
callback(_loadedStorage[path] = null);
} else {
_loadedStorage[path] = dflt;
await backend.set(dflt);
callback(dflt);
}
} else {
callback(_loadedStorage[path] = await backend.get());
}
});
}
}
type StorageOptions<T extends object> = Parameters<typeof createStorageAndCallback<T>>[2];
export async function createStorageAsync<T extends object = {}>(
path: string,
opts: StorageOptions<T> = {}
): Promise<T> {
return new Promise(r => createStorageAndCallback(path, r, opts));
}
export const createStorage = <T extends object = {}>(
path: string,
opts: StorageOptions<T> = {}
): T => {
const promise = new Promise(r => resolvePromise = r);
let awaited: any, resolved: boolean, error: any, resolvePromise: (val?: unknown) => void;
createStorageAndCallback(path, proxy => {
awaited = proxy;
resolved = true;
resolvePromise();
}, opts);
const check = () => {
if (resolved) return true;
throw new Error(`Attempted to access storage without initializing: ${path}`);
};
return new Proxy({} as any, {
...Object.fromEntries(
Object.getOwnPropertyNames(Reflect)
// @ts-expect-error
.map(k => [k, (t: T, ...a: any[]) => Reflect[k](awaited ?? t, ...a)])
.map(k => [k, (t: T, ...a: any[]) => {
// @ts-expect-error
return check() && Reflect[k](awaited, ...a);
}])
),
get(target, prop, recv) {
if (prop === syncAwaitSymbol) return awaitInit;
return Reflect.get(awaited ?? target, prop, recv);
if (prop === storageInitErrorSymbol) return error;
if (prop === storagePromiseSymbol) return promise;
return check() && Reflect.get(awaited ?? target, prop, recv);
},
});
};
export async function preloadStorageIfExists(path: string): Promise<boolean> {
if (_loadedStorage[path]) return true;
const backend = createFileBackend(path);
if (await backend.exists()) {
_loadedStorage[path] = await backend.get();
return false;
}
return true;
}
export function awaitStorage(...stores: any[]) {
return Promise.all(
stores.map(store => new Promise<void>(res => store[syncAwaitSymbol](res)))
);
export async function purgeStorage(path: string) {
await removeFile(path);
delete _loadedStorage[path];
}
export {
createFileBackend,
createMMKVBackend,
purgeStorage
} from "@lib/api/storage/backends";
export function awaitStorage(...proxies: any[]) {
return Promise.all(proxies.map(proxy => proxy[storagePromiseSymbol]));
}
/** @internal */
export function getPreloadedStorage<T extends object = {}>(path: string): T | undefined {
return _loadedStorage[path];
}

View file

@ -1,205 +0,0 @@
// New iteration of storage API, mostly yoinked from unreleased pyoncord (and sunrise?)
import { fileExists, readFile, writeFile } from "@lib/api/native/fs";
import { Emitter } from "@lib/utils/Emitter";
interface StorageBackend<T = unknown> {
get: () => Promise<T>;
set: (data: T) => Promise<void>;
exists: () => Promise<boolean>;
}
const emitterSymbol = Symbol.for("bunny.storage.emitter");
const storageInitErrorSymbol = Symbol.for("bunny.storage.initError");
const storagePromiseSymbol = Symbol.for("bunny.storage.promise");
const _loadedPath = {} as Record<string, any>;
function createFileBackend<T = any>(filePath: string): StorageBackend<T> {
return {
get: async () => {
try {
return JSON.parse(await readFile(filePath));
} catch (e) {
throw new Error(`Failed to parse storage from '${filePath}'`, { cause: e });
}
},
set: async data => {
if (!data || typeof data !== "object") throw new Error("data needs to be an object");
await writeFile(filePath, JSON.stringify(data));
},
exists: async () => {
return await fileExists(filePath);
}
};
}
function _createProxy(target: any, path: string[], emitter: Emitter): any {
// cache the children proxies so proxy.child === proxy.child (yields the same value)
const objChildrens = new WeakMap<object, object>();
return new Proxy(target, {
get(target, prop: string) {
if ((prop as unknown) === emitterSymbol) return emitter;
const newPath = [...path, prop];
let value: any = target[prop];
if (value && typeof value === "object") {
const origValue = value;
value = objChildrens.get(origValue);
if (!value) {
value = _createProxy(origValue, newPath, emitter);
objChildrens.set(origValue, value!);
}
}
if (value != null) {
emitter.emit("GET", {
path: newPath,
value,
});
}
return value;
},
set(target, prop: string, value) {
target[prop] = value;
emitter.emit("SET", {
path: [...path, prop],
value,
});
// we do not care about success, if this actually does fail we have other problems
return true;
},
deleteProperty(target, prop: string) {
const success = delete target[prop];
if (success)
emitter.emit("DEL", {
path: [...path, prop],
});
return success;
},
});
}
export function createProxy(target: any = {}): { proxy: any; emitter: Emitter; } {
const emitter = new Emitter();
return {
proxy: _createProxy(target, [], emitter),
emitter,
};
}
export function useProxy<T>(storage: T & { [key: string | symbol]: any; }) {
if (storage[storageInitErrorSymbol]) throw new Error(
"An error occured while initializing the storage",
{ cause: storage[storageInitErrorSymbol] }
);
const emitter = storage[emitterSymbol] as Emitter;
if (emitter == null) {
throw new Error(`InvalidArgumentException - storage[emitterSymbol] is ${typeof emitter}`);
}
const [, forceUpdate] = React.useReducer(n => ~n, 0);
React.useEffect(() => {
const listener = () => forceUpdate();
emitter.on("SET", listener);
emitter.on("DEL", listener);
return () => {
emitter.off("SET", listener);
emitter.off("DEL", listener);
};
}, []);
}
export async function updateStorageAsync<T>(path: string, value: T): Promise<void> {
_loadedPath[path] = value;
await createFileBackend<T>(path).set(value);
}
export function createStorageAndCallback<T>(path: string, dflt = {} as T, cb: (proxy: T) => void) {
const callback = (data: any) => {
const { proxy, emitter } = createProxy(data);
const handler = () => backend.set(proxy);
emitter.on("SET", handler);
emitter.on("DEL", handler);
cb(proxy);
};
const backend = createFileBackend<T>(path);
if (_loadedPath[path]) callback(_loadedPath[path]);
else {
backend.exists().then(async exists => {
if (!exists) {
await backend.set(dflt);
callback(dflt);
} else {
callback(await backend.get());
}
});
}
}
export async function createStorageAsync<T>(path: string, dflt = {} as T): Promise<T> {
return new Promise(r => createStorageAndCallback(path, dflt, r));
}
export const createStorage = <T>(path: string, dflt = {} as T): T & { [key: symbol]: any; } => {
const promise = new Promise(r => resolvePromise = r);
let awaited: any, resolved: boolean, error: any, resolvePromise: (val?: unknown) => void;
createStorageAndCallback(path, dflt, proxy => {
awaited = proxy;
resolved = true;
resolvePromise();
});
const check = () => {
if (resolved) return true;
throw new Error("Attempted to access storage without initializing");
};
return new Proxy({} as any, {
...Object.fromEntries(
Object.getOwnPropertyNames(Reflect)
.map(k => [k, (t: T, ...a: any[]) => {
// @ts-expect-error
return check() && Reflect[k](awaited, ...a);
}])
),
get(target, prop, recv) {
if (prop === storageInitErrorSymbol) return error;
if (prop === storagePromiseSymbol) return promise;
return check() && Reflect.get(awaited ?? target, prop, recv);
},
});
};
export async function preloadStorageIfExists(path: string) {
if (_loadedPath[path]) return _loadedPath[path];
const backend = createFileBackend(path);
if (await backend.exists()) {
return _loadedPath[path] = await backend.get();
}
console.log("no " + path);
}
export function getPreloadedStorage<T>(path: string): T {
return _loadedPath[path];
}
export function awaitStorage(...proxies: any[]) {
return Promise.all(proxies.map(proxy => proxy[storagePromiseSymbol]));
}

View file

@ -1,19 +1,27 @@
import "../global.d.ts"; // eslint-disable-line import-alias/import-alias
import "../modules.d.ts"; // eslint-disable-line import-alias/import-alias
export * as fonts from "./addons/fonts/index.js";
export * as plugins from "./addons/plugins/index.js";
export * as themes from "./addons/themes/index.js";
export * as api from "./api";
export * as fonts from "./fonts";
export * as plugins from "./plugins";
export * as themes from "./themes";
export * as ui from "./ui";
export * as utils from "./utils";
export * as metro from "@metro";
import * as fonts from "./fonts";
import * as plugins from "./plugins";
import * as themes from "./themes";
import * as fonts from "./addons/fonts/index.js";
import * as plugins from "./addons/plugins/index.js";
import * as themes from "./addons/themes/index.js";
/** @internal */
export * as _jsx from "react/jsx-runtime";
import { proxyLazy } from "./utils/lazy";
/**
* @internal
* @deprecated Moved to top level (bunny.*)
*/
export const managers = proxyLazy(() => {
console.warn("bunny.managers.* is deprecated, and moved the top level (bunny.*). bunny.managers will be eventually removed soon");

View file

@ -1,4 +1,4 @@
import { color } from "@lib/themes";
import { color } from "@lib/addons/themes";
import { constants } from "@metro/common";
import { findByStoreNameLazy } from "@metro/wrappers";

View file

@ -1,10 +1,9 @@
import { Strings } from "@core/i18n";
import { Nullish } from "@lib/utils/types";
import { React } from "@metro/common";
import { Button, LegacyFormText } from "@metro/common/components";
import { Codeblock } from "@ui/components";
import { createThemedStyleSheet } from "@ui/styles";
import { ScrollView } from "react-native";
import { Falsy, ScrollView } from "react-native";
type ErrorBoundaryState = {
hasErr: false;
@ -14,7 +13,7 @@ type ErrorBoundaryState = {
};
export interface ErrorBoundaryProps {
children: JSX.Element | Nullish | (JSX.Element | Nullish)[];
children: JSX.Element | Falsy | (JSX.Element | Falsy)[];
}
const styles = createThemedStyleSheet({

View file

@ -1,4 +1,3 @@
import { ButtonColors } from "@lib/utils/types";
import { LegacyAlert, LegacyFormInput } from "@metro/common/components";
import { findByPropsLazy } from "@metro/wrappers";
@ -7,7 +6,7 @@ const Alerts = findByPropsLazy("openLazy", "close");
export interface InputAlertProps {
title?: string;
confirmText?: string;
confirmColor?: ButtonColors;
confirmColor?: string;
onConfirm: (input: string) => (void | Promise<void>);
cancelText?: string;
placeholder?: string;

View file

@ -1,3 +1,4 @@
export * as wrappers from "./wrappers";
export { default as Codeblock } from "@ui/components/Codeblock";
export { default as ErrorBoundary } from "@ui/components/ErrorBoundary";
export { default as Search } from "@ui/components/Search";

View file

@ -0,0 +1,39 @@
import { lazyDestructure } from "@lib/utils/lazy";
import { findByFilePath, findByProps } from "@metro";
import { Text } from "@metro/common/components";
import { Button } from "@metro/common/types/components";
import { ComponentProps, ComponentType, ReactNode } from "react";
import { View } from "react-native";
type ActionButtonProps = Omit<ComponentProps<Button>, "onPress"> & {
onPress?: () => void | Promise<unknown>;
};
const {
AlertModal: _AlertModal,
AlertActionButton: _AlertActionButton
} = lazyDestructure(() => findByProps("AlertModal", "AlertActions"));
export const AlertActionButton = _AlertActionButton as ComponentType<ActionButtonProps>;
export default function AlertModal(props: Record<string, unknown>) {
const forwardFailedModal = findByFilePath("modules/forwarding/native/ForwardFailedAlertModal.tsx");
// ponyfill for extraContent
if (!forwardFailedModal && "extraContent" in props) {
props.content = (
<View style={{ gap: 16 }}>
<Text variant="text-md/medium" color="text-muted">
{props.content as string}
</Text>
<View>
{props.extraContent as ReactNode}
</View>
</View>
);
delete props.extraContent;
}
return <_AlertModal {...props} />;
}

View file

@ -0,0 +1,3 @@
import AlertModal, { AlertActionButton } from "./AlertModal";
export { AlertActionButton, AlertModal };

View file

@ -1,7 +1,6 @@
export * as alerts from "./alerts";
export * as color from "./color";
export * as components from "./components";
export * as settings from "./settings";
export * as sheets from "./sheets";
export * as styles from "./styles";
export * as toasts from "./toasts";

View file

@ -4,7 +4,6 @@ import { DeviceManager } from "@lib/api/native/modules";
import { after } from "@lib/api/patcher";
import { settings } from "@lib/api/settings";
import { lazyDestructure } from "@lib/utils/lazy";
import { ButtonColors } from "@lib/utils/types";
import { Button, CompatButton, SafeAreaView } from "@metro/common/components";
import { _lazyContextSymbol } from "@metro/lazy";
import { LazyModuleContext } from "@metro/types";
@ -91,7 +90,7 @@ export default () => after.await("render", getErrorBoundaryContext(), function (
const buttons: Button[] = [
{ text: Strings.RELOAD_DISCORD, onPress: this.handleReload },
...!settings.safeMode?.enabled ? [{ text: Strings.RELOAD_IN_SAFE_MODE, onPress: toggleSafeMode }] : [],
{ text: Strings.RETRY_RENDER, color: ButtonColors.RED, onPress: () => this.setState({ info: null, error: null }) },
{ text: Strings.RETRY_RENDER, color: "red", onPress: () => this.setState({ info: null, error: null }) },
];
return (
@ -130,7 +129,7 @@ export default () => after.await("render", getErrorBoundaryContext(), function (
return <CompatButton
text={button.text}
color={button.color ?? ButtonColors.BRAND}
color={button.color ?? "brand"}
size={button.size ?? "small"}
onPress={button.onPress}
style={{

View file

@ -12,6 +12,7 @@ export interface RowConfig {
render?: Parameters<typeof lazy>[0];
icon?: ImageURISource | number;
usePredicate?: () => boolean,
useTrailing?: () => string | JSX.Element,
rawTabsConfig?: Record<string, any>;
}

View file

@ -1,7 +1,7 @@
import { after } from "@lib/api/patcher";
import { findInReactTree } from "@lib/utils";
import { i18n, NavigationNative } from "@metro/common";
import { LegacyFormIcon, LegacyFormRow, LegacyFormSection } from "@metro/common/components";
import { LegacyFormDivider,LegacyFormIcon, LegacyFormRow, LegacyFormSection } from "@metro/common/components";
import { findByNameLazy } from "@metro/wrappers";
import { registeredSections } from "@ui/settings";
@ -12,16 +12,19 @@ function SettingsSection() {
const navigation = NavigationNative.useNavigation();
return <>
{Object.keys(registeredSections).map(sect => (
{Object.keys(registeredSections).map(sect => registeredSections[sect].length > 0 && (
<LegacyFormSection key={sect} title={sect}>
{ /** Is usePredicate here safe? */}
{registeredSections[sect].filter(r => r.usePredicate?.() ?? true).map(row => (
<LegacyFormRow
label={row.title()}
leading={<LegacyFormIcon source={row.icon} />}
trailing={LegacyFormRow.Arrow}
onPress={wrapOnPress(row.onPress, navigation, row.render, row.title())}
/>
{registeredSections[sect].filter(r => r.usePredicate?.() ?? true).map((row, i, arr) => (
<>
<LegacyFormRow
label={row.title()}
leading={<LegacyFormIcon source={row.icon} />}
trailing={<LegacyFormRow.Arrow label={row.useTrailing?.() || undefined} />}
onPress={wrapOnPress(row.onPress, navigation, row.render, row.title())}
/>
{i !== arr.length - 1 && <LegacyFormDivider />}
</>
))}
</LegacyFormSection>
))}
@ -55,7 +58,7 @@ export function patchPanelUI(unpatches: (() => void | boolean)[]) {
const sections = findInReactTree(
res.props.children,
n => n?.children?.[1]?.type === LegacyFormSection
)?.children;
)?.children || res.props.children;
if (sections) {
const index = sections.findIndex((c: any) => titles.includes(c?.props.label));

View file

@ -24,6 +24,7 @@ export function patchTabsUI(unpatches: (() => void | boolean)[]) {
title: row.title,
icon: row.icon,
usePredicate: row.usePredicate,
useTrailing: row.useTrailing,
onPress: wrapOnPress(row.onPress, null, row.render, row.title()),
withArrow: true,
...row.rawTabsConfig

View file

@ -1,163 +1,3 @@
import { OptionalKeys } from "@lib/utils/types";
import { LiteralUnion } from "type-fest";
export type DiscordTextStyles = OptionalKeys<[
"heading-sm/normal",
"heading-sm/medium",
"heading-sm/semibold",
"heading-sm/bold",
"heading-sm/extrabold",
"heading-md/normal",
"heading-md/medium",
"heading-md/semibold",
"heading-md/bold",
"heading-md/extrabold",
"heading-lg/normal",
"heading-lg/medium",
"heading-lg/semibold",
"heading-lg/bold",
"heading-lg/extrabold",
"heading-xl/normal",
"heading-xl/medium",
"heading-xl/semibold",
"heading-xl/bold",
"heading-xl/extrabold",
"heading-xxl/normal",
"heading-xxl/medium",
"heading-xxl/semibold",
"heading-xxl/bold",
"heading-xxl/extrabold",
"eyebrow",
"heading-deprecated-12/normal",
"heading-deprecated-12/medium",
"heading-deprecated-12/semibold",
"heading-deprecated-12/bold",
"heading-deprecated-12/extrabold",
"redesign/heading-18/bold",
"text-xxs/normal",
"text-xxs/medium",
"text-xxs/semibold",
"text-xxs/bold",
"text-xs/normal",
"text-xs/medium",
"text-xs/semibold",
"text-xs/bold",
"text-sm/normal",
"text-sm/medium",
"text-sm/semibold",
"text-sm/bold",
"text-md/normal",
"text-md/medium",
"text-md/semibold",
"text-md/bold",
"text-lg/normal",
"text-lg/medium",
"text-lg/semibold",
"text-lg/bold",
"redesign/message-preview/normal",
"redesign/message-preview/medium",
"redesign/message-preview/semibold",
"redesign/message-preview/bold",
"redesign/channel-title/normal",
"redesign/channel-title/medium",
"redesign/channel-title/semibold",
"redesign/channel-title/bold",
"display-sm",
"display-md",
"display-lg"
]>;
export type RedesignModuleKeys = OptionalKeys<[
"AlertActionButton",
"AlertModal",
"AlertModalContainer",
"AvatarDuoPile",
"AvatarPile",
"BACKDROP_OPAQUE_MAX_OPACITY",
"Backdrop",
"Button",
"Card",
"ContextMenu",
"ContextMenuContainer",
"FauxHeader",
"FloatingActionButton",
"GhostInput",
"GuildIconPile",
"HeaderActionButton",
"HeaderButton",
"HeaderSubmittingIndicator",
"IconButton",
"Input",
"InputButton",
"InputContainer",
"LayerContext",
"LayerScope",
"Modal",
"ModalActionButton",
"ModalContent",
"ModalDisclaimer",
"ModalFloatingAction",
"ModalFloatingActionSpacer",
"ModalFooter",
"ModalScreen",
"ModalStepIndicator",
"NAV_BAR_HEIGHT",
"NAV_BAR_HEIGHT_MULTILINE",
"Navigator",
"NavigatorHeader",
"NavigatorScreen",
"Pile",
"PileOverflow",
"RedesignCompat",
"RedesignCompatContext",
"RowButton",
"STATUS_BAR_HEIGHT",
"SceneLoadingIndicator",
"SearchField",
"SegmentedControl",
"SegmentedControlPages",
"Slider",
"Stack",
"StepModal",
"StickyContext",
"StickyHeader",
"StickyWrapper",
"TABLE_ROW_CONTENT_HEIGHT",
"TABLE_ROW_HEIGHT",
"TableCheckboxRow",
"TableRadioGroup",
"TableRadioRow",
"TableRow",
"TableRowGroup",
"TableRowGroupTitle",
"TableRowIcon",
"TableSwitchRow",
"Tabs",
"TextArea",
"TextField",
"TextInput",
"Toast",
"dismissAlerts",
"getHeaderBackButton",
"getHeaderCloseButton",
"getHeaderConditionalBackButton",
"getHeaderNoTitle",
"getHeaderTextButton",
"hideContextMenu",
"navigatorShouldCrossfade",
"openAlert",
"useAccessibilityNativeStackOptions",
"useAndroidNavScrim",
"useCoachmark",
"useFloatingActionButtonScroll",
"useFloatingActionButtonState",
"useNativeStackNavigation",
"useNavigation",
"useNavigationTheme",
"useNavigatorBackPressHandler",
"useNavigatorScreens",
"useNavigatorShouldCrossfade",
"useSegmentedControlState",
"useStackNavigation",
"useTabNavigation",
"useTooltip"
]>;
export type DiscordTextStyles = LiteralUnion<"heading-sm/normal" | "heading-sm/medium" | "heading-sm/semibold" | "heading-sm/bold" | "heading-sm/extrabold" | "heading-md/normal" | "heading-md/medium" | "heading-md/semibold" | "heading-md/bold" | "heading-md/extrabold" | "heading-lg/normal" | "heading-lg/medium" | "heading-lg/semibold" | "heading-lg/bold" | "heading-lg/extrabold" | "heading-xl/normal" | "heading-xl/medium" | "heading-xl/semibold" | "heading-xl/bold" | "heading-xl/extrabold" | "heading-xxl/normal" | "heading-xxl/medium" | "heading-xxl/semibold" | "heading-xxl/bold" | "heading-xxl/extrabold" | "eyebrow" | "heading-deprecated-12/normal" | "heading-deprecated-12/medium" | "heading-deprecated-12/semibold" | "heading-deprecated-12/bold" | "heading-deprecated-12/extrabold" | "redesign/heading-18/bold" | "text-xxs/normal" | "text-xxs/medium" | "text-xxs/semibold" | "text-xxs/bold" | "text-xs/normal" | "text-xs/medium" | "text-xs/semibold" | "text-xs/bold" | "text-sm/normal" | "text-sm/medium" | "text-sm/semibold" | "text-sm/bold" | "text-md/normal" | "text-md/medium" | "text-md/semibold" | "text-md/bold" | "text-lg/normal" | "text-lg/medium" | "text-lg/semibold" | "text-lg/bold" | "redesign/message-preview/normal" | "redesign/message-preview/medium" | "redesign/message-preview/semibold" | "redesign/message-preview/bold" | "redesign/channel-title/normal" | "redesign/channel-title/medium" | "redesign/channel-title/semibold" | "redesign/channel-title/bold" | "display-sm" | "display-md" | "display-lg", string>;

View file

@ -2,13 +2,12 @@ export const DISCORD_SERVER = "https://discord.com/invite/ddcQf3s2Uq";
export const GITHUB = "https://github.com/revenge-mod";
export const HTTP_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/;
export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&//=]*)/g;
export const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
export const BUNNY_PROXY_PREFIX = "https://bn-plugins.github.io/vd-proxy";
export const OLD_BUNNY_PROXY_PREFIX = "https://bunny-mod.github.io/plugins-proxy";
export const OFFICIAL_PLUGINS_REPO_URL = "https://bn-plugins.github.io/dist/repo.json";
// These are still Vendetta's
export const DISCORD_SERVER_ID = "1015931589865246730";
export const PLUGINS_CHANNEL_ID = "1091880384561684561";
export const THEMES_CHANNEL_ID = "1091880434939482202";
export const VD_PROXY_PREFIX = "https://vd-plugins.github.io/proxy";
export const VD_DISCORD_SERVER_ID = "1015931589865246730";
export const VD_PLUGINS_CHANNEL_ID = "1091880384561684561";
export const VD_THEMES_CHANNEL_ID = "1091880434939482202";

View file

@ -3,7 +3,7 @@
// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity.
// See https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript/52171480#52171480
// https://github.com/bryc/code/blob/master/jshash/experimental/cyrb53.js
export function cyrb64(str: string, seed = 0) {
export default function cyrb64(str: string, seed = 0) {
let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed;
for (let i = 0, ch; i < str.length; i++) {
ch = str.charCodeAt(i);

View file

@ -1,6 +1,8 @@
import { findInTree } from "@lib/utils";
import { SearchFilter } from "@lib/utils/findInTree";
export const findInReactTree = (tree: { [key: string]: any; }, filter: SearchFilter): any => findInTree(tree, filter, {
walkable: ["props", "children", "child", "sibling"],
});
export default function findInReactTree(tree: { [key: string]: any; }, filter: SearchFilter): any {
return findInTree(tree, filter, {
walkable: ["props", "children", "child", "sibling"],
});
}

View file

@ -15,7 +15,7 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindI
try {
if (filter(tree)) return tree;
} catch {}
} catch { }
if (Array.isArray(tree)) {
for (const item of tree) {
@ -24,7 +24,7 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindI
try {
const found = treeSearch(item, filter, opts, depth + 1);
if (found) return found;
} catch {}
} catch { }
}
} else if (typeof tree === "object") {
for (const key of Object.keys(tree)) {
@ -35,12 +35,12 @@ function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required<FindI
try {
const found = treeSearch(tree[key], filter, opts, depth + 1);
if (found) return found;
} catch {}
} catch { }
}
}
}
export const findInTree = (
export default function findInTree(
tree: SearchTree,
filter: SearchFilter,
{
@ -48,4 +48,6 @@ export const findInTree = (
ignore = [],
maxDepth = 100
}: FindInTreeOptions = {},
): any | undefined => treeSearch(tree, filter, { walkable, ignore, maxDepth }, 0);
): any | undefined {
return treeSearch(tree, filter, { walkable, ignore, maxDepth }, 0);
}

View file

@ -2,7 +2,7 @@ import { LiteralUnion } from "type-fest";
type KeyOfOrAny<P, T extends object> = P extends keyof T ? T[P] : any;
export function hookDefineProperty<
export default function hookDefineProperty<
T extends object,
P extends LiteralUnion<keyof T, PropertyKey>
>(target: T, property: LiteralUnion<keyof T, PropertyKey>, cb: (val: KeyOfOrAny<P, T>) => KeyOfOrAny<P, T>) {

View file

@ -1,10 +1,20 @@
export * as constants from "./constants";
export { cyrb64 } from "./cyrb64";
export { Emitter } from "./Emitter";
export { findInReactTree } from "./findInReactTree";
export { findInTree } from "./findInTree";
export { hookDefineProperty } from "./hookDefineProperty";
export { default as invariant } from "./invariant";
export * as lazy from "./lazy";
export * as logger from "./logger";
export { safeFetch } from "./safeFetch";
import * as constants from "./constants";
import cyrb64 from "./cyrb64";
import findInReactTree from "./findInReactTree";
import findInTree from "./findInTree";
import hookDefineProperty from "./hookDefineProperty";
import invariant from "./invariant";
import * as lazy from "./lazy";
import * as logger from "./logger";
import safeFetch from "./safeFetch";
export {
constants,
cyrb64,
findInReactTree,
findInTree,
hookDefineProperty,
invariant,
lazy,
logger,
safeFetch };

View file

@ -1,6 +1,6 @@
import { findByNameLazy } from "@metro/wrappers";
export type LoggerFunction = (...messages: any[]) => void;
type LoggerFunction = (...messages: any[]) => void;
export interface Logger {
log: LoggerFunction;
info: LoggerFunction;
@ -11,5 +11,5 @@ export interface Logger {
verbose: LoggerFunction;
}
export const DiscordLogger = findByNameLazy("Logger");
export const logger: Logger = new DiscordLogger("Revenge");
export const LoggerClass = findByNameLazy("Logger");
export const logger: Logger = new LoggerClass("Revenge");

View file

@ -1,5 +1,5 @@
// A really basic fetch wrapper which throws on non-ok response codes
export async function safeFetch(input: RequestInfo | URL, options?: RequestInit, timeout = 10000) {
export default async function safeFetch(input: RequestInfo | URL, options?: RequestInit, timeout = 10000) {
const req = await fetch(input, {
signal: timeoutSignal(timeout),
...options

View file

@ -1,22 +0,0 @@
import { LiteralUnion } from "type-fest";
export type Nullish = null | undefined;
export type OptionalKeys<T extends Array<string>> = LiteralUnion<T[number], string>;
export interface Author {
name: string;
id?: string;
}
export enum ButtonColors {
BRAND = "brand",
RED = "red",
GREEN = "green",
PRIMARY = "primary",
TRANSPARENT = "transparent",
GREY = "grey",
LIGHTGREY = "lightgrey",
WHITE = "white",
LINK = "link"
}

View file

@ -2,6 +2,8 @@ import { findByFilePathLazy, findByProps, findByPropsLazy } from "@metro/wrapper
import type { Dispatcher } from "./types/flux";
export * as components from "./components";
// Discord
export const constants = findByPropsLazy("Fonts", "Permissions");
export const channels = findByPropsLazy("getVoiceChannelId");

View file

@ -1,4 +1,3 @@
import { Nullish } from "@lib/utils/types";
import { DiscordTextStyles } from "@ui/types";
import { MutableRefObject, ReactNode, RefObject } from "react";
import type * as RN from "react-native";
@ -25,6 +24,7 @@ interface ButtonProps {
scaleAmountInPx?: number;
icon?: ImageSourcePropType | ReactNode;
style?: Style;
grow?: boolean;
}
export type Button = React.ForwardRefExoticComponent<ButtonProps>;
@ -103,7 +103,7 @@ interface TextInputProps extends Omit<RN.TextInputProps, "onChange" | "onChangeT
trailingIcon?: React.FC<any>;
trailingPressableProps?: PressableProps;
trailingText?: string;
value?: string | Nullish;
value?: string | RN.Falsy;
}
export type TextInput = React.FC<TextInputProps>;

Some files were not shown because too many files have changed in this diff Show more