├── .dockerignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── discord_ping_on_release.yml ├── .gitignore ├── .prettierrc.json ├── DEVELOPERS.md ├── DOCKERCOMPOSE.md ├── Dockerfile ├── LICENSE ├── README.md ├── android-interface.sh ├── docker-compose.yml ├── index.js ├── package-lock.json ├── package.json ├── public ├── about │ └── index.html ├── apps.js ├── apps │ └── index.html ├── colors.js ├── dependencies │ └── index.html ├── devices │ └── index.html ├── exited │ └── index.html ├── failure │ └── index.html ├── index.html ├── index.js ├── installer │ └── index.html ├── patch │ └── index.html ├── patches │ └── index.html ├── settings.js ├── settings │ └── index.html ├── styles │ ├── about.css │ ├── apps.css │ ├── core.css │ ├── dependencies.css │ ├── failure.css │ ├── fontawesome.css │ ├── fonts │ │ ├── Source Sans HW Bold.otf │ │ └── Source Sans HW Regular.otf │ ├── home.css │ ├── patches.css │ └── settings.css ├── versions │ └── index.html └── webfonts │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff2 │ ├── fa-solid-900.ttf │ └── fa-solid-900.woff2 ├── shell.nix ├── utils ├── AntiSplit.js ├── FileDownloader.js ├── PatchesParser.js ├── Settings.js ├── checkJDKAndADB.js ├── checkJDKAndAapt2.js ├── downloadApp.js ├── fetchWithUserAgent.js ├── getAppVersion.js ├── getDeviceArch.js ├── getDeviceID.js ├── mountReVanced.js ├── mountReVancedInstaller.js ├── promisifiedExec.js └── uploadAPKFile.js └── wsEvents ├── checkFileAlreadyExists.js ├── checkForUpdates.js ├── getApp.js ├── getAppVersion.js ├── getDevices.js ├── getPatches.js ├── getSettings.js ├── index.js ├── installReVanced.js ├── patchApp.js ├── resetPatchOptions.js ├── resetSettings.js ├── selectApp.js ├── selectAppVersion.js ├── selectPatches.js ├── setDevice.js ├── setSettings.js └── updateFiles.js /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "overrides": [], 10 | "parserOptions": { 11 | "ecmaVersion": "latest" 12 | }, 13 | "rules": {} 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **rvx-builder Version:** [e.g. v3.11.1] 24 | 25 | **Desktop (please complete the following information):** 26 | 27 | - OS: [e.g. Windows] 28 | - Version: [e.g. 10] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/workflows/discord_ping_on_release.yml: -------------------------------------------------------------------------------- 1 | name: Ping Discord on release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | notify-discord: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: sarisia/actions-status-discord@v1 12 | if: always() 13 | id: webhook # set id to reference output payload later 14 | with: 15 | ack_no_webhook: true # suppress warning 16 | nodetail: true 17 | notimestamp: true 18 | 19 | username: ReVanced Extended 20 | content: "<@&1271198236350087283>" 21 | title: "RVX-Builder `${{ github.event.release.tag_name }}` has been released!" 22 | description: | 23 | Click [here](${{ github.event.release.html_url }}) to download it and read the changelog. 24 | 25 | - run: npm install axios 26 | - uses: actions/github-script@v7 27 | env: 28 | WEBHOOK_PAYLOAD: ${{ steps.webhook.outputs.payload }} 29 | WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} 30 | with: 31 | script: | 32 | const axios = require("axios") 33 | 34 | const { WEBHOOK_PAYLOAD, WEBHOOK_URL } = process.env 35 | 36 | const payload = JSON.parse(WEBHOOK_PAYLOAD) 37 | 38 | // remove the color field to make it transparent 39 | delete payload.embeds[0].color 40 | 41 | // send to Discord 42 | axios.post(WEBHOOK_URL, payload) 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | ignore/ 3 | revanced/ 4 | rvx-builder-nodejs-macos 5 | rvx-builder-nodejs-linux 6 | rvx-builder-nodejs-win.exe 7 | excludedPatchesList.json 8 | includedPatchesList.json 9 | dist/ 10 | revanced-resource-cache/ 11 | index-old.js 12 | options.json 13 | settings.json -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none" 4 | } 5 | -------------------------------------------------------------------------------- /DEVELOPERS.md: -------------------------------------------------------------------------------- 1 | # Using the Builder (for developers) 2 | 3 | ## From source 4 | 5 | This is simple enough: 6 | 7 | ```bash 8 | git clone -b revanced-extended https://github.com/inotia00/rvx-builder.git --depth=1 --no-tags 9 | cd rvx-builder 10 | npm i 11 | node . 12 | ``` 13 | 14 | Note that you need [git](https://git-scm.com/downloads) and [NodeJS >= 16](https://nodejs.org/en/) for this. 15 | 16 | ## Building a binary 17 | 18 | This is also simple: 19 | 20 | ```bash 21 | git clone -b revanced-extended https://github.com/inotia00/rvx-builder.git --depth=1 --no-tags 22 | cd rvx-builder 23 | npm i 24 | npx pkg -t linux-x64,macos-x64,win-x64 -C GZip . 25 | ``` 26 | -------------------------------------------------------------------------------- /DOCKERCOMPOSE.md: -------------------------------------------------------------------------------- 1 | ### These are the Windows 10/11 `docker-compose` instructions for beginners to run the rvx-builder in a clean docker container: 2 | 3 | **Please follow each step carefully and make sure you read everything.** 4 | 5 | 1. Install [`Git`](https://git-scm.com/) if you don't have it already. If you are not sure, install it. 6 | 7 | 2. Install and open [`Docker Desktop`](https://www.docker.com/products/docker-desktop/), it has to be running in the background for you to be able to run the builder container. Wait for it to load completely. 8 | 9 | If you're running Windows 11, you can open your PowerShell terminal and install both of the above requirements with this command: 10 | ``` 11 | winget install --id=Git.Git -e && winget install --id=Docker.DockerDesktop -e --accept-package-agreements 12 | ``` 13 | 14 | 3. After you install both I recommend creating a new empty folder and performing the next steps inside it for better organization. 15 | 16 | 4. Right click inside the newly created folder and select "Open in Terminal" or "Open PowerShell Here" in the context menu. 17 | 18 | 5. Now that you have a Terminal running, run: 19 | ``` 20 | git clone -b revanced-extended https://github.com/inotia00/rvx-builder --depth=1 --no-tags 21 | ``` 22 | 23 | 6. Then run this command to get from here to the rvx-builder directory: 24 | ``` 25 | cd .\rvx-builder\ 26 | ``` 27 | *Or manually reopen the Terminal inside the newly created rvx-builder repository folder.* 28 | 29 | 7. To start building with **docker-compose** run: 30 | ``` 31 | docker-compose build --no-cache 32 | ``` 33 | *This can take a while, please wait for it to finish.* 34 | 35 | 8. After building, launch the container by running: 36 | ``` 37 | docker-compose up -d 38 | ``` 39 | In your browser open [localhost:8000](http://localhost:8000) to access the builder interface. 40 | Your built applications will be located in the **revanced** folder inside the the **rvx-builder** folder that is located in the folder you created in step 3. 41 | 42 | 9. When you are done building your applications, to stop the container you can run: 43 | ``` 44 | docker-compose down 45 | ``` 46 | Once that is done you can also stop Docker Desktop and close your Terminal window now. 47 |

48 | 49 | In the future when you're running the builder again, **after starting Docker Desktop**, you can open a Terminal inside the **rvx-builder** folder and start from step 7. 50 | 51 |
52 | 53 | #### Additonal information for advanced users: 54 | ##### Although it's recommended to build with the --no-cache flag, you don't necessarily have to unless you want to update the builder or just being safe, that way it will use the previous image to get you running the builder quickly. 55 | 56 | ##### If you run it in this fashion, to update the builder when you want to, open the Terminal inside the rvx-builder folder, do a `git pull` to be sure, and follow the `docker-compose` instructions to build again, and you really **have to** use the `--no-cache` flag this time. 57 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-slim 2 | 3 | WORKDIR /app 4 | 5 | RUN apt-get --yes update && \ 6 | apt-get --yes install git wget java-common libasound2 libxi6 libxtst6 xdg-utils libgtk2.0-0 libatk1.0-0 libpango1.0-0 libgdk-pixbuf2.0-0 libcairo2 libgl1-mesa-glx && \ 7 | wget -O /app/zulu.deb https://cdn.azul.com/zulu/bin/zulu17.56.15-ca-crac-jdk17.0.14-linux_amd64.deb && \ 8 | yes | dpkg -i /app/zulu.deb && \ 9 | rm /app/zulu.deb && \ 10 | apt-get -f install 11 | 12 | RUN git clone -b revanced-extended https://github.com/inotia00/rvx-builder --depth=1 --no-tags 13 | 14 | WORKDIR /app/rvx-builder 15 | 16 | RUN npm install --omit=dev 17 | 18 | EXPOSE 8000 19 | 20 | CMD ["node", ".", "--no-open"] 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReVanced Extended Builder 2 | 3 | This project will allow you to download the APK of [supported](https://github.com/inotia00/revanced-patches?tab=readme-ov-file#-list-of-patches-in-this-repository) apps and build ReVanced Extended easily! 4 | 5 | ## How to use 6 | 7 | To use on PC, see [this document](https://github.com/inotia00/revanced-documentation/blob/main/docs/rvx-builder%20(pc).md). 8 | 9 | To use on Android (via Termux), see [this document](https://github.com/inotia00/revanced-documentation/blob/main/docs/rvx-builder%20(android).md). 10 | 11 | ## For developers 12 | 13 | For developers, see [this](https://github.com/inotia00/rvx-builder/blob/revanced-extended/DEVELOPERS.md) 14 | 15 | ## How to use (Docker) 16 | 17 | Required [docker](https://docs.docker.com/get-docker/) and [docker-compose (for \*nix cli)](https://docs.docker.com/compose/install/linux/) must be installed 18 | 19 | **Note:** If you're using Docker Desktop, `docker-compose` will be pre-installed. 20 | 21 | Clone the repository and `cd` into the directory `rvx-builder` 22 | 23 | ### Build using `docker-compose` 24 | 25 | ```bash 26 | docker-compose build --pull --no-cache 27 | ``` 28 | 29 | This builds the Docker image (`--no-cache` is used to build the image from scratch; sometimes the cache might cause version conflicts). 30 | 31 | After building, launch the container (runs at `localhost:8000`): 32 | 33 | ```bash 34 | docker-compose up -d 35 | ``` 36 | 37 | To stop the container: 38 | 39 | ```bash 40 | docker-compose down 41 | ``` 42 | 43 | **Note: docker-compose uses docker-compose.yml so make sure you are in the same directory `rvx-builder`** 44 | 45 | To update to a newer version, stop the existing container if it is running, build the image and start it again. 46 | 47 | ### Build using only `docker` 48 | 49 | ```bash 50 | docker build . --pull -t --no-cache 51 | ``` 52 | 53 | Run the newly built container: 54 | 55 | ```bash 56 | docker run -d --name -p 8000:8000 --restart unless-stopped -v ./revanced/:/app/rvx-builder/revanced/ 57 | ``` 58 | 59 | This launches the container on `http://localhost:8000` 60 | 61 | To stop the container: 62 | 63 | ```bash 64 | docker rm -f 65 | docker rmi -f 66 | ``` 67 | 68 | To update to a newer version of Builder, stop the existing container if it is running, build the container start it again. 69 | 70 | In both the builds, a persistent storage is kept. All the builds are stored in `/rvx-builder/revanced/`. 71 | -------------------------------------------------------------------------------- /android-interface.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | shopt -s extglob 4 | 5 | SCR_NAME_EXEC=$0 6 | SCR_NAME_EXEC_FP=$(realpath "$0") 7 | SCR_NAME=$(basename "$SCR_NAME_EXEC") 8 | SCR_NAME=${SCR_NAME%.*} 9 | RVB_DIR=$HOME/rvx-builder 10 | 11 | COLOR_OFF='\033[0m' 12 | COLOR_RED='\033[1;31m' 13 | 14 | help_info() { 15 | cat <> ../usr/etc/bash.bashrc 59 | echo "alias rvxre='./rvx-builder.sh reinstall && ./rvx-builder.sh run'" >> ../usr/etc/bash.bashrc 60 | echo "alias rvxup='./rvx-builder.sh update && ./rvx-builder.sh run'" >> ../usr/etc/bash.bashrc 61 | echo "alias opon='nano rvx-builder/options.json'" >> ../usr/etc/bash.bashrc 62 | } 63 | 64 | dload_and_install() { 65 | log "Downloading rvx-builder..." 66 | curl -sLo rvx-builder.zip https://github.com/inotia00/rvx-builder/archive/refs/heads/revanced-extended.zip 67 | log "Unzipping..." 68 | unzip -qqo rvx-builder.zip 69 | rm rvx-builder.zip 70 | mv rvx-builder-revanced-extended/{.[!.]*,*} . 71 | log "Installing packages..." 72 | npm install --omit=dev 73 | rmdir rvx-builder-revanced-extended 74 | [[ -z "$1" ]] && log "Done. Execute \`$SCR_NAME_EXEC run\` to launch the builder." 75 | } 76 | 77 | preflight() { 78 | setup_storage() { 79 | [[ ! -d "$HOME"/storage ]] && { 80 | log "You will now get a permission dialog to allow access to storage." 81 | log "This is needed in order to move the built APK (+ MicroG) to internal storage." 82 | sleep 5 83 | termux-setup-storage 84 | } || { 85 | log "Already gotten storage access." 86 | } 87 | } 88 | 89 | install_dependencies() { 90 | [[ -f "$RVB_DIR/settings.json" ]] && { 91 | log "Node.js and JDK already installed." 92 | return 93 | } 94 | log "Updating Termux and installing dependencies..." 95 | pkg update -y 96 | pkg install nodejs-lts openjdk-17 -y || { 97 | error "$COLOR_RED 98 | Failed to install Node.js and OpenJDK 17. 99 | Possible reasons (in the order of commonality): 100 | 1. Termux was downloaded from Play Store. Termux in Play Store is deprecated, and has packaging bugs. Please install it from F-Droid. 101 | 2. Mirrors are down at the moment. Try running \`termux-change-repo\`. 102 | 3. Internet connection is unstable. 103 | 4. Lack of free storage.$COLOR_OFF" n 2 104 | } 105 | } 106 | 107 | setup_storage 108 | install_dependencies 109 | 110 | [[ ! -d "$RVB_DIR" ]] && { 111 | set_alias 112 | log "rvx-builder not installed. Installing..." 113 | mkdir -p "$RVB_DIR" 114 | cd "$RVB_DIR" 115 | dload_and_install n 116 | } || { 117 | log "rvx-builder found." 118 | log "All checks done." 119 | } 120 | } 121 | 122 | run_builder() { 123 | preflight 124 | termux-wake-lock 125 | echo 126 | [[ "$1" == "--delete-cache" ]] || [[ "$1" == "--dc" ]] && { 127 | delete_cache 128 | } 129 | [[ "$1" == "--delete-cache-no-keystore" ]] || [[ "$1" == "--dcnk" ]] && { 130 | delete_cache_no_keystore 131 | } 132 | cd "$RVB_DIR" 133 | node . 134 | [[ "$1" == "--delete-cache-after" ]] || [[ "$1" == "--dca" ]] && { 135 | delete_cache 136 | } 137 | [[ "$1" == "--delete-cache-after-no-keystore" ]] || [[ "$1" == "--dcank" ]] && { 138 | delete_cache_no_keystore 139 | } 140 | termux-wake-unlock 141 | } 142 | 143 | delete_cache() { 144 | # Is this even called a cache? 145 | log "Deleting builder cache..." 146 | rm -rf "$RVB_DIR"/revanced 147 | } 148 | 149 | delete_cache_no_keystore() { 150 | log "Deleting builder cache preserving keystore..." 151 | mv "$RVB_DIR"/revanced/revanced.keystore "$HOME"/revanced.keystore 152 | rm -rf "$RVB_DIR"/revanced 153 | mkdir -p "$RVB_DIR"/revanced 154 | mv "$HOME"/revanced.keystore "$RVB_DIR"/revanced/revanced.keystore 155 | } 156 | 157 | reinstall_builder() { 158 | log "Deleting rvx-builder..." 159 | [[ "$1" != "--delete-keystore" ]] && { 160 | [[ -f "$RVB_DIR/revanced/revanced.keystore" ]] && { 161 | mv "$RVB_DIR"/revanced/revanced.keystore "$HOME"/revanced.keystore 162 | log "Preserving the keystore. If you do not want this, use the --delete-keystore flag." 163 | log "Execute \`$SCR_NAME_EXEC help\` for more info." 164 | } 165 | } 166 | rm -r "$RVB_DIR" 167 | mkdir -p "$RVB_DIR" 168 | [[ -f "$HOME/revanced.keystore" ]] && { 169 | log "Restoring the keystore..." 170 | mkdir -p "$RVB_DIR"/revanced 171 | mv "$HOME"/revanced.keystore "$RVB_DIR"/revanced/revanced.keystore 172 | } 173 | log "Reinstalling..." 174 | cd "$RVB_DIR" 175 | dload_and_install 176 | } 177 | 178 | update_builder() { 179 | log "Backing up some stuff..." 180 | [[ -d "$RVB_DIR/revanced" ]] && { 181 | mkdir -p "$HOME"/revanced_backup 182 | mv "$RVB_DIR"/revanced/* "$HOME"/revanced_backup 183 | } 184 | [[ -f "$RVB_DIR/settings.json" ]] && { 185 | mv "$RVB_DIR"/settings.json "$HOME"/settings.json 186 | } 187 | log "Deleting rvx-builder..." 188 | rm -r "$RVB_DIR" 189 | log "Restoring the backup..." 190 | mkdir -p "$RVB_DIR" 191 | [[ -d "$HOME/revanced_backup" ]] && { 192 | mkdir -p "$RVB_DIR"/revanced 193 | mv "$HOME"/revanced_backup/* "$RVB_DIR"/revanced 194 | } 195 | [[ -f "$HOME/settings.json" ]] && { 196 | mv "$HOME"/settings.json "$RVB_DIR"/settings.json 197 | } 198 | log "Updating rvx-builder..." 199 | cd "$RVB_DIR" 200 | dload_and_install n 201 | run_self_update 202 | } 203 | 204 | run_self_update() { 205 | log "Performing self-update..." 206 | 207 | # Download new version 208 | log "Downloading latest version..." 209 | ! curl -sLo "$SCR_NAME_EXEC_FP".tmp https://raw.githubusercontent.com/inotia00/rvx-builder/revanced-extended/android-interface.sh && { 210 | log "Failed: Error while trying to download new version!" 211 | error "File requested: https://raw.githubusercontent.com/inotia00/rvx-builder/revanced-extended/android-interface.sh" n 212 | } || log "Done." 213 | 214 | # Copy over modes from old version 215 | OCTAL_MODE=$(stat -c '%a' "$SCR_NAME_EXEC_FP") 216 | ! chmod "$OCTAL_MODE" "$SCR_NAME_EXEC_FP.tmp" && error "Failed: Error while trying to set mode on $SCR_NAME_EXEC.tmp." n 217 | 218 | # Spawn update script 219 | cat > updateScript.sh << EOF 220 | #!/bin/bash 221 | 222 | # Overwrite old file with new 223 | mv "$SCR_NAME_EXEC_FP.tmp" "$SCR_NAME_EXEC_FP" && { 224 | echo -e "[$SCR_NAME] Done. Execute '$SCR_NAME_EXEC run' to launch the builder." 225 | rm \$0 226 | } || { 227 | echo "[$SCR_NAME] Failed!" 228 | } 229 | EOF 230 | 231 | log "Running update process..." 232 | exec /bin/bash updateScript.sh 233 | } 234 | 235 | main() { 236 | if [[ -z "$@" ]]; then 237 | run_builder 238 | elif [[ $# -gt 2 ]]; then 239 | error "2 optional arguments acceptable, got $#." 240 | else 241 | case $1 in 242 | run) 243 | run_builder "$2" 244 | ;; 245 | reinstall) 246 | reinstall_builder "$2" 247 | ;; 248 | update) 249 | update_builder 250 | ;; 251 | help) 252 | help_info 253 | ;; 254 | *) 255 | error "Invalid argument(s): $@." 256 | ;; 257 | esac 258 | fi 259 | } 260 | 261 | main $@ 262 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | rvx-builder: 4 | container_name: rvx-builder 5 | restart: unless-stopped 6 | build: 7 | context: ./ 8 | dockerfile: Dockerfile 9 | volumes: 10 | - ./revanced/:/app/rvx-builder/revanced/ 11 | ports: 12 | - 8000:8000 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require('node:http'); 2 | const { join } = require('node:path'); 3 | 4 | const exec = require('./utils/promisifiedExec.js'); 5 | const uploadAPKFile = require('./utils/uploadAPKFile.js'); 6 | 7 | const Express = require('express'); 8 | const fileUpload = require('express-fileupload'); 9 | const { WebSocketServer } = require('ws'); 10 | const open_ = require('open'); 11 | const pf = require('portfinder'); 12 | 13 | const fkill = require('fkill'); 14 | 15 | const { 16 | checkFileAlreadyExists, 17 | checkForUpdates, 18 | fetchWithUserAgent, 19 | getApp, 20 | getAppVersion, 21 | getDevices, 22 | getPatches, 23 | getSettings, 24 | installReVanced, 25 | patchApp, 26 | resetPatchOptions, 27 | resetSettings, 28 | selectApp, 29 | selectAppVersion, 30 | selectPatches, 31 | setDevice, 32 | setSettings, 33 | updateFiles 34 | } = require('./wsEvents/index.js'); 35 | 36 | const app = Express(); 37 | const server = createServer(app); 38 | const wsServer = new WebSocketServer({ server }); 39 | const wsClients = []; 40 | 41 | app.use(fileUpload()); 42 | app.use(Express.static(join(__dirname, 'public'))); 43 | app.get('/revanced.apk', (_, res) => { 44 | const file = join(process.cwd(), 'revanced', global.outputName); 45 | 46 | res.download(file); 47 | }); 48 | 49 | app.post('/uploadApk', (req, res) => { 50 | req.socket.setTimeout(60000 * 60); 51 | req.setTimeout(60000 * 60); 52 | uploadAPKFile(req, res, wsClients); 53 | }); 54 | 55 | /** 56 | * @param {number} port 57 | */ 58 | const open = async (port) => { 59 | if (process.platform === 'android') 60 | await exec(`termux-open-url http://localhost:${port}`); 61 | else await open_(`http://localhost:${port}`); 62 | }; 63 | 64 | /** 65 | * @param {string} msg 66 | */ 67 | const log = (msg, newline = true, tag = true) => { 68 | if (newline) console.log(`${tag ? '[builder] ' : ''}${msg}`); 69 | else process.stdout.write(`${tag ? '[builder] ' : ''}${msg} `); 70 | }; 71 | 72 | /** 73 | * @param {number} port 74 | */ 75 | const listen = (port) => { 76 | server.listen(port, async () => { 77 | if (process.argv.includes('--no-open')) 78 | log(`The webserver is now running at http://localhost:${port}`); 79 | else { 80 | log('The webserver is now running!'); 81 | 82 | try { 83 | log('Opening the app in the default browser...', false); 84 | 85 | await open(port); 86 | 87 | log('Done, check if a browser window has opened', true, false); 88 | } catch { 89 | log( 90 | `Failed. Open up http://localhost:${port} manually in your browser.`, 91 | true, 92 | false 93 | ); 94 | } 95 | } 96 | }); 97 | }; 98 | 99 | /** 100 | * @param {import('http').Server} svr 101 | */ 102 | const cleanExit = async (svr) => { 103 | log('Killing any dangling processes...', false); 104 | 105 | try { 106 | await fkill(['adb', 'java', 'aapt2'], { 107 | forceAfterTimeout: 3000, 108 | tree: true, 109 | silent: true 110 | }); 111 | log('Done.', true, false); 112 | } catch (error) { 113 | log('Failed.', true, false); 114 | log( 115 | 'If there are certain processes still running, you can kill them manually' 116 | ); 117 | log(error?.stack, true, false); 118 | } 119 | 120 | log('Stopping the server...', false); 121 | 122 | svr.close(() => log('Done', true, false)); 123 | setTimeout(() => process.exit(0), 2_500); 124 | }; 125 | 126 | pf.getPortPromise() 127 | .then((freePort) => { 128 | log(`Listening at port ${freePort}`); 129 | listen(freePort); 130 | }) 131 | .catch((err) => { 132 | log(`Unable to determine free ports.\nReason: ${err}`); 133 | log('Falling back to 8080.'); 134 | listen(8080); 135 | }); 136 | 137 | process.on('uncaughtException', (reason) => { 138 | log(`An error occured.\n${reason.stack}`); 139 | log( 140 | 'Please report this bug here: https://github.com/inotia00/rvx-builder/issues.' 141 | ); 142 | }); 143 | 144 | process.on('unhandledRejection', (reason) => { 145 | log(`An error occured.\n${reason.stack}`); 146 | log( 147 | 'Please report this bug here: https://github.com/inotia00/rvx-builder/issues.' 148 | ); 149 | 150 | for (const wsClient of wsClients) { 151 | wsClient.send( 152 | JSON.stringify({ 153 | event: 'error', 154 | error: encodeURIComponent( 155 | `An error occured:\n${reason.stack}\nPlease report this bug here: https://github.com/inotia00/rvx-builder/issues.` 156 | ) 157 | }) 158 | ); 159 | } 160 | }); 161 | 162 | process.on('SIGTERM', () => cleanExit(server)); 163 | process.on('SIGINT', () => cleanExit(server)); 164 | 165 | // The websocket server 166 | wsServer.on('connection', (ws) => { 167 | wsClients.push(ws); 168 | ws.on('message', async (msg) => { 169 | /** @type {Record} */ 170 | const message = JSON.parse(msg); 171 | 172 | // Theres no file handler, soo... 173 | 174 | switch (message.event) { 175 | case 'checkFileAlreadyExists': 176 | checkFileAlreadyExists(ws); 177 | break; 178 | case 'checkForUpdates': 179 | await checkForUpdates(ws); 180 | break; 181 | case 'fetchWithUserAgent': 182 | await fetchWithUserAgent(ws); 183 | break; 184 | case 'getAppList': 185 | await getApp(ws); 186 | break; 187 | case 'getAppVersion': 188 | await getAppVersion(ws, message); 189 | break; 190 | case 'getDevices': 191 | await getDevices(ws); 192 | break; 193 | case 'getPatches': 194 | await getPatches(ws, message); 195 | break; 196 | case 'getSettings': 197 | await getSettings(ws); 198 | break; 199 | case 'installReVanced': 200 | await installReVanced(ws); 201 | break; 202 | case 'patchApp': 203 | await patchApp(ws, message); 204 | break; 205 | case 'resetPatchOptions': 206 | await resetPatchOptions(ws); 207 | break; 208 | case 'resetSettings': 209 | await resetSettings(ws); 210 | break; 211 | case 'selectApp': 212 | selectApp(message); 213 | break; 214 | case 'selectAppVersion': 215 | await selectAppVersion(message, ws); 216 | break; 217 | case 'selectPatches': 218 | selectPatches(message); 219 | break; 220 | case 'setDevice': 221 | setDevice(message); 222 | break; 223 | case 'setSettings': 224 | await setSettings(message); 225 | break; 226 | case 'updateFiles': 227 | await updateFiles(ws); 228 | break; 229 | case 'exit': 230 | process.kill(process.pid, 'SIGTERM'); 231 | } 232 | }); 233 | }); 234 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rvx-builder", 3 | "version": "1.0.0", 4 | "description": "", 5 | "bin": "index.js", 6 | "main": "index.js", 7 | "type": "commonjs", 8 | "pkg": { 9 | "assets": [ 10 | "public/**/*" 11 | ] 12 | }, 13 | "scripts": { 14 | "start": "node .", 15 | "fmt": "prettier --write . && eslint --fix ." 16 | }, 17 | "keywords": [], 18 | "author": "", 19 | "license": "ISC", 20 | "dependencies": { 21 | "app-info-parser": "^1.1.6", 22 | "cheerio": "^1.0.0", 23 | "express": "^4.21.2", 24 | "express-fileupload": "^1.5.1", 25 | "fkill": "^7.2.1", 26 | "node-fetch": "^2.7.0", 27 | "node-fetch-progress": "^1.0.2", 28 | "open": "^8.4.2", 29 | "portfinder": "^1.0.32", 30 | "prettier": "^3.4.2", 31 | "semver": "^7.6.0", 32 | "tar": "^6.2.1", 33 | "ws": "^8.18.0" 34 | }, 35 | "devDependencies": { 36 | "eslint": "^9.16.0" 37 | }, 38 | "engines": { 39 | "node": ">=18" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/about/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | About 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |

About

19 |
20 |
21 |
22 |

ReVanced Builder

23 |

24 | ReVanced Builder is a simple tool built with NodeJS to patch 25 | ReVanced-supported applications. It ships with a beginner-friendly 26 | UI so that everyone can use it easily. Check out 27 | GitHub Readme 32 | for more information. 33 |

34 |

35 | Found a Bug? 36 | Report here! 41 |

42 |

Contributors

43 |

We are highly grateful to them!

44 | 50 | 55 | 60 | 61 |
62 |
63 |
64 |
65 | 75 | 78 | 79 | 80 |
81 |
82 | 83 | 84 | -------------------------------------------------------------------------------- /public/apps.js: -------------------------------------------------------------------------------- 1 | /* global ws, sendCommand */ 2 | 3 | ws.onopen = () => sendCommand({ event: 'getAppList' }); 4 | const apkField = document.getElementById('apkUpload'); 5 | apkField.addEventListener('change', () => { 6 | if (!apkField.value == '') { 7 | postFile(); 8 | const info = document.createElement('div'); 9 | info.className = 'inf'; 10 | info.innerHTML = '

Please wait...

'; 11 | document.getElementById('content').appendChild(info); 12 | } 13 | }); 14 | function postFile() { 15 | document.querySelector('ul').style.display = 'none'; 16 | document.querySelector('.upl').style.display = 'none'; 17 | document.querySelector('#search').style.display = 'none'; 18 | document.querySelector('h1').style.display = 'none'; 19 | const stat = document.querySelector('.prog'); 20 | stat.style.display = 'block'; 21 | // stackoverflow 22 | let formdata = new FormData(); 23 | 24 | formdata.append('apk', apkField.files[0]); 25 | let request = new XMLHttpRequest(); 26 | 27 | request.upload.addEventListener('progress', function (e) { 28 | let file1Size = apkField.files[0].size; 29 | 30 | if (e.loaded <= file1Size) { 31 | let percent = Math.round((e.loaded / file1Size) * 100); 32 | stat.innerHTML = `${percent + '%'} Uploading APK...`; 33 | } 34 | 35 | if (e.loaded == e.total) { 36 | stat.innerHTML = `Upload Successful`; 37 | } 38 | }); 39 | 40 | request.open('post', '/uploadApk'); 41 | request.timeout = 60 * 10 * 1000; 42 | request.send(formdata); 43 | } 44 | 45 | // eslint-disable-next-line no-unused-vars 46 | function oldState(e) { 47 | e.style.display = 'none'; 48 | document.querySelector('ul').style.display = 'block'; 49 | document.querySelector('h1').style.display = 'block'; 50 | document.querySelector('.prog').style.display = 'none'; 51 | document.querySelector('#search').style.display = null; 52 | document.querySelector('.upl').style.display = 'inline-flex'; 53 | document.querySelector('.inf').remove(); 54 | document.getElementById('continue').setAttribute('onClick', 'setApp()'); 55 | document.getElementById('uploadForm').reset(); 56 | } 57 | -------------------------------------------------------------------------------- /public/apps/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | App Selector 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |

19 | Select the app to patch 20 |

21 |

22 |
23 | 24 | 25 |
26 | 29 | 30 | 36 |
37 |
38 |
39 |
    40 |
    41 |
    42 |
    43 |
    44 | 54 | 63 | 66 | 67 | 70 |
    71 |
    72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /public/colors.js: -------------------------------------------------------------------------------- 1 | const colorNow = localStorage.getItem('theme') ?? '#4873b3'; 2 | document.documentElement.style.setProperty('--accentColor', colorNow); 3 | -------------------------------------------------------------------------------- /public/dependencies/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Downloading dependencies 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
    16 |
    17 |
    18 |

    Downloading dependencies

    19 | Please wait while the app downloads the required 21 | dependencies. 23 |
    24 |
    25 |
    26 | 27 |
    28 |
    29 | 30 |
    31 |
    32 | 42 | 52 | 59 | 60 | 65 | 66 | 73 |
    74 |
    75 | 76 | 79 | 80 | -------------------------------------------------------------------------------- /public/devices/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Device Selector 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
    16 |
    17 |
    18 |

    19 | Select the device(s) 20 | you want to install ReVanced to 21 |

    22 | ReVanced Builder has detected that you have more than one device 24 | plugged in.
    Please select the device(s) that you want to 25 | install ReVanced.
    27 |
    28 |
    29 |
    30 |
      31 |
      32 |
      33 |
      34 |
      35 | 45 | 55 | 62 | 63 | 64 | 67 |
      68 |
      69 | 70 | 73 | 74 | -------------------------------------------------------------------------------- /public/exited/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Exited 7 | 8 | 9 | 10 | 11 | 12 | 13 |
      14 |
      15 |
      16 |

      Exited

      17 | You can now close this tab/window. 18 |
      19 |
      20 |
      21 | 22 | 27 | 28 | -------------------------------------------------------------------------------- /public/failure/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Failure 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
      16 |
      17 |
      18 |

      Failure

      19 |
      20 |
      21 |
      22 | 23 |
      24 |
      25 |
      26 |
      27 | 37 | 46 | 49 | 50 | 51 | 52 | 53 |
      54 |
      55 | 56 | 57 | 66 | 67 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ReVanced Builder by Reis 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
      16 | 17 | 21 | 25 | 29 | 33 | 37 | 38 | 42 | 46 | 50 | 51 | 55 | 56 | 60 | 64 | 68 | 69 | 73 | 77 | 81 | 85 | 89 | 93 | 97 | 101 | 102 | 103 | 104 | 105 | 108 | 109 | 110 |

      111 | Welcome to RVX Builder! 114 |

      115 |

      116 | A simple tool to patch ReVanced Extended supported applications 117 |

      118 | 125 |
      126 | 127 | 128 | 131 | 135 | 136 |
      129 |

      Status

      130 |
      132 | Latest: Loading..
      Installed: 133 | Loading.. 134 |
      137 |
      138 |
      139 | Your current version of builder is up-to-date. 142 |
      143 | Your current version of builder is outdated and no longer 145 | supported.
      152 |
      153 |
      154 |
      155 | 165 | 174 | 177 |
      178 |
      179 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /public/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | 3 | const WS_URI = `${window?.location?.protocol === 'https:' ? 'wss' : 'ws'}://${ 4 | window?.location?.host ?? 'localhost:8080' 5 | }`; 6 | const ws = new WebSocket(WS_URI); 7 | 8 | let currentFile; 9 | let alreadyAddedLog = false; 10 | let isDownloading = false; 11 | let hasFinished = false; 12 | let arch; 13 | let versionChoosen; 14 | let uploadedApk = false; 15 | 16 | if (localStorage.getItem('black-theme')) { 17 | document.documentElement.classList.add('black'); 18 | } 19 | 20 | function sendCommand(args) { 21 | ws.send(JSON.stringify(args)); 22 | } 23 | 24 | function setApp() { 25 | const appChecked = document.querySelector('input[name="app"]:checked'); 26 | 27 | if (appChecked === null) return alert("You didn't select an app to patch!"); 28 | 29 | sendCommand({ 30 | event: 'selectApp', 31 | selectedApp: { 32 | packageName: appChecked.value, 33 | link: appChecked.attributes.link.value, 34 | appName: appChecked.attributes.appName.value 35 | } 36 | }); 37 | 38 | location.href = '/patches'; 39 | } 40 | 41 | function loadPatches() { 42 | sendCommand({ event: 'getPatches', showUniversalPatches: localStorage.getItem('universal-patches') }); 43 | } 44 | 45 | function updateFiles() { 46 | sendCommand({ event: 'updateFiles' }); 47 | } 48 | 49 | /** 50 | * @param {boolean} bool 51 | */ 52 | function toggle(bool) { 53 | for (const checkbox of document.getElementsByClassName('select')) { 54 | if (bool && checkbox.getAttribute('data-excluded') !== '0') continue; 55 | 56 | checkbox.checked = bool; 57 | } 58 | } 59 | 60 | function goToPatches() { 61 | if (hasFinished) location.href = '/patches'; 62 | } 63 | 64 | function checkForUpdates() { 65 | sendCommand({ event: 'checkForUpdates' }); 66 | } 67 | 68 | function setPatches() { 69 | const patchElementList = Array.from(document.querySelectorAll('.select')); 70 | 71 | let selectedPatchList = []; 72 | let excludedPatchList = []; 73 | 74 | for (const patchElement of patchElementList) 75 | (patchElement.checked ? selectedPatchList : excludedPatchList).push( 76 | patchElement 77 | ); 78 | 79 | if (selectedPatchList.length === 0) 80 | return alert("You haven't selected any patches."); 81 | 82 | selectedPatchList = selectedPatchList.map((x) => 83 | x.getAttribute('data-patch-name') 84 | ); 85 | excludedPatchList = excludedPatchList.map((x) => 86 | x.getAttribute('data-patch-name') 87 | ); 88 | 89 | sendCommand({ 90 | event: 'selectPatches', 91 | selectedPatches: selectedPatchList, 92 | excludedPatches: excludedPatchList 93 | }); 94 | if (uploadedApk) { 95 | location.href = '/patch'; 96 | } else location.href = '/versions'; 97 | } 98 | 99 | /** 100 | * @param {string} arch 101 | * @param {string} version 102 | */ 103 | function setAppVersion(arch, version) { 104 | if (!isDownloading) { 105 | const versionChecked = document.querySelector( 106 | 'input[name="version"]:checked' 107 | ); 108 | 109 | if (arch == null && versionChecked === null) 110 | return alert("You didn't select an app version!"); 111 | 112 | if (versionChecked !== null) { 113 | if ( 114 | versionChecked.hasAttribute('data-recommended') && 115 | versionChecked.getAttribute('data-recommended') !== '1' 116 | ) { 117 | const alertVersion = confirm( 118 | "**Non-supported version selected**\n***Are you sure?***\nThis version isn't supported, do you really want to use this version?" 119 | ); 120 | 121 | if (!alertVersion) return; 122 | } 123 | 124 | if (versionChecked.getAttribute('data-beta') !== '0') { 125 | const alertBetaVersion = confirm( 126 | "**Non-supported version selected**\n***Are you sure?***\nThis version isn't supported, do you really want to use this version?" 127 | ); 128 | 129 | if (!alertBetaVersion) return; 130 | } 131 | } 132 | 133 | document.getElementById('continue').classList.add('disabled'); 134 | 135 | sendCommand({ 136 | event: 'selectAppVersion', 137 | versionChoosen: version ?? versionChecked.value, 138 | arch 139 | }); 140 | 141 | document.getElementsByTagName('header')[0].innerHTML = 142 | '

      Downloading APK

      '; 143 | document.getElementById('content').innerHTML = ''; 144 | document.getElementsByTagName('main')[0].innerHTML += 145 | ''; 146 | isDownloading = true; 147 | } else { 148 | if (!hasFinished) return alert("Downloading process hasn't finished yet."); 149 | 150 | location.href = '/patch'; 151 | } 152 | } 153 | 154 | /** 155 | * @param {boolean} isRooted 156 | */ 157 | function getAppVersions(isRooted, page = 1) { 158 | document.getElementsByTagName('header')[0].innerHTML = ` 159 |

      Select the version you want to download

      160 | Versions marked as beta might have bugs or can be unstable, unless marked as supported 161 | ${ 162 | isRooted 163 | ? 'You are building rooted ReVanced Extended, ReVanced Extended Builder will automatically download the correct version for you.
      If you didn\'t intend on doing a rooted build, include all "Root required to exclude" patches' 164 | : '' 165 | } 166 | `; 167 | 168 | const continueButton = document.getElementById('continue'); 169 | const backButton = document.getElementById('back'); 170 | 171 | continueButton.innerHTML = 'Continue'; 172 | continueButton.onclick = () => setAppVersion(); 173 | backButton.innerHTML = 'Back'; 174 | backButton.onclick = () => history.back(); 175 | 176 | if (page < 1) page = 1; 177 | 178 | sendCommand({ event: 'getAppVersion', checkVer: true, page }); 179 | } 180 | 181 | function buildReVanced() { 182 | sendCommand({ event: 'patchApp', ripLibs: localStorage.getItem('rip-libs') }); 183 | } 184 | 185 | function getAlreadyExists() { 186 | sendCommand({ event: 'checkFileAlreadyExists' }); 187 | } 188 | 189 | /** 190 | * @param {string} phrase 191 | */ 192 | function toTitleCase(phrase) { 193 | return phrase 194 | .split('-') 195 | .map((word) => word[0].toUpperCase() + word.slice(1)) 196 | .join(' '); 197 | } 198 | 199 | function exitApp() { 200 | location.href = '/exited'; 201 | } 202 | 203 | window.addEventListener('keypress', (e) => { 204 | if (e.key !== 'Enter') return; 205 | 206 | e.preventDefault(); 207 | 208 | document.getElementById('continue').click(); 209 | }); 210 | 211 | function setDevice() { 212 | const deviceElementList = Array.from(document.querySelectorAll('.select')); 213 | 214 | let selectedDeviceList = []; 215 | 216 | for (const deviceElement of deviceElementList) 217 | (deviceElement.checked ? selectedDeviceList : []).push(deviceElement); 218 | 219 | if (selectedDeviceList.length === 0) 220 | return alert("You haven't selected any devices."); 221 | 222 | selectedDeviceList = selectedDeviceList.map((x) => x.getAttribute('value')); 223 | 224 | sendCommand({ 225 | event: 'setDevice', 226 | devices: selectedDeviceList ?? [] 227 | }); 228 | 229 | location.href = '/patches'; 230 | } 231 | 232 | function getDevices() { 233 | sendCommand({ event: 'getDevices' }); 234 | } 235 | 236 | function installReVanced() { 237 | sendCommand({ event: 'installReVanced' }); 238 | } 239 | 240 | function addSearch(isPatches) { 241 | document.getElementById('search').addEventListener('keyup', () => { 242 | const searchText = document.getElementById('search').value.toLowerCase(); 243 | 244 | Array.from(document.getElementsByTagName('li')).forEach( 245 | (x) => (x.style.display = 'none') 246 | ); 247 | 248 | if (isPatches) { 249 | Array.from(document.getElementsByClassName('patchName')) 250 | .filter((x) => x.innerText.toLowerCase().includes(searchText)) 251 | .forEach( 252 | (x) => (x.parentNode.parentNode.parentNode.style.display = 'flex') 253 | ); 254 | } else { 255 | Array.from(document.getElementsByClassName('appName')) 256 | .filter((x) => x.innerText.toLowerCase().includes(searchText)) 257 | .forEach((x) => (x.parentNode.style.display = 'flex')); 258 | } 259 | }); 260 | } 261 | 262 | function resetPatchOptions() { 263 | sendCommand({ event: 'resetPatchOptions' }); 264 | } 265 | 266 | function resetSettings() { 267 | sendCommand({ event: 'resetSettings' }); 268 | setSourcesRVX(); 269 | } 270 | 271 | function setSources() { 272 | const cliOrg = document.getElementById('cli-org').value; 273 | const cliSrc = document.getElementById('cli-src').value; 274 | const cli = `${cliOrg}/${cliSrc}`; 275 | 276 | const cli4 = document.getElementById('cli4').value; 277 | 278 | const patchesOrg = document.getElementById('patch-org').value; 279 | const patchesSrc = document.getElementById('patch-src').value; 280 | const patches = `${patchesOrg}/${patchesSrc}`; 281 | 282 | const integrationsOrg = document.getElementById('integrations-org').value; 283 | const integrationsSrc = document.getElementById('integrations-src').value; 284 | const integrations = `${integrationsOrg}/${integrationsSrc}`; 285 | 286 | const microgOrg = document.getElementById('microg-org').value; 287 | const microgSrc = document.getElementById('microg-src').value; 288 | const microg = `${microgOrg}/${microgSrc}`; 289 | 290 | const prereleases = localStorage.getItem('pre-releases') + ''; 291 | 292 | sendCommand({ 293 | event: 'setSettings', 294 | settings: { 295 | cli, 296 | patches, 297 | integrations, 298 | microg, 299 | prereleases, 300 | cli4 301 | } 302 | }); 303 | } 304 | 305 | function setSourcesRVX() { 306 | document.getElementById('cli-org').value = 'inotia00'; 307 | document.getElementById('cli-src').value = 'revanced-cli'; 308 | 309 | document.getElementById('cli4').value = 'false'; 310 | 311 | document.getElementById('patch-org').value = 'inotia00'; 312 | document.getElementById('patch-src').value = 'revanced-patches'; 313 | 314 | document.getElementById('integrations-org').value = 'inotia00'; 315 | document.getElementById('integrations-src').value = 'revanced-integrations'; 316 | 317 | document.getElementById('microg-org').value = 'ReVanced'; 318 | document.getElementById('microg-src').value = 'GmsCore'; 319 | 320 | setSources(); 321 | } 322 | 323 | function setSourcesRVX_anddea() { 324 | document.getElementById('cli-org').value = 'inotia00'; 325 | document.getElementById('cli-src').value = 'revanced-cli'; 326 | 327 | document.getElementById('cli4').value = 'false'; 328 | 329 | document.getElementById('patch-org').value = 'anddea'; 330 | document.getElementById('patch-src').value = 'revanced-patches'; 331 | 332 | document.getElementById('integrations-org').value = 'anddea'; 333 | document.getElementById('integrations-src').value = 'revanced-integrations'; 334 | 335 | setSources(); 336 | } 337 | 338 | function setSourcesReVanced() { 339 | document.getElementById('cli-org').value = 'inotia00'; 340 | document.getElementById('cli-src').value = 'revanced-cli'; 341 | 342 | document.getElementById('cli4').value = 'false'; 343 | 344 | document.getElementById('patch-org').value = 'revanced'; 345 | document.getElementById('patch-src').value = 'revanced-patches'; 346 | 347 | document.getElementById('integrations-org').value = 'revanced'; 348 | document.getElementById('integrations-src').value = 'revanced-integrations'; 349 | 350 | setSources(); 351 | } 352 | 353 | function setSourcesPiko() { 354 | document.getElementById('cli-org').value = 'inotia00'; 355 | document.getElementById('cli-src').value = 'revanced-cli'; 356 | 357 | document.getElementById('cli4').value = 'true'; 358 | 359 | document.getElementById('patch-org').value = 'crimera'; 360 | document.getElementById('patch-src').value = 'piko'; 361 | 362 | document.getElementById('integrations-org').value = 'crimera'; 363 | document.getElementById('integrations-src').value = 'revanced-integrations'; 364 | 365 | setSources(); 366 | } 367 | 368 | ws.onmessage = (msg) => { 369 | /** @type {Record} */ 370 | const message = JSON.parse(msg.data); 371 | 372 | switch (message.event) { 373 | case 'patchList': 374 | { 375 | uploadedApk = message.uploadedApk; 376 | const len = message.patchList.length; 377 | 378 | const patchListElement = document.getElementById('patchList'); 379 | 380 | for (let i = 0; i < len; i++) { 381 | const patch = message.patchList[i]; 382 | 383 | patchListElement.innerHTML += `
    • 384 | 387 | 400 |
    • `; 401 | } 402 | 403 | addSearch(true); 404 | 405 | Array.from(document.getElementsByClassName('select')) 406 | .filter((patch) => 407 | message.rememberedPatchList.includes( 408 | patch.getAttribute('data-patch-name') 409 | ) 410 | ) 411 | .forEach((patch) => (patch.checked = true)); 412 | } 413 | break; 414 | case 'downloadingFile': 415 | { 416 | isDownloading = true; 417 | 418 | let logElement = document.getElementsByClassName('log')[0]; 419 | 420 | if (!logElement) { 421 | document.getElementById('content').innerHTML = 422 | ''; 423 | document.getElementsByTagName('main')[0].innerHTML += 424 | ''; 425 | logElement = document.getElementsByClassName('log')[0]; 426 | } 427 | 428 | const downloadMessage = `[builder] Downloading ${message.name}...
      `; 429 | 430 | if (currentFile !== message.name) { 431 | currentFile = message.name; 432 | logElement.innerHTML += downloadMessage; 433 | } 434 | 435 | document.getElementsByTagName('progress')[0].value = ( 436 | message.percentage / 100 437 | ).toString(); 438 | } 439 | break; 440 | case 'mergingFile': 441 | document.getElementsByClassName('log')[0].innerHTML += 442 | '[builder] Merging files...
      '; 443 | 444 | break; 445 | case 'finished': 446 | hasFinished = true; 447 | 448 | try { 449 | document.getElementById('resetoptions').classList.remove('disabled'); 450 | } catch (_) {} 451 | document.getElementById('continue').classList.remove('disabled'); 452 | if (localStorage.getItem('auto-next')) { 453 | document.getElementById('continue').click(); 454 | } 455 | document.getElementsByClassName('log')[0].innerHTML += 456 | '[builder] Finished downloading files.
      '; 457 | break; 458 | case 'appVersions': 459 | { 460 | const len = message.versionList.length; 461 | 462 | const versionsElement = document.getElementById('versions'); 463 | versionsElement.innerHTML = ''; 464 | 465 | versionsElement.innerHTML += ` 466 |
    • 467 | ${ 468 | message.page != 1 469 | ? `` 472 | : '' 473 | } 474 | 477 |
    • `; 478 | 479 | for (let i = 0; i < len; i++) { 480 | const version = message.versionList[i]; 481 | const noRec = version.recommended == 'NOREC'; 482 | const recommended = version.recommended ? 1 : 0; 483 | const autoSelect = message.supported == 'C' ? version.version : message.supported; 484 | 485 | versionsElement.innerHTML += ` 486 | ${ 487 | message.page == 1 && i == 0 488 | ? `
    • 491 |
    • ` 492 | : '' 493 | }`; 494 | 495 | versionsElement.innerHTML += ` 496 |
    • 497 | 502 |
    • `; 507 | } 508 | 509 | if ( 510 | message.selectedApp === 'com.google.android.apps.youtube.music' && 511 | !message.foundDevice 512 | ) 513 | document.getElementById('continue').onclick = () => { 514 | const version = document.querySelector( 515 | 'input[name="version"]:checked' 516 | ).value; 517 | 518 | document.getElementsByTagName('header')[0].innerHTML = ` 519 |

      Please select the architecture

      520 | YouTube Music APKs only have specific architecture APKs. 521 |
      If you don't know which one to choose, either look at your devices architecture using CPU-Z or select Arm64.
      `; 522 | document.getElementById('versions').innerHTML = ` 523 |
    • 524 | 525 |
    • 526 |
    • 527 | 528 |
    • `; 529 | document.getElementById('continue').onclick = () => { 530 | if (isDownloading && hasFinished) location.href = '/patch'; 531 | 532 | document.getElementById('continue').classList.add('disabled'); 533 | 534 | setAppVersion( 535 | document.querySelector('input[name="arch"]:checked').value, 536 | version 537 | ); 538 | }; 539 | }; 540 | } 541 | break; 542 | case 'installingStockApp': 543 | { 544 | if (message.status === 'DOWNLOAD_STARTED') { 545 | document.getElementsByTagName('header')[0].innerHTML = 546 | '

      Downloading APK

      '; 547 | document.getElementById('content').innerHTML = 548 | ''; 549 | document.getElementsByTagName('main')[0].innerHTML += 550 | ''; 551 | isDownloading = true; 552 | document.getElementById('continue').classList.add('disabled'); 553 | } else if (message.status === 'DOWNLOAD_COMPLETE') { 554 | document.getElementById('continue').classList.add('disabled'); 555 | isDownloading = false; 556 | document.getElementsByClassName( 557 | 'log' 558 | )[0].innerHTML += `[builder] Uninstalling the stock app...
      `; 559 | } else if (message.status === 'UNINSTALL_COMPLETE') { 560 | document.getElementsByClassName( 561 | 'log' 562 | )[0].innerHTML += `[builder] Installing the downloaded (stock) APK...
      `; 563 | } else if (message.status === 'ALL_DONE') { 564 | document.getElementsByClassName( 565 | 'log' 566 | )[0].innerHTML += `[builder] Complete.
      `; 567 | document.getElementById('continue').classList.remove('disabled'); 568 | document.getElementById('continue').onclick = () => { 569 | location.href = '/patch'; 570 | }; 571 | } 572 | } 573 | break; 574 | case 'patchLog': 575 | { 576 | const logLevel = message.log.includes('WARNING') 577 | ? 'warn' 578 | : message.log.includes('SEVERE') 579 | ? 'error' 580 | : 'info'; 581 | 582 | document.getElementsByClassName( 583 | 'log' 584 | )[0].innerHTML += `[builder] ${message.log}
      `; 585 | 586 | const logWrap = document.getElementById('content--wrapper'); 587 | logWrap.scrollTop = logWrap.scrollHeight; 588 | } 589 | break; 590 | case 'fileExists': 591 | { 592 | // TODO: on a root install, if the file already exists and the user selects yes it skips checking if a device is plugged in 593 | document.getElementsByTagName('header')[0].innerHTML = ` 594 |

      Use already downloaded APK?

      595 | The APK already exists in the revanced folder.${ 596 | message.isRooted ? ' ' : '
      ' 597 | }Do you want to use it?${ 598 | message.isRooted 599 | ? '
      (Saying no is supported for rooted building)
      If you didn\'t intend on doing a rooted build, include all "Root required to exclude" patches' 600 | : '' 601 | }
      `; 602 | 603 | const continueButton = document.getElementById('continue'); 604 | const backButton = document.getElementById('back'); 605 | 606 | continueButton.innerHTML = 'Yes'; 607 | continueButton.onclick = () => { 608 | location.href = '/patch'; 609 | }; 610 | backButton.innerHTML = 'No'; 611 | backButton.onclick = () => getAppVersions(message.isRooted); 612 | } 613 | break; 614 | case 'fileDoesntExist': 615 | getAppVersions(message.isRooted); 616 | break; 617 | case 'buildFinished': 618 | { 619 | if (message.install) location.href = '/installer'; 620 | document.getElementsByTagName('header')[0].innerHTML = 621 | '

      Finished

      '; 622 | 623 | const firstFooterElement = document.getElementsByTagName('footer')[0]; 624 | 625 | if (!WS_URI.startsWith('ws://localhost')) 626 | firstFooterElement.innerHTML += 627 | ''; 628 | 629 | firstFooterElement.innerHTML += 630 | ''; 631 | } 632 | break; 633 | case 'error': 634 | location.href = `/failure?error=${message.error}`; 635 | break; 636 | case 'notUpToDate': 637 | document.getElementById( 638 | 'builderVersion' 639 | ).innerHTML = `${message.builderVersion}`; 640 | document.getElementById( 641 | 'currentVersion' 642 | ).innerHTML = `${message.currentVersion}`; 643 | document.querySelector('.updater .latest').style.display = 'none'; 644 | document.querySelector('.updater .outdated').style.display = 'block'; 645 | document.querySelector('.updater').style.display = 'block'; 646 | break; 647 | case 'upToDate': 648 | document.getElementById( 649 | 'builderVersion' 650 | ).innerHTML = `${message.currentVersion}`; 651 | document.getElementById( 652 | 'currentVersion' 653 | ).innerHTML = `${message.currentVersion}`; 654 | document.querySelector('.updater').style.display = 'block'; 655 | break; 656 | case 'multipleDevices': 657 | location.href = '/devices'; 658 | break; 659 | case 'devices': { 660 | const len = message.devices.length; 661 | 662 | for (let i = 0; i < len; i++) { 663 | const device = message.devices[i]; 664 | 665 | document.getElementById('devices').innerHTML += ` 666 |
    • 667 | 668 | 671 |
    • `; 672 | } 673 | break; 674 | } 675 | case 'askRootVersion': 676 | { 677 | const confirmVer = confirm( 678 | `**Non Supported Version**\nYour device has a non supported version. This means you have to let the builder replace the stock YouTube with a supported version.\nContinue?` 679 | ); 680 | 681 | if (confirmVer) 682 | return sendCommand({ 683 | event: 'getAppVersion', 684 | installLatestRecommended: true 685 | }); 686 | else { 687 | if (confirm('Alright, proceed with the non-supported version?')) 688 | return sendCommand({ 689 | event: 'getAppVersion', 690 | useVer: true 691 | }); 692 | } 693 | } 694 | break; 695 | case 'appList': { 696 | let id = 0; 697 | for (const app of message.list) { 698 | const appName = app.appName.replace(' (Wear OS)', '').replace(' (Android Automotive)', ''); 699 | const link = app.link.replace('-wear-os', '').replace('-android-automotive', ''); 700 | document.getElementById('appList').innerHTML += ` 701 |
    • 702 | 710 |
    • `; 711 | 712 | id++; 713 | } 714 | 715 | addSearch(false); 716 | break; 717 | } 718 | 719 | case 'apkUploaded': { 720 | document.querySelector( 721 | '.inf' 722 | ).innerHTML = `

      ${message.appName}

      ${message.package}
      v${message.versionName}

      `; 723 | document.querySelector('.shw').style.display = 'block'; 724 | document 725 | .getElementById('continue') 726 | .setAttribute('onClick', "location.href = '/patches'"); 727 | break; 728 | } 729 | 730 | case 'settings': { 731 | const cli = message.settings.cli.split('/'); 732 | const patches = message.settings.patches.split('/'); 733 | const integrations = message.settings.integrations.split('/'); 734 | const microg = message.settings.microg.split('/'); 735 | const cli4 = message.settings.cli4 == 'true'; 736 | 737 | const cliOrg = document.getElementById('cli-org'); 738 | const cliSrc = document.getElementById('cli-src'); 739 | 740 | cliOrg.value = cli[0]; 741 | cliSrc.value = cli[1]; 742 | 743 | const patchesOrg = document.getElementById('patch-org'); 744 | const patchesSrc = document.getElementById('patch-src'); 745 | 746 | patchesOrg.value = patches[0]; 747 | patchesSrc.value = patches[1]; 748 | 749 | const integrationsOrg = document.getElementById('integrations-org'); 750 | const integrationsSrc = document.getElementById('integrations-src'); 751 | 752 | integrationsOrg.value = integrations[0]; 753 | integrationsSrc.value = integrations[1]; 754 | 755 | const microgOrg = document.getElementById('microg-org'); 756 | const microgSrc = document.getElementById('microg-src'); 757 | 758 | microgOrg.value = microg[0]; 759 | microgSrc.value = microg[1]; 760 | 761 | const cli4Src = document.getElementById('cli4'); 762 | cli4Src.value = cli4; 763 | 764 | const preReleases = document.getElementById('pre-releases'); 765 | if (typeof prereleases === 'undefined') { 766 | preReleases.value = 'false'; 767 | } else { 768 | preReleases.value = prereleases; 769 | } 770 | } 771 | } 772 | }; 773 | -------------------------------------------------------------------------------- /public/installer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Installing 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
      16 |
      17 |
      18 |

      Installing

      19 |
      20 |
      21 |
      22 | 23 |
      24 |
      25 |
      26 |
      27 | 37 | 47 | 54 | 55 |
      56 |
      57 | 58 | 61 | 62 | -------------------------------------------------------------------------------- /public/patch/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Patching 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
      16 |
      17 |
      18 |

      Patching

      19 |
      20 |
      21 |
      22 | 23 |
      24 |
      25 |
      26 |
      27 | 37 | 47 | 54 | 55 |
      56 |
      57 | 58 | 61 | 62 | -------------------------------------------------------------------------------- /public/patches/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Select Patches 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
      16 |
      17 |
      18 |

      19 | Select patches to include 20 |

      21 | 22 |
      23 | 24 | 25 |
      26 |
      27 | 33 |
      34 |
      35 |
      36 |
      37 |
        38 |
        39 |
        40 |
        41 |
        42 | 52 | 62 | 69 | 70 | 71 | 74 |
        75 |
        76 | 77 | 80 | 81 | -------------------------------------------------------------------------------- /public/settings.js: -------------------------------------------------------------------------------- 1 | const accentColors = document.querySelectorAll('#themecolor-picker span'); 2 | const dElement = document.documentElement; 3 | if (localStorage.getItem('auto-next')) { 4 | document.getElementById('autoBtn').checked = true; 5 | } 6 | if (localStorage.getItem('black-theme')) { 7 | document.getElementById('blackBtn').checked = true; 8 | } 9 | if (localStorage.getItem('pre-releases')) { 10 | document.getElementById('preReleasesBtn').checked = true; 11 | } 12 | if (localStorage.getItem('rip-libs')) { 13 | document.getElementById('ripLibsBtn').checked = true; 14 | } 15 | if (localStorage.getItem('universal-patches')) { 16 | document.getElementById('universalPatchesBtn').checked = true; 17 | } 18 | 19 | accentColors.forEach((color) => { 20 | if ( 21 | dElement.style.getPropertyValue('--accentColor') == 22 | color.style.getPropertyValue('background-color') 23 | ) { 24 | color.classList.add('active'); 25 | } 26 | color.addEventListener('click', () => { 27 | accentColors.forEach((el) => el.classList.remove('active')); 28 | color.classList.add('active'); 29 | const colorNow1 = color.style.getPropertyValue('background-color'); 30 | dElement.style.setProperty('--accentColor', colorNow1); 31 | localStorage.setItem('theme', colorNow1); 32 | }); 33 | }); 34 | 35 | document.getElementById('autoBtn').addEventListener('click', function () { 36 | if (localStorage.getItem('auto-next')) { 37 | localStorage.removeItem('auto-next'); 38 | } else { 39 | localStorage.setItem('auto-next', true); 40 | } 41 | }); 42 | document.getElementById('blackBtn').addEventListener('click', function () { 43 | if (localStorage.getItem('black-theme')) { 44 | localStorage.removeItem('black-theme'); 45 | dElement.classList.remove('black'); 46 | } else { 47 | localStorage.setItem('black-theme', true); 48 | dElement.classList.add('black'); 49 | } 50 | }); 51 | document.getElementById('preReleasesBtn').addEventListener('click', function () { 52 | if (localStorage.getItem('pre-releases')) { 53 | localStorage.removeItem('pre-releases'); 54 | } else { 55 | localStorage.setItem('pre-releases', true); 56 | } 57 | document.getElementById('set-sources-update').click(); 58 | }); 59 | document.getElementById('ripLibsBtn').addEventListener('click', function () { 60 | if (localStorage.getItem('rip-libs')) { 61 | localStorage.removeItem('rip-libs'); 62 | } else { 63 | localStorage.setItem('rip-libs', true); 64 | } 65 | }); 66 | document.getElementById('universalPatchesBtn').addEventListener('click', function () { 67 | if (localStorage.getItem('universal-patches')) { 68 | localStorage.removeItem('universal-patches'); 69 | } else { 70 | localStorage.setItem('universal-patches', true); 71 | } 72 | }); 73 | -------------------------------------------------------------------------------- /public/settings/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Settings 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
        17 |
        18 |
        19 |

        Settings

        20 |
        21 |
        22 |
        23 |
        24 |

        Accent Color

        25 |

        26 | Customize the Accent color of the builder. Use the palette 27 | option to use a custom color. 28 |

        29 |
        30 | 33 | 36 | 39 | 42 | 45 | 🎨 52 |
        53 |
        54 | 55 |
        56 |

        57 | Pure Black Theme 58 |

        59 |

        60 | The pure black theme uses a black background and helps reduce 61 | battery usage on AMOLED devices or maybe you can just turn it on 62 | when the dark theme is not enough. 63 |

        64 | 71 |
        72 |
        73 |

        74 | Automatically 75 | proceed to next page 76 |

        77 | 78 |

        79 | This helps save time by proceeding automatically to the next 80 | page on downloading pages.

        Caution: It may break your back 81 | button. 82 |

        83 | 90 |
        91 |
        92 |

        93 | Use pre-release 94 |

        95 | 96 |

        97 | Use the pre-release versions of patches and integrations if ahead of the stable version. 98 |

        Caution: Use pre-release is for development purposes and may cause unexpected issues. 99 |

        100 | 107 |
        108 |
        109 |

        110 | Remove unused library 111 |

        112 | 113 |

        114 | Leave only libraries that match the device's architecture. 115 |

        Caution: Available only on Android (termux). 116 |

        117 | 124 |
        125 | 126 |
        127 |

        128 | Show universal patches 129 |

        130 | 131 |

        132 | Display all universal patches. 133 |

        134 | 141 |
        142 | 143 |
        144 |

        145 | Reset settings 146 |

        147 | 148 |

        149 | Resets the source and selected patch list. 150 |

        Try this if the patch is excluded even though you included it. 151 |

        152 | 155 |
        156 |
        157 |

        Sources

        158 | 159 |

        160 | Configure your custom sources.
        Eg. GitHub 161 | Username/Repository. 162 |

        163 | 164 |
        165 | 166 | 167 | 168 | 169 | 170 |
        171 | 172 |
        173 |

        Cli Organization

        174 | 175 |

        Cli Source

        176 | 177 |
        178 | 179 |
        180 |

        Use Cli 4

        181 | 182 |
        183 | 184 |
        185 |

        Patches Organization

        186 | 187 |

        Patches Source

        188 | 189 |
        190 | 191 | 192 | 193 | 194 |
        195 |

        MicroG Organization

        196 | 197 |

        MicroG Source

        198 | 199 |
        200 | 201 | 202 | 203 | 206 |
        207 |
        208 |
        209 |
        210 |
        211 | 221 | 230 | 231 | 232 |
        233 |
        234 | 235 | 240 | 241 | -------------------------------------------------------------------------------- /public/styles/about.css: -------------------------------------------------------------------------------- 1 | .mobile { 2 | display: none; 3 | } 4 | 5 | @media only screen and (max-width: 900px) { 6 | .desktop { 7 | display: none; 8 | } 9 | 10 | .mobile { 11 | display: block; 12 | } 13 | } 14 | 15 | a { 16 | color: #ddd; 17 | } 18 | -------------------------------------------------------------------------------- /public/styles/apps.css: -------------------------------------------------------------------------------- 1 | /* Make unordered lists actually look good */ 2 | 3 | ul { 4 | width: 100%; 5 | margin: 0; 6 | padding: 0; 7 | list-style: none; 8 | } 9 | 10 | /* Styles for the devices */ 11 | 12 | li { 13 | display: flex; 14 | margin-bottom: 15px; 15 | } 16 | 17 | /* Hides multiple choice inputs */ 18 | 19 | input[type='radio'] { 20 | display: none; 21 | } 22 | 23 | /* Makes selected list items look selected */ 24 | 25 | input[type='radio']:checked + label { 26 | background: var(--accentColor); 27 | box-shadow: inset 0px 0px 0px 1px #ddd; 28 | border-color: #ddd; 29 | } 30 | 31 | /* Labels */ 32 | 33 | input[type='radio'] + label { 34 | width: 100%; 35 | font-family: 'Roboto', sans-serif; 36 | font-size: 14px; 37 | color: var(--textColor); 38 | border-radius: var(--border-radius); 39 | padding: 14px; 40 | background-color: var(--interactableColor); 41 | font-weight: bold; 42 | border: 2px solid #dddddd2e; 43 | cursor: pointer; 44 | transition: 200ms; 45 | } 46 | 47 | input[type='radio'] + label:hover { 48 | box-shadow: inset 0px 0px 0px 1px #ddd; 49 | border-color: #ddd; 50 | } 51 | 52 | header h2, 53 | .shw { 54 | display: none; 55 | } 56 | 57 | .inf div { 58 | align-items: center; 59 | display: inline-flex; 60 | } 61 | 62 | .inf p { 63 | margin: auto; 64 | } 65 | 66 | .upl { 67 | display: inline-flex; 68 | align-items: center; 69 | } 70 | 71 | .upl i { 72 | margin-right: 8px; 73 | } 74 | -------------------------------------------------------------------------------- /public/styles/core.css: -------------------------------------------------------------------------------- 1 | /* Fonts */ 2 | 3 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); 4 | @import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@300&display=swap'); 5 | 6 | /* Color Varibles */ 7 | 8 | :root { 9 | --textColor: #ffffff; 10 | --accentColor: #4873b3; 11 | --primaryColor: #0f111a; 12 | --contrastColor: #1b1e29; 13 | --interactableColor: #2a2d3a; 14 | --buttonColor: #3d404f; 15 | --scrollbarColor: #3d404f; 16 | --icon-foreground: #686b76; 17 | --icon-background: #1b1e29; 18 | --border-radius: 10px; 19 | } 20 | 21 | html.black:root { 22 | --primaryColor: black; 23 | } 24 | 25 | html.black 26 | input[type='radio'] 27 | + label:not(input[type='radio']:checked + label) { 28 | background-color: black; 29 | } 30 | 31 | /* 32 | * Makes elements box size include the border, 33 | * this prevents elements that have borders 34 | * from overlapping with eachother 35 | */ 36 | 37 | * { 38 | box-sizing: border-box; 39 | /* !! Dont use if you have editable textboxes anywhere !! */ 40 | /* Disables selection */ 41 | user-select: none; 42 | } 43 | 44 | /* Fonts for all spans */ 45 | 46 | span { 47 | color: var(--textColor); 48 | font-family: 'Roboto', sans-serif; 49 | font-weight: 400; 50 | } 51 | 52 | p { 53 | color: var(--textColor); 54 | font-family: 'Roboto', sans-serif; 55 | } 56 | 57 | /* Fonts and styles for headers */ 58 | 59 | h1, 60 | h2, 61 | h3, 62 | h4, 63 | h5, 64 | h6 { 65 | color: var(--textColor); 66 | font-family: 'Roboto', sans-serif; 67 | font-weight: 700; 68 | margin: 0; 69 | } 70 | 71 | h1 i { 72 | vertical-align: revert; 73 | margin-right: 8px; 74 | } 75 | 76 | /* Make BODY and HTML elements inherit :root properies */ 77 | 78 | body, 79 | html { 80 | width: 100%; 81 | height: 100%; 82 | margin: 0; 83 | } 84 | 85 | body { 86 | background-color: var(--primaryColor); 87 | margin: auto; 88 | max-width: 1300px; 89 | } 90 | 91 | /* Styles for container */ 92 | 93 | div#container { 94 | display: flex; 95 | flex-direction: column; 96 | height: 100%; 97 | } 98 | 99 | div#container > main { 100 | display: flex; 101 | flex-direction: column; 102 | min-width: 0; 103 | min-height: 0; 104 | height: 100%; 105 | padding: 20px; 106 | background-color: var(--primaryColor); 107 | } 108 | 109 | /* Main content */ 110 | 111 | @media (min-width: 500px) { 112 | div#content--wrapper { 113 | padding: 13px !important; 114 | } 115 | } 116 | 117 | /* Wrapper for the content */ 118 | 119 | div#content--wrapper { 120 | border-radius: var(--border-radius); 121 | height: 100%; 122 | padding: 8px; 123 | margin-top: 5px; 124 | background-color: var(--primaryColor); 125 | box-shadow: 0 0 0 2px var(--interactableColor); 126 | overflow-x: auto; 127 | scroll-behavior: smooth; 128 | } 129 | 130 | /* Content Wrapper Scrollbar */ 131 | 132 | ::-webkit-scrollbar { 133 | width: 18px; 134 | } 135 | 136 | /* div#content--wrapper */ 137 | ::-webkit-scrollbar-thumb { 138 | border-radius: 10px; 139 | border: 6px solid transparent; 140 | background-clip: padding-box; 141 | background-color: var(--scrollbarColor); 142 | } 143 | 144 | /* Style for all buttons */ 145 | 146 | button { 147 | border: 0; 148 | border-radius: 5px; 149 | margin: 10px; 150 | color: var(--textColor); 151 | font-family: 'Roboto', sans-serif; 152 | font-weight: 400; 153 | padding: 10px 20px; 154 | background-color: var(--buttonColor); 155 | cursor: pointer; 156 | transition: 200ms; 157 | box-shadow: 0px 0px 0px 2px #ddd; 158 | } 159 | 160 | button:hover { 161 | box-shadow: 0 0 0 3px #ddd; 162 | } 163 | 164 | button:focus, 165 | button:active { 166 | box-shadow: 0 0 0 2px #ddd; 167 | } 168 | 169 | .highlighted { 170 | background-color: var(--accentColor); 171 | } 172 | 173 | /* Styles for alignment */ 174 | 175 | span.right { 176 | margin-left: auto; 177 | } 178 | 179 | span.left { 180 | margin-left: auto; 181 | } 182 | 183 | /* Footer for buttons */ 184 | 185 | footer { 186 | display: flex; 187 | align-items: center; 188 | padding: 12px; 189 | background-color: var(--primaryColor); 190 | overflow-y: hidden; 191 | overflow-x: auto; 192 | box-shadow: 0 -2px 0 0 var(--interactableColor); 193 | } 194 | 195 | footer a { 196 | margin-right: 10px; 197 | } 198 | 199 | /* Header for title and optional buttons ( addressed per module ) */ 200 | 201 | header { 202 | margin: 5px; 203 | } 204 | 205 | /* Darken interactable elements on hover to show that they are clickable */ 206 | 207 | button:hover, 208 | li:hover, 209 | i.social-icon:hover { 210 | filter: brightness(95%); 211 | } 212 | 213 | /* Darken interactable elements on mouse-down */ 214 | button:active, 215 | li:active, 216 | i.social-icon:active { 217 | filter: brightness(85%); 218 | /* Usage of !important is only to override :hover's filter */ 219 | } 220 | 221 | i.social-icon { 222 | width: 40px; 223 | height: 40px; 224 | border-radius: 25px; 225 | line-height: 1em; 226 | color: var(--icon-foreground); 227 | box-shadow: 0 0 0 2px var(--interactableColor); 228 | cursor: pointer; 229 | transition: 200ms; 230 | } 231 | 232 | i.social-icon:hover { 233 | box-shadow: 0 0 0 2px #ddd; 234 | } 235 | 236 | i.social-icon::before { 237 | width: 40px; 238 | height: 40px; 239 | position: relative; 240 | left: 8px; 241 | top: 8px; 242 | border-radius: 25px; 243 | color: var(--icon-foreground); 244 | } 245 | 246 | .disabled { 247 | filter: brightness(50%) !important; 248 | } 249 | 250 | .log * { 251 | user-select: text; 252 | } 253 | 254 | html.black .searchBox { 255 | background-color: black !important; 256 | } 257 | 258 | .searchBox { 259 | width: 100%; 260 | color: var(--textColor); 261 | border-radius: var(--border-radius); 262 | padding: 12px; 263 | background-color: var(--interactableColor); 264 | border: 2px solid #dddddd2e; 265 | font-size: 14px; 266 | margin-top: 5px; 267 | user-select: auto; 268 | } 269 | -------------------------------------------------------------------------------- /public/styles/dependencies.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Source Sans HW Regular'; 3 | src: url('/styles/fonts/Source Sans HW Regular.otf') format('opentype'); 4 | } 5 | 6 | @font-face { 7 | font-family: 'Source Sans HW Bold'; 8 | src: url('/styles/fonts/Source Sans HW Bold.otf') format('opentype'); 9 | } 10 | 11 | span.log .log-line { 12 | font-family: 'Fira Code', 'Source Sans HW Regular', monospace; 13 | font-weight: 300; 14 | font-size: 0.8em; 15 | line-height: 0.8em; 16 | width: 100%; 17 | height: 100%; 18 | } 19 | 20 | span.log .log-line strong { 21 | font-family: 'Fira Code', 'Source Sans HW Bold', monospace; 22 | font-weight: bold; 23 | } 24 | 25 | span.log .log-line.error { 26 | color: #d21404; 27 | } 28 | 29 | span.log .log-line.warn { 30 | color: #c4d204; 31 | } 32 | 33 | span.log:after { 34 | content: '|'; 35 | animation: blinking-cursor 1s steps(2, start) infinite; 36 | } 37 | 38 | @keyframes blinking-cursor { 39 | to { 40 | opacity: 0; 41 | } 42 | } 43 | 44 | progress { 45 | margin-top: 20px; 46 | width: 100%; 47 | border-radius: 25px; 48 | border: none; 49 | } 50 | 51 | progress::-moz-progress-bar { 52 | border-radius: 25px; 53 | background-color: var(--accentColor); 54 | } 55 | 56 | progress::-webkit-progress-bar { 57 | background-color: var(--contrastColor); 58 | border-radius: 7px; 59 | } 60 | progress::-webkit-progress-value { 61 | background-color: var(--accentColor); 62 | border-radius: 7px; 63 | } 64 | -------------------------------------------------------------------------------- /public/styles/failure.css: -------------------------------------------------------------------------------- 1 | span.log-error { 2 | color: red; 3 | font-family: 'Fira Code', monospace; 4 | font-weight: 300; 5 | font-size: 0.8em; 6 | line-height: 0.8em; 7 | width: 100%; 8 | height: 100%; 9 | user-select: text; 10 | } 11 | span.log:after { 12 | content: '|'; 13 | animation: blinking-cursor 1s steps(2, start) infinite; 14 | } 15 | 16 | @keyframes blinking-cursor { 17 | to { 18 | opacity: 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /public/styles/fontawesome.css: -------------------------------------------------------------------------------- 1 | .fa-brands, 2 | .fa-solid { 3 | -moz-osx-font-smoothing: grayscale; 4 | -webkit-font-smoothing: antialiased; 5 | display: inline-block; 6 | font-style: normal; 7 | font-variant: normal; 8 | line-height: 1; 9 | text-rendering: auto; 10 | } 11 | 12 | .fa-xl { 13 | font-size: 1.5em; 14 | line-height: 0.04167em; 15 | vertical-align: -0.125em; 16 | } 17 | 18 | .fa-github:before { 19 | content: ''; 20 | } 21 | 22 | .fa-question-circle:before { 23 | content: ''; 24 | } 25 | 26 | .fa-brush:before { 27 | content: '\f55d'; 28 | } 29 | 30 | .fa-arrow-right:before { 31 | content: '\f061'; 32 | } 33 | 34 | .fa-circle-half-stroke:before { 35 | content: '\f042'; 36 | } 37 | 38 | .fa-upload:before { 39 | content: '\f093'; 40 | } 41 | 42 | .fa-circle-chevron-right:before { 43 | content: '\f138'; 44 | } 45 | 46 | .fa-circle-xmark:before { 47 | content: '\f057'; 48 | } 49 | 50 | .fa-code:before { 51 | content: '\f121'; 52 | } 53 | 54 | .fa-screwdriver-wrench:before { 55 | content: '\f7d9'; 56 | } 57 | 58 | .fa-backward:before { 59 | content: '\f04a'; 60 | } 61 | 62 | .fa-download:before { 63 | content: '\f019'; 64 | } 65 | 66 | .fa-square-check:before { 67 | content: '\f14a'; 68 | } 69 | 70 | .fa-rectangle-list:before { 71 | content: '\f022'; 72 | } 73 | 74 | .fa-file-arrow-down:before { 75 | content: '\f56d'; 76 | } 77 | 78 | .fa-terminal:before { 79 | content: '\f120'; 80 | } 81 | 82 | .fa-spinner:before { 83 | content: '\f110'; 84 | } 85 | 86 | .fa-spin { 87 | animation-name: spin; 88 | animation-duration: 1500ms; 89 | animation-iteration-count: infinite; 90 | animation-timing-function: linear; 91 | } 92 | @keyframes spin { 93 | from { 94 | transform: rotate(0deg); 95 | } 96 | to { 97 | transform: rotate(360deg); 98 | } 99 | } 100 | 101 | .fa-right-from-bracket:before { 102 | content: '\f2f5'; 103 | } 104 | 105 | .fa-circle-exclamation:before { 106 | content: '\f06a'; 107 | } 108 | 109 | .fa-tablet-screen-button:before { 110 | content: '\f3fa'; 111 | } 112 | 113 | .fa-gear:before { 114 | content: '\f013'; 115 | } 116 | 117 | .fa-server:before { 118 | content: '\f233'; 119 | } 120 | 121 | .fa-brands { 122 | font-family: 'Font Awesome 6 Brands'; 123 | font-size: 400; 124 | } 125 | 126 | @font-face { 127 | font-family: 'Font Awesome 6 Brands'; 128 | font-style: normal; 129 | font-weight: 400; 130 | font-display: block; 131 | src: url(../webfonts/fa-brands-400.woff2) format('woff2'), 132 | url(../webfonts/fa-brands-400.ttf) format('truetype'); 133 | } 134 | 135 | @font-face { 136 | font-family: 'Font Awesome 6 Pro'; 137 | font-style: normal; 138 | font-weight: 900; 139 | font-display: block; 140 | src: url(../webfonts/fa-solid-900.woff2) format('woff2'), 141 | url(../webfonts/fa-solid-900.ttf) format('truetype'); 142 | } 143 | 144 | .fa-solid { 145 | font-family: 'Font Awesome 6 Pro'; 146 | font-size: 900; 147 | } 148 | -------------------------------------------------------------------------------- /public/styles/home.css: -------------------------------------------------------------------------------- 1 | body { 2 | align-items: center; 3 | display: flex; 4 | flex-wrap: wrap; 5 | justify-content: center; 6 | } 7 | 8 | main { 9 | padding: 20px; 10 | text-align: center; 11 | align-items: center; 12 | display: flex; 13 | flex-wrap: wrap; 14 | flex-direction: column; 15 | } 16 | 17 | #go { 18 | font-size: 15px; 19 | display: inline-flex; 20 | align-items: center; 21 | } 22 | 23 | #go i { 24 | margin-left: 8px; 25 | } 26 | 27 | .social-icon { 28 | padding-right: 16px; 29 | } 30 | 31 | .actions { 32 | display: flex; 33 | padding: 12px; 34 | } 35 | 36 | .actions a { 37 | margin-right: 10px; 38 | } 39 | 40 | .theming.show { 41 | border-bottom: 2px solid #ddd; 42 | border-radius: 10px; 43 | padding: 30px; 44 | width: 280px; 45 | height: 82px; 46 | top: 50%; 47 | left: 50%; 48 | margin-top: -41px; 49 | margin-left: -140px; 50 | display: flex !important; 51 | justify-content: center; 52 | align-items: center; 53 | } 54 | svg { 55 | min-height: 160px; 56 | max-height: 250px; 57 | margin-bottom: 17px; 58 | } 59 | 60 | h1 span { 61 | font-weight: bold; 62 | } 63 | 64 | h1 .accent-fun { 65 | color: var(--accentColor); 66 | filter: brightness(160%); 67 | } 68 | 69 | .status { 70 | margin-top: 10px; 71 | color: #ddd; 72 | font-family: fira code, source sans hw regular, monospace; 73 | padding: 8px; 74 | border-radius: 21px; 75 | box-shadow: 0 0 0 2px var(--interactableColor); 76 | } 77 | 78 | .updater { 79 | margin-top: 10px; 80 | display: none; 81 | } 82 | 83 | .updater .latest { 84 | color: #56d956; 85 | } 86 | 87 | .updater .outdated { 88 | display: none; 89 | } 90 | 91 | .updater .outdated span { 92 | color: #ff3737; 93 | } 94 | -------------------------------------------------------------------------------- /public/styles/patches.css: -------------------------------------------------------------------------------- 1 | html.black 2 | input[type='checkbox'].select 3 | + label:not(input[type='checkbox'].select:checked + label) { 4 | background-color: black !important; 5 | } 6 | 7 | input[type='checkbox'] { 8 | display: none; 9 | } 10 | 11 | input[type='checkbox'].select:checked + label { 12 | background: var(--accentColor); 13 | box-shadow: inset 0px 0px 0px 1px #ddd; 14 | border-color: #ddd; 15 | } 16 | 17 | .patch-description { 18 | display: inline-block; 19 | width: 100%; 20 | } 21 | 22 | input[type='checkbox'].select + label { 23 | width: 100%; 24 | font-family: 'Roboto', sans-serif; 25 | font-weight: 400; 26 | font-size: 14px; 27 | color: var(--textColor); 28 | border-radius: var(--border-radius); 29 | padding: 12px; 30 | background-color: var(--interactableColor); 31 | cursor: pointer; 32 | border: 2px solid #dddddd2e; 33 | transition: 200ms; 34 | } 35 | 36 | input[type='checkbox'].select + label:hover { 37 | box-shadow: inset 0px 0px 0px 1px #ddd; 38 | border-color: #ddd; 39 | } 40 | 41 | ul { 42 | width: 100%; 43 | margin: 0; 44 | padding: 0; 45 | list-style: none; 46 | } 47 | 48 | li { 49 | display: flex; 50 | margin-bottom: 15px; 51 | } 52 | 53 | .no-root { 54 | float: right; 55 | } 56 | 57 | @media only screen and (max-width: 700px) { 58 | li .no-root { 59 | display: block; 60 | float: none; 61 | border-bottom: 2px solid; 62 | padding-bottom: 4.5px; 63 | margin-bottom: 4.5px; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /public/styles/settings.css: -------------------------------------------------------------------------------- 1 | h3 { 2 | font-size: 1.22em; 3 | } 4 | h3 i { 5 | margin-right: 8px; 6 | vertical-align: revert; 7 | } 8 | p { 9 | margin-top: 10px; 10 | } 11 | .option { 12 | padding: 20px 8px 20px 8px; 13 | border-bottom: 2px solid var(--interactableColor); 14 | } 15 | 16 | .option button { 17 | display: block; 18 | margin: 0px; 19 | } 20 | 21 | #themecolor-picker { 22 | display: flex; 23 | flex-wrap: wrap; 24 | padding: 0; 25 | margin-left: -4px; 26 | } 27 | 28 | #set-sources { 29 | display: flex; 30 | flex-wrap: wrap; 31 | padding: 12px; 32 | margin-right: 15px; 33 | margin-bottom: 15px; 34 | } 35 | 36 | #set-sources-rvx { 37 | display: flex; 38 | flex-wrap: wrap; 39 | padding: 12px; 40 | margin-right: 15px; 41 | margin-bottom: 15px; 42 | } 43 | 44 | #set-sources-rvx-anddea { 45 | display: flex; 46 | flex-wrap: wrap; 47 | padding: 12px; 48 | margin-right: 15px; 49 | margin-bottom: 15px; 50 | } 51 | 52 | #set-sources-revanced { 53 | display: flex; 54 | flex-wrap: wrap; 55 | padding: 12px; 56 | margin-right: 15px; 57 | margin-bottom: 15px; 58 | } 59 | 60 | #set-sources-piko { 61 | display: flex; 62 | flex-wrap: wrap; 63 | padding: 12px; 64 | margin-right: 15px; 65 | margin-bottom: 15px; 66 | } 67 | 68 | #set-sources-update { 69 | display: none; 70 | } 71 | 72 | #integrations-org { 73 | display: none; 74 | } 75 | 76 | #integrations-src { 77 | display: none; 78 | } 79 | 80 | #pre-releases { 81 | display: none; 82 | } 83 | 84 | #themecolor-picker span { 85 | padding: 16px; 86 | transition: 200ms; 87 | display: inline-block; 88 | border-radius: 100%; 89 | margin: 4px; 90 | cursor: pointer; 91 | height: 22px; 92 | width: 22px; 93 | } 94 | 95 | #themecolor-picker span.active { 96 | box-shadow: inset 0 0 0 2px #ddd; 97 | } 98 | 99 | /* toggle swtich from codepen.io/alvarotrigo/pen/GROPMZe */ 100 | .switch .check { 101 | display: none; 102 | } 103 | 104 | .switch { 105 | display: inline-block; 106 | width: 55px; 107 | height: 28px; 108 | position: relative; 109 | -webkit-tap-highlight-color: transparent; 110 | } 111 | 112 | .slider { 113 | position: absolute; 114 | top: 0; 115 | bottom: 0; 116 | left: 0; 117 | right: 0; 118 | border-radius: 30px; 119 | box-shadow: 0 0 0 2px #ddd, 0 0 4px #ddd; 120 | cursor: pointer; 121 | border: 4px solid transparent; 122 | overflow: hidden; 123 | transition: 0.2s; 124 | background-color: var(--buttonColor); 125 | } 126 | 127 | .slider:before { 128 | position: absolute; 129 | content: ''; 130 | width: 100%; 131 | height: 100%; 132 | background-color: #fff; 133 | border-radius: 30px; 134 | transform: translateX(-27px); 135 | transition: 0.2s; 136 | } 137 | 138 | .check:checked + .slider:before { 139 | transform: translateX(27px); 140 | } 141 | 142 | .check:checked + .slider { 143 | background-color: var(--accentColor); 144 | } 145 | 146 | .option#src div { 147 | padding: 0 8px 0 8px; 148 | } 149 | 150 | .option#src div input { 151 | margin-bottom: 15px; 152 | } 153 | -------------------------------------------------------------------------------- /public/versions/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Version Selector 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
        17 |
        18 |
        19 |

        20 | Select the version you 21 | want to download 22 |

        23 |
        24 |
        25 |
        26 |
          27 |
          28 |
          29 |
          30 |
          31 | 41 | 51 | 58 | 59 | 60 | 63 |
          64 |
          65 | 66 | 69 | 70 | -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inotia00/rvx-builder/ab5a108aebb18c9eac2d36437b5ddbcbe89bc90d/public/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inotia00/rvx-builder/ab5a108aebb18c9eac2d36437b5ddbcbe89bc90d/public/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inotia00/rvx-builder/ab5a108aebb18c9eac2d36437b5ddbcbe89bc90d/public/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inotia00/rvx-builder/ab5a108aebb18c9eac2d36437b5ddbcbe89bc90d/public/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import {}; 3 | in 4 | pkgs.mkShellNoCC { 5 | name = "rvx-builder"; 6 | packages = [ 7 | pkgs.android-tools 8 | pkgs.nodejs 9 | pkgs.jdk17_headless 10 | ]; 11 | shellHook = '' 12 | npm install 13 | ''; 14 | } 15 | -------------------------------------------------------------------------------- /utils/AntiSplit.js: -------------------------------------------------------------------------------- 1 | const { existsSync, rmSync } = require('node:fs'); 2 | 3 | const exec = require('./promisifiedExec.js'); 4 | 5 | /** @type {import('ws').WebSocket} */ 6 | let ws; 7 | 8 | /** 9 | * @param {string} inputPath 10 | * @param {string} outputPath 11 | * @param {import('ws').WebSocket} [websocket] 12 | */ 13 | async function antiSplit(inputPath, outputPath, websocket) { 14 | if (websocket != null) ws = websocket; 15 | 16 | if (existsSync(outputPath)) { 17 | rmSync(outputPath, { recursive: true, force: true }); 18 | } 19 | 20 | ws.send( 21 | JSON.stringify({ 22 | event: 'mergingFile' 23 | }) 24 | ); 25 | 26 | await exec( 27 | `${global.javaCmd} -jar "${global.jarNames.apkEditor}" m -i "${inputPath}" -o "${outputPath}"`, 28 | { maxBuffer: 1024 * 1024 * 10 } 29 | ); 30 | } 31 | 32 | module.exports = { 33 | antiSplit 34 | }; -------------------------------------------------------------------------------- /utils/FileDownloader.js: -------------------------------------------------------------------------------- 1 | const { readdirSync, createWriteStream, unlink } = require('node:fs'); 2 | const { join: joinPath } = require('node:path'); 3 | 4 | const { load } = require('cheerio'); 5 | const { getSources } = require('../utils/Settings.js'); 6 | const Progress = require('node-fetch-progress'); 7 | const fetchWithUserAgent = require('../utils/fetchWithUserAgent.js'); 8 | 9 | /** @type {import('ws').WebSocket} */ 10 | let ws; 11 | 12 | /** 13 | * @param {string} fileName 14 | */ 15 | async function overWriteJarNames(fileName, cli4) { 16 | const filePath = joinPath(global.revancedDir, fileName); 17 | 18 | const source = getSources(); 19 | const cli = source.cli.split('/')[1]; 20 | const microg = source.microg.split('/')[1]; 21 | 22 | if (fileName.includes(cli) && fileName.endsWith('.jar')) { 23 | global.jarNames.cli = filePath; 24 | } 25 | 26 | if (fileName.includes(microg)) { 27 | global.jarNames.microG = filePath; 28 | } 29 | 30 | if (fileName.includes('APKEditor') && fileName.endsWith('.jar')) { 31 | global.jarNames.apkEditor = filePath; 32 | } 33 | 34 | if (cli4) { 35 | const patches = source.patches.split('/')[1]; 36 | const integrations = source.integrations.split('/')[1]; 37 | if (fileName.includes(patches)) { 38 | if (fileName.endsWith('.jar')) { 39 | global.jarNames.patchesJar = filePath; 40 | } else if (fileName.endsWith('.json')) { 41 | global.jarNames.patchesList = filePath; 42 | } 43 | } else if (fileName.includes(integrations) && fileName.endsWith('.apk')) { 44 | global.jarNames.integrations = filePath; 45 | } 46 | } else { 47 | if (fileName.endsWith('.rvp')) { 48 | global.jarNames.patchesJar = filePath; 49 | global.jarNames.patchesList = joinPath(global.revancedDir, fileName.replace('.rvp', '.json')); 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * @param {Record} json 56 | */ 57 | async function getDownloadLink(json, preReleases, cli4) { 58 | const preReleaseUrl = `https://github.com/${json.owner}/${json.repo}/releases` 59 | const preReleaseTag = 'span[class="ml-1 wb-break-all"]' 60 | const stableReleaseUrl = `https://github.com/${json.owner}/${json.repo}/releases/latest` 61 | const stableReleaseTag = 'span[class="ml-1"]' 62 | 63 | let releaseTag = stableReleaseTag; 64 | let releaseUrl = stableReleaseUrl; 65 | 66 | if (preReleases) releaseTag = preReleaseTag; 67 | if (preReleases) releaseUrl = preReleaseUrl; 68 | 69 | const json_ = { 70 | version: '', 71 | assets: '', 72 | repo: json.repo 73 | }; 74 | 75 | /** @type {{ browser_download_url: string }[]} */ 76 | const assets = []; 77 | const releasesPage = await fetchWithUserAgent(releaseUrl); 78 | 79 | if (!releasesPage.ok) 80 | throw new Error( 81 | 'You got ratelimited from GitHub\n...Completely? What did you even do?' 82 | ); 83 | 84 | const releasePage = await releasesPage.text(); 85 | const $ = load(releasePage); 86 | 87 | json_.version = $(releaseTag).first().text().replace(/\s/g, ''); 88 | 89 | if (json.owner === 'inotia00' && json.repo === 'revanced-cli' && cli4) { 90 | json_.version = 'v4.6.2'; 91 | } 92 | const expandedAssets = await fetchWithUserAgent( 93 | `https://github.com/${json.owner}/${json.repo}/releases/expanded_assets/${json_.version}` 94 | ); 95 | 96 | const assetsPageText = await expandedAssets.text(); 97 | const assetsPage = load(assetsPageText); 98 | 99 | for (const downloadLink of assetsPage('a[rel="nofollow"]').get()) 100 | if ( 101 | !downloadLink.attribs.href.endsWith('.tar.gz') && 102 | !downloadLink.attribs.href.endsWith('.zip') 103 | ) 104 | assets.push({ 105 | browser_download_url: `https://github.com${downloadLink.attribs.href}` 106 | }); 107 | 108 | json_.assets = assets; 109 | 110 | return json_; 111 | } 112 | 113 | /** 114 | * @param {Record} assets 115 | */ 116 | async function downloadFile(assets, cli4) { 117 | for (const asset of assets.assets) { 118 | const dir = readdirSync(global.revancedDir); 119 | 120 | const fileExt = asset.browser_download_url 121 | .split('/') 122 | .at(-1) 123 | .split('.') 124 | .at(-1); 125 | if (fileExt == 'asc') continue; 126 | const fileName = `${assets.repo}-${assets.version}.${fileExt}`; 127 | 128 | overWriteJarNames(fileName, cli4); 129 | 130 | if (dir.includes(fileName)) continue; 131 | 132 | await dloadFromURL( 133 | asset.browser_download_url, 134 | joinPath(global.revancedDir, fileName) 135 | ); 136 | } 137 | } 138 | 139 | /** 140 | * @param {string} url 141 | * @param {string} outputPath 142 | * @param {import('ws').WebSocket} [websocket] 143 | */ 144 | async function dloadFromURL(url, outputPath, websocket) { 145 | if (websocket != null) ws = websocket; 146 | 147 | try { 148 | const res = await fetchWithUserAgent(url); 149 | const writeStream = createWriteStream(outputPath); 150 | const downloadStream = res.body.pipe(writeStream); 151 | 152 | ws.send( 153 | JSON.stringify({ 154 | event: 'downloadingFile', 155 | name: outputPath.split('/').at(-1), 156 | percentage: 0 157 | }) 158 | ); 159 | 160 | const progress = new Progress(res, { throttle: 50 }); 161 | 162 | return new Promise((resolve, reject) => { 163 | progress.on('progress', (p) => { 164 | ws.send( 165 | JSON.stringify({ 166 | event: 'downloadingFile', 167 | name: outputPath.split('/').at(-1), 168 | percentage: Math.floor(p.progress * 100) 169 | }) 170 | ); 171 | }); 172 | 173 | downloadStream.once('finish', resolve); 174 | downloadStream.once('error', (err) => { 175 | unlink(outputPath, () => { 176 | reject(new Error('Download failed.', err)); 177 | }); 178 | }); 179 | }); 180 | } catch (err) { 181 | global.downloadFinished = false; 182 | 183 | throw err; 184 | } 185 | } 186 | 187 | /** 188 | * @param {Record[]} repos 189 | * @param {import('ws').WebSocket} websocket 190 | */ 191 | async function downloadFiles(repos, preReleases, cli4, websocket) { 192 | ws = websocket; 193 | 194 | for (const repo of repos) { 195 | const downloadLink = await getDownloadLink(repo, preReleases, cli4); 196 | 197 | await downloadFile(downloadLink, cli4); 198 | } 199 | } 200 | 201 | module.exports = { downloadFiles, dloadFromURL, getDownloadLink }; 202 | -------------------------------------------------------------------------------- /utils/PatchesParser.js: -------------------------------------------------------------------------------- 1 | const { readFileSync } = require('node:fs'); 2 | 3 | /** 4 | * @param {string} packageName 5 | * @param {boolean} hasRoot 6 | */ 7 | module.exports = async function parsePatch(packageName, hasRoot, showUniversalPatches) { 8 | const patchesList = JSON.parse( 9 | readFileSync(global.jarNames.patchesList, 'utf8') 10 | ); 11 | 12 | const rootedPatches = [ 13 | 'MicroG support', 14 | 'GmsCore support' 15 | ]; 16 | const patches = []; 17 | const generalPatches = []; 18 | const universalPatches = []; 19 | 20 | global.versions = []; 21 | 22 | for (const patch of patchesList) { 23 | const isRooted = rootedPatches.includes(patch.name); 24 | 25 | // Check if the patch is compatible: 26 | let isCompatible = false; 27 | /** @type {string} */ 28 | let compatibleVersion; 29 | 30 | if (patch.compatiblePackages === null) continue; 31 | 32 | for (const pkg of patch.compatiblePackages) 33 | if (pkg.name === packageName) { 34 | isCompatible = true; 35 | 36 | if (pkg.versions !== null) { 37 | compatibleVersion = pkg.versions.at(-1); 38 | 39 | global.versions.push(compatibleVersion); 40 | } 41 | } 42 | 43 | if (!isCompatible) { 44 | if (patch.compatiblePackages.length !== 0) continue; 45 | } 46 | 47 | if (isRooted && !hasRoot) continue; 48 | 49 | generalPatches.push({ 50 | name: patch.name, 51 | description: patch.description || '(Not Provided)', 52 | maxVersion: compatibleVersion || 'ALL', 53 | universal: false, 54 | isRooted, 55 | excluded: patch.excluded || (patch.use !== undefined && !patch.use), 56 | }); 57 | } 58 | 59 | if (global.versions.length === 0) { 60 | global.versions = 'NOREC'; 61 | } 62 | 63 | if (!showUniversalPatches) return generalPatches; 64 | 65 | for (const patch of patchesList) { 66 | const isRooted = rootedPatches.includes(patch.name); 67 | 68 | // Check if the patch is compatible: 69 | let isCompatible = false; 70 | 71 | /** @type {string} */ 72 | let compatibleVersion; 73 | 74 | if (patch.compatiblePackages !== null) continue; 75 | patch.compatiblePackages = []; 76 | 77 | for (const pkg of patch.compatiblePackages) 78 | if (pkg.name === packageName) { 79 | isCompatible = true; 80 | 81 | if (pkg.versions !== null) { 82 | compatibleVersion = pkg.versions.at(-1); 83 | 84 | global.versions.push(compatibleVersion); 85 | } 86 | } 87 | 88 | if (!isCompatible) { 89 | if (patch.compatiblePackages.length !== 0) continue; 90 | } 91 | 92 | if (isRooted && !hasRoot) continue; 93 | 94 | universalPatches.push({ 95 | name: patch.name, 96 | description: patch.description || '(Not Provided)', 97 | maxVersion: 'UNIVERSAL', 98 | universal: true, 99 | isRooted, 100 | excluded: patch.excluded || (patch.use !== undefined && !patch.use), 101 | }); 102 | } 103 | 104 | for (const patch of generalPatches) { 105 | patches.push(patch); 106 | } 107 | 108 | for (const patch of universalPatches) { 109 | patches.push(patch); 110 | } 111 | 112 | return patches; 113 | }; 114 | -------------------------------------------------------------------------------- /utils/Settings.js: -------------------------------------------------------------------------------- 1 | const { existsSync, readFileSync, rmSync, writeFileSync } = require('node:fs'); 2 | 3 | const defaultSettings = { 4 | sources: { 5 | cli: 'inotia00/revanced-cli', 6 | patches: 'inotia00/revanced-patches', 7 | integrations: 'inotia00/revanced-integrations', 8 | microg: 'ReVanced/GmsCore', 9 | prereleases: 'false', 10 | cli4: 'false' 11 | }, 12 | patches: [] 13 | }; 14 | const defaultSettingsJSON = JSON.stringify(defaultSettings, null, 2); 15 | 16 | function createSettingsFile() { 17 | writeFileSync('settings.json', defaultSettingsJSON); 18 | } 19 | 20 | /** 21 | * @param {string} pkgName 22 | * @returns {Record} 23 | */ 24 | function getPatchesList(pkgName) { 25 | const patchesList = JSON.parse(readFileSync('settings.json', 'utf8')); 26 | 27 | const package = patchesList.patches.find( 28 | (package) => package.name === pkgName 29 | ); 30 | 31 | if (!package) { 32 | return []; 33 | } else { 34 | return package.patches; 35 | } 36 | } 37 | 38 | /** 39 | * @param {string} packageName 40 | * @param {Record} patches 41 | */ 42 | function writePatches({ packageName }, patches) { 43 | if (!existsSync('settings.json')) { 44 | createSettingsFile(); 45 | } 46 | 47 | const patchesList = JSON.parse(readFileSync('settings.json', 'utf8')); 48 | 49 | const index = patchesList.patches.findIndex( 50 | (package) => package.name === packageName 51 | ); 52 | 53 | if (index === -1) { 54 | patchesList.patches.push({ 55 | name: packageName, 56 | patches 57 | }); 58 | } else patchesList.patches[index].patches = patches; 59 | 60 | writeFileSync('settings.json', JSON.stringify(patchesList, null, 2)); 61 | } 62 | 63 | /** 64 | * @param {string} pkgName 65 | */ 66 | function getPatchList(pkgName) { 67 | if (!existsSync('settings.json')) { 68 | createSettingsFile(); 69 | 70 | return []; 71 | } else return getPatchesList(pkgName); 72 | } 73 | 74 | function getSettings() { 75 | const settings = JSON.parse(readFileSync('settings.json', 'utf8')); 76 | 77 | return settings; 78 | } 79 | 80 | function resetPatchesSources(ws) { 81 | rmSync('settings.json', { recursive: true, force: true }); 82 | createSettingsFile(); 83 | } 84 | 85 | function writeSources(sources) { 86 | const settings = JSON.parse(readFileSync('settings.json', 'utf8')); 87 | 88 | settings.sources = sources; 89 | 90 | writeFileSync('settings.json', JSON.stringify(settings, null, 2)); 91 | } 92 | 93 | function getSources() { 94 | if (!existsSync('settings.json')) { 95 | createSettingsFile(); 96 | 97 | return defaultSettings.sources; 98 | } else return getSettings().sources; 99 | } 100 | 101 | module.exports = { 102 | getPatchList, 103 | writePatches, 104 | getSources, 105 | resetPatchesSources, 106 | writeSources 107 | }; 108 | -------------------------------------------------------------------------------- /utils/checkJDKAndADB.js: -------------------------------------------------------------------------------- 1 | const { join: joinPath } = require('node:path'); 2 | const { unlinkSync, existsSync } = require('node:fs'); 3 | const { extract } = require('tar'); 4 | const exec = require('./promisifiedExec.js'); 5 | 6 | const getDeviceID = require('../utils/getDeviceID.js'); 7 | const { dloadFromURL } = require('../utils/FileDownloader'); 8 | 9 | function getConsts(name) { 10 | switch (name) { 11 | case 'jreDir': 12 | return joinPath( 13 | global.revancedDir, 14 | `zulu17.56.15-ca-jre17.0.14-${getConsts('platform')}_${getConsts( 15 | 'arch' 16 | )}` 17 | ); 18 | case 'jreBin': 19 | return joinPath( 20 | getConsts('jreDir'), 21 | `bin/java${process.platform === 'win32' ? '.exe' : ''}` 22 | ); 23 | case 'platform': { 24 | let platform = ''; 25 | switch (process.platform) { 26 | case 'darwin': 27 | platform = 'macosx'; 28 | break; 29 | case 'linux': 30 | platform = 'linux'; 31 | break; 32 | case 'win32': 33 | platform = 'win'; 34 | break; 35 | } 36 | return platform; 37 | } 38 | case 'arch': { 39 | let arch = ''; 40 | switch (process.arch) { 41 | case 'arm': 42 | arch = 'aarch32hf'; 43 | break; 44 | case 'arm64': 45 | arch = 'aarch64'; 46 | break; 47 | case 'ia32': 48 | arch = 'i686'; 49 | break; 50 | case 'x64': 51 | arch = 'x64'; 52 | break; 53 | } 54 | return arch; 55 | } 56 | } 57 | } 58 | 59 | async function downloadJDK(ws) { 60 | try { 61 | const output = joinPath(global.revancedDir, 'JRE.tar.gz'); 62 | 63 | await dloadFromURL( 64 | `https://cdn.azul.com/zulu/bin/zulu17.56.15-ca-jre17.0.14-${getConsts( 65 | 'platform' 66 | )}_${getConsts('arch')}.tar.gz`, 67 | output, 68 | ws 69 | ); 70 | 71 | extract({ cwd: global.revancedDir, file: output }, (err) => { 72 | if (err) throw err; 73 | unlinkSync(output); 74 | global.javaCmd = getConsts('jreBin'); 75 | }); 76 | } catch (err) { 77 | console.error(err); 78 | ws.send( 79 | JSON.stringify({ 80 | event: 'error', 81 | error: 82 | 'An error occured while trying to install Zulu JDK.
          Please install a supported version manually from here: https://www.azul.com/downloads/?version=java-17-lts&package=jdk' 83 | }) 84 | ); 85 | } 86 | } 87 | 88 | /** 89 | * @param {import('ws').WebSocket} ws 90 | */ 91 | module.exports = async function checkJDKAndADB(ws) { 92 | try { 93 | const javaCheck = await exec('java -version'); 94 | const javaVerLog = javaCheck.stderr || javaCheck.stdout; 95 | const javaVer = javaVerLog.match(/version\s([^:]+)/)[1].match(/"(.*)"/)[1]; 96 | 97 | if (javaVer.split('.')[0] < 17 || !javaVerLog.includes('openjdk')) { 98 | if (existsSync(getConsts('jreBin'))) { 99 | global.javaCmd = getConsts('jreBin'); 100 | } else { 101 | await downloadJDK(ws); 102 | } 103 | } else { 104 | global.javaCmd = 'java'; // If the System java is supported, use it. 105 | } 106 | 107 | const deviceIds = await getDeviceID(); 108 | 109 | if (deviceIds !== null) { 110 | if (deviceIds[1]) { 111 | ws.send( 112 | JSON.stringify({ 113 | event: 'multipleDevices' 114 | }) 115 | ); 116 | 117 | return; 118 | } else if (deviceIds[0]) global.jarNames.devices = deviceIds; 119 | } else global.jarNames.devices = []; 120 | } catch (err) { 121 | if (err.stderr.includes('java')) { 122 | if (existsSync(getConsts('jreBin'))) { 123 | global.javaCmd = getConsts('jreBin'); 124 | } else { 125 | await downloadJDK(ws); 126 | } 127 | } 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /utils/checkJDKAndAapt2.js: -------------------------------------------------------------------------------- 1 | const { existsSync } = require('node:fs'); 2 | const { join: joinPath } = require('node:path'); 3 | 4 | const exec = require('./promisifiedExec.js'); 5 | 6 | const { dloadFromURL } = require('./FileDownloader.js'); 7 | 8 | /** 9 | * @param {import('ws').WebSocket} ws 10 | */ 11 | module.exports = async function checkJDKAndAapt2(ws) { 12 | global.jarNames.devices = []; 13 | try { 14 | await exec('java -v'); 15 | } catch (e) { 16 | if (e.stderr.includes('not found')) 17 | ws.send( 18 | JSON.stringify({ 19 | event: 'error', 20 | error: 21 | "You don't have JDK installed. Please close ReVanced Builder and install it using: `pkg install openjdk-17`" 22 | }) 23 | ); 24 | } 25 | 26 | if (!existsSync(joinPath(global.revancedDir, 'aapt2'))) { 27 | await dloadFromURL( 28 | 'https://github.com/inotia00/rvx-builder/raw/aapt2/aapt2.zip', 29 | 'revanced/aapt2.zip', 30 | ws 31 | ); 32 | await exec( 33 | `unzip ${joinPath(global.revancedDir, 'aapt2.zip')} -d ${ 34 | global.revancedDir 35 | }` 36 | ); 37 | 38 | switch (process.arch) { 39 | case 'arm64': 40 | await exec( 41 | `cp ${joinPath(global.revancedDir, 'arm64-v8a/aapt2')} ${joinPath( 42 | global.revancedDir, 43 | 'aapt2' 44 | )}` 45 | ); 46 | await exec(`chmod +x ${joinPath(global.revancedDir, 'aapt2')}`); 47 | break; 48 | case 'arm': 49 | await exec( 50 | `cp ${joinPath(global.revancedDir, 'armeabi-v7a/aapt2')} ${joinPath( 51 | global.revancedDir, 52 | 'aapt2' 53 | )}` 54 | ); 55 | await exec(`chmod +x ${joinPath(global.revancedDir, 'aapt2')}`); 56 | } 57 | 58 | await exec( 59 | `rm -rf ${joinPath(global.revancedDir, 'arm64-v8a')} ${joinPath( 60 | global.revancedDir, 61 | 'armeabi-v7a' 62 | )} ${joinPath(global.revancedDir, 'x86')} ${joinPath( 63 | global.revancedDir, 64 | 'aapt2.zip' 65 | )}` 66 | ); 67 | } 68 | }; 69 | -------------------------------------------------------------------------------- /utils/downloadApp.js: -------------------------------------------------------------------------------- 1 | const { join: joinPath } = require('node:path'); 2 | 3 | const { load } = require('cheerio'); 4 | const { dloadFromURL } = require('./FileDownloader.js'); 5 | const fetchWithUserAgent = require('../utils/fetchWithUserAgent.js'); 6 | const { antiSplit } = require('./AntiSplit.js'); 7 | 8 | 9 | /** 10 | * @param {import('ws').WebSocket} ws 11 | */ 12 | async function downloadApp(ws) { 13 | const { version, arch } = global.apkInfo; 14 | const apkMirrorVersionArg = version.replace(/\./g, '-'); 15 | const link = global.jarNames.selectedApp.link; 16 | const url = `https://www.apkmirror.com${link}${link.split('/')[3]}-${apkMirrorVersionArg}-release/` 17 | 18 | let versionDownload = await fetchWithUserAgent(url); 19 | 20 | if ( 21 | !versionDownload.ok && 22 | global.jarNames.selectedApp.packageName === 'com.twitter.android' 23 | ) { 24 | versionDownload = await fetchWithUserAgent(url.replace('/twitter/twitter', '/twitter/x-previously-twitter')); 25 | } 26 | 27 | if (!versionDownload.ok) { 28 | ws.send( 29 | JSON.stringify({ 30 | event: 'error', 31 | error: `Failed to scrape download link for ${version}.
          Please try downgrading.` 32 | }) 33 | ); 34 | 35 | return; 36 | } 37 | 38 | const versionDownloadList = await versionDownload.text(); 39 | 40 | const $ = load(versionDownloadList); 41 | 42 | const apkDownloadLink = 43 | arch && 44 | global.jarNames.selectedApp.packageName === 45 | 'com.google.android.apps.youtube.music' 46 | ? $(`div:contains("${arch}")`) 47 | .parent() 48 | .children('div[class^="table-cell rowheight"]') 49 | .first() 50 | .children('a[class="accent_color"]') 51 | .first() 52 | .attr('href') 53 | : $('span[class="apkm-badge"]') 54 | .first() 55 | .parent() 56 | .children('a[class="accent_color"]') 57 | .first() 58 | .attr('href'); 59 | 60 | const apkmDownloadLink = 61 | $('span[class="apkm-badge success"]') 62 | .first() 63 | .parent() 64 | .children('a[class="accent_color"]') 65 | .first() 66 | .attr('href'); 67 | 68 | if (!apkDownloadLink && !apkmDownloadLink) { 69 | return ws.send( 70 | JSON.stringify({ 71 | event: 'error', 72 | error: `The version ${version} does not have an APK available, please use an older version.` 73 | }) 74 | ); 75 | } 76 | 77 | const downloadLink = !apkDownloadLink 78 | ? `https://www.apkmirror.com${apkmDownloadLink}` 79 | : `https://www.apkmirror.com${apkDownloadLink}`; 80 | 81 | const downloadLinkPage = await fetchWithUserAgent(downloadLink) 82 | .then((res) => res.text()); 83 | 84 | const $2 = load(downloadLinkPage); 85 | const pageLink = $2('a[class^="accent_bg btn btn-flat downloadButton"]') 86 | .first() 87 | .attr('href'); 88 | 89 | const downloadPage = await fetchWithUserAgent(`https://www.apkmirror.com${pageLink}`).then( 90 | (res) => res.text() 91 | ); 92 | const $3 = load(downloadPage); 93 | const apkLink = $3('a[rel="nofollow"]').first().attr('href'); 94 | 95 | if (!apkDownloadLink) { 96 | await dloadFromURL( 97 | `https://www.apkmirror.com${apkLink}`, 98 | `${joinPath( 99 | global.revancedDir, 100 | global.jarNames.selectedApp.packageName 101 | )}.apkm`, 102 | ws 103 | ); 104 | await antiSplit( 105 | `${joinPath( 106 | global.revancedDir, 107 | global.jarNames.selectedApp.packageName 108 | )}.apkm`, 109 | `${joinPath( 110 | global.revancedDir, 111 | global.jarNames.selectedApp.packageName 112 | )}.apk`, 113 | ws 114 | ); 115 | } else { 116 | await dloadFromURL( 117 | `https://www.apkmirror.com${apkLink}`, 118 | `${joinPath( 119 | global.revancedDir, 120 | global.jarNames.selectedApp.packageName 121 | )}.apk`, 122 | ws 123 | ); 124 | } 125 | 126 | ws.send( 127 | JSON.stringify({ 128 | event: 'finished' 129 | }) 130 | ); 131 | } 132 | 133 | module.exports = { 134 | downloadApp 135 | }; 136 | -------------------------------------------------------------------------------- /utils/fetchWithUserAgent.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | const USER_AGENT_HEADERS = { 4 | headers: { 5 | 'User-Agent': 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.231 Mobile Safari/537.36' 6 | } 7 | } 8 | 9 | module.exports = async function fetchWithUserAgent(url) { 10 | return fetch(url, USER_AGENT_HEADERS); 11 | }; 12 | -------------------------------------------------------------------------------- /utils/getAppVersion.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require('node:os'); 2 | 3 | const exec = require('./promisifiedExec.js'); 4 | function sendError(ws) { 5 | ws.send( 6 | JSON.stringify({ 7 | event: 'error', 8 | error: 9 | "The app you selected is not installed on your device. It's needed for rooted ReVanced." 10 | }) 11 | ); 12 | } 13 | async function getAppVersion_(pkgName, ws, shouldReturnMsg, deviceId) { 14 | try { 15 | const { stdout, stderr } = await exec( 16 | process.platform !== 'android' 17 | ? `adb -s ${deviceId} shell dumpsys package ${pkgName}` 18 | : `su -c dumpsys package ${pkgName}`, 19 | { maxBuffer: 1024 * 1024 * 10 } 20 | ); 21 | const dumpSysOut = stdout || stderr; 22 | 23 | const versionMatch = dumpSysOut.match(/versionName=([^=]+)/); 24 | 25 | if (versionMatch === null) { 26 | if (shouldReturnMsg) { 27 | sendError(ws); 28 | } 29 | 30 | return null; 31 | } 32 | 33 | return versionMatch[1].replace(`${EOL} `, '').match(/\d+(\.\d+)+/g)[0]; 34 | } catch (e) { 35 | if (shouldReturnMsg) { 36 | sendError(ws); 37 | } 38 | 39 | return null; 40 | } 41 | } 42 | 43 | /** 44 | * @param {string} pkgName 45 | * @param {import('ws').WebSocket} ws 46 | * @param {boolean} shouldReturnMsg 47 | */ 48 | async function getAppVersion(pkgName, ws, shouldReturnMsg) { 49 | const versions = []; 50 | 51 | if (process.platform === 'android') 52 | return await getAppVersion_(pkgName, ws, shouldReturnMsg); 53 | 54 | for (const deviceId of global.jarNames.devices) { 55 | const version = await getAppVersion_( 56 | pkgName, 57 | ws, 58 | shouldReturnMsg, 59 | deviceId 60 | ); 61 | versions.push(version); 62 | } 63 | 64 | if (versions.every((version) => version === versions[0])) { 65 | return versions[0]; 66 | } else { 67 | return ws.send( 68 | JSON.stringify({ 69 | event: 'error', 70 | error: `The devices you're trying to install ReVanced doesn't have a matching version. Please install version ${versions[0]} to every device.` 71 | }) 72 | ); 73 | } 74 | } 75 | 76 | module.exports = { 77 | getAppVersion_, 78 | getAppVersion 79 | }; 80 | -------------------------------------------------------------------------------- /utils/getDeviceArch.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require('node:os'); 2 | const exec = require('./promisifiedExec.js'); 3 | 4 | async function getDeviceArch_(ws, deviceId) { 5 | try { 6 | const deviceArch = await exec( 7 | process.platform !== 'android' 8 | ? `adb -s ${deviceId} shell getprop ro.product.cpu.abi` 9 | : 'getprop ro.product.cpu.abi' 10 | ); 11 | 12 | return deviceArch.stdout.replace(EOL, ''); 13 | } catch (e) { 14 | ws.send( 15 | JSON.stringify({ 16 | event: 'error', 17 | error: 18 | 'Failed to get device architecture, please see console for the error.' 19 | }) 20 | ); 21 | 22 | throw e; 23 | } 24 | } 25 | 26 | module.exports = async function getDeviceArch(ws) { 27 | if (process.platform === 'android') return await getDeviceArch_(ws); 28 | 29 | const architectures = []; 30 | for (const deviceId of global.jarNames.devices) { 31 | const arch = await getDeviceArch_(ws, deviceId); 32 | 33 | architectures.push(arch); 34 | } 35 | 36 | if (architectures.every((arch) => arch === architectures[0])) { 37 | return architectures[0]; 38 | } else { 39 | return ws.send( 40 | JSON.stringify({ 41 | event: 'error', 42 | error: `The devices you're trying to install ReVanced doesn't have the same architecture. Please patch some devices seperately.` 43 | }) 44 | ); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /utils/getDeviceID.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require('node:os'); 2 | 3 | const exec = require('./promisifiedExec.js'); 4 | 5 | const adbDeviceIdRegex = new RegExp(`${EOL}(.*?)\t`, 'g'); 6 | 7 | module.exports = async function getDeviceID() { 8 | try { 9 | const { stdout } = await exec('adb devices'); 10 | const matches = stdout.match(adbDeviceIdRegex); 11 | 12 | if (matches === null) return null; 13 | 14 | return matches.map((match) => match.replace(EOL, '').replace('\t', '')); 15 | } catch (e) { 16 | return null; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /utils/mountReVanced.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync } = require('node:fs'); 2 | const { join: joinPath } = require('node:path'); 3 | 4 | const exec = require('./promisifiedExec.js'); 5 | 6 | /** 7 | * @param {string} pkg 8 | * @param {import('ws').WebSocket} ws 9 | */ 10 | module.exports = async function mountReVanced(pkg, ws) { 11 | // Create folder 12 | await exec('su -c \'mkdir -p "/data/adb/revanced/"\''); 13 | 14 | // Copy ReVanced APK to the folder **directly** 15 | await exec( 16 | `su -c 'cp "${joinPath( 17 | global.revancedDir, 18 | global.outputName 19 | )}" "/data/adb/revanced/"'` 20 | ); 21 | 22 | // Unmount the already existing ReVanced APK, so it can be updated 23 | try { 24 | // Force stop the app 25 | await exec(`su -c 'am force-stop ${pkg}'`); 26 | // Unmount 27 | await exec( 28 | `su -mm -c 'stock_path="$(pm path ${pkg} | grep base | sed 's/package://g')" && umount -l "$stock_path"'` 29 | ); 30 | // eslint-disable-next-line no-empty 31 | } catch {} // An error occured, probably because there is no existing ReVanced APK to be unmounted, ignore it and continue 32 | 33 | // Move APK to folder 34 | await exec( 35 | `su -c 'base_path="/data/adb/revanced/${pkg}.apk" && mv -f "/data/adb/revanced/${global.outputName}" "$base_path" && chmod 644 "$base_path" && chown system:system "$base_path" && chcon u:object_r:apk_data_file:s0 "$base_path"'` 36 | ); 37 | 38 | // Create mount script 39 | writeFileSync( 40 | 'mount.sh', 41 | `#!/system/bin/sh 42 | 43 | # Wait for the system to boot. 44 | until [ "$( getprop sys.boot_completed )" = 1 ]; do sleep 3; done 45 | until [ -d "/sdcard/Android" ]; do sleep 1; done 46 | 47 | # Unmount any existing mount as a safety measure. 48 | grep ${pkg} /proc/mounts | while read -r line; do echo $line | cut -d ' ' -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done 49 | 50 | base_path="/data/adb/revanced/${pkg}.apk" 51 | stock_path=$(pm path ${pkg} | grep base | sed 's/package://g') 52 | am force-stop ${pkg} 53 | chcon u:object_r:apk_data_file:s0 $base_path 54 | [ ! -z "$stock_path" ] && mount -o bind $base_path $stock_path 55 | am force-stop ${pkg}` 56 | ); 57 | 58 | // Move Mount script to folder 59 | await exec( 60 | `su -c 'cp "./mount.sh" "/data/adb/service.d/mount_revanced_${pkg}.sh"'` 61 | ); 62 | // Give execution perms to Mount script 63 | await exec(`su -c 'chmod +x "/data/adb/service.d/mount_revanced_${pkg}.sh"'`); 64 | 65 | // Run Mount script 66 | await exec(`su -mm -c '"/data/adb/service.d/mount_revanced_${pkg}.sh"'`); 67 | 68 | // Kill app process 69 | await exec(`su -c 'am force-stop ${pkg}'`); 70 | 71 | ws.send( 72 | JSON.stringify({ 73 | event: 'patchLog', 74 | log: 'ReVanced should be now mounted! Please open the app and check if it has been mounted.' 75 | }) 76 | ); 77 | }; 78 | -------------------------------------------------------------------------------- /utils/mountReVancedInstaller.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync } = require('node:fs'); 2 | const { join: joinPath } = require('node:path'); 3 | 4 | const exec = require('./promisifiedExec.js'); 5 | const { spawn } = require('node:child_process'); 6 | 7 | async function promisifiedSpawn(command, args) { 8 | return new Promise((resolve, reject) => { 9 | const proc = spawn(command, args); 10 | proc.on('close', (code) => { 11 | resolve(code); 12 | }); 13 | proc.on('error', (err) => { 14 | reject(err); 15 | }); 16 | }); 17 | } 18 | 19 | async function runCommand(command, deviceId) { 20 | return await promisifiedSpawn('adb', ['-s', deviceId, 'shell', command]); 21 | } 22 | 23 | /** 24 | * @param {string} pkg 25 | * @param {import('ws').WebSocket} ws 26 | * @param {string} deviceId 27 | */ 28 | module.exports = async function mountReVancedInstaller(deviceId) { 29 | let pkg = global.jarNames.selectedApp.packageName; 30 | 31 | // Copy ReVanced APK to temp. 32 | await exec( 33 | `adb -s ${deviceId} push "${joinPath( 34 | global.revancedDir, 35 | global.outputName 36 | )}" /data/local/tmp/revanced.delete`.trim() 37 | ); 38 | // Create folder 39 | await runCommand('su -c \'mkdir -p "/data/adb/revanced/"\'', deviceId); 40 | 41 | // Prepare mounting 42 | await runCommand( 43 | `su -c 'base_path="/data/adb/revanced/${pkg}.apk" && mv /data/local/tmp/revanced.delete $base_path && chmod 644 $base_path && chown system:system $base_path && chcon u:object_r:apk_data_file:s0 $base_path'`, 44 | deviceId 45 | ); 46 | 47 | // Create mount script 48 | writeFileSync( 49 | 'mount.sh', 50 | `#!/system/bin/sh 51 | 52 | # Wait for the system to boot. 53 | until [ "$( getprop sys.boot_completed )" = 1 ]; do sleep 3; done 54 | until [ -d "/sdcard/Android" ]; do sleep 1; done 55 | 56 | # Unmount any existing mount as a safety measure. 57 | grep ${pkg} /proc/mounts | while read -r line; do echo $line | cut -d ' ' -f 2 | sed 's/apk.*/apk/' | xargs -r umount -l; done 58 | 59 | base_path="/data/adb/revanced/${pkg}.apk" 60 | stock_path=$(pm path ${pkg} | grep base | sed 's/package://g') 61 | am force-stop ${pkg} 62 | chcon u:object_r:apk_data_file:s0 $base_path 63 | [ ! -z "$stock_path" ] && mount -o bind $base_path $stock_path 64 | am force-stop ${pkg}` 65 | ); 66 | 67 | // Push mount script to device 68 | await exec( 69 | `adb -s ${deviceId} push mount.sh /data/local/tmp/revanced.delete` 70 | ); 71 | 72 | // Move Mount script to folder 73 | await runCommand( 74 | `su -c 'cp "/data/local/tmp/revanced.delete" "/data/adb/service.d/mount_revanced_${pkg}.sh"'`, 75 | deviceId 76 | ); 77 | 78 | // Give execution perms to Mount script 79 | await runCommand( 80 | `su -c 'chmod +x "/data/adb/service.d/mount_revanced_${pkg}.sh"'`, 81 | deviceId 82 | ); 83 | 84 | // Unmount apk for ...sanity? 85 | await runCommand( 86 | `su -c "stock_path=$( pm path ${pkg} | grep base | sed 's/package://g' ) && umount -l $stock_path"`, 87 | deviceId 88 | ); 89 | 90 | // Run mount script 91 | await runCommand( 92 | `su -mm -c '"/data/adb/service.d/mount_revanced_${pkg}.sh"'`, 93 | deviceId 94 | ); 95 | 96 | // Restart app 97 | await runCommand( 98 | `su -c 'monkey -p ${pkg} 1 && kill $(pidof -s ${pkg})'`, 99 | deviceId 100 | ); 101 | }; 102 | -------------------------------------------------------------------------------- /utils/promisifiedExec.js: -------------------------------------------------------------------------------- 1 | const { promisify } = require('node:util'); 2 | const { exec: exec_ } = require('node:child_process'); 3 | 4 | const exec = promisify(exec_); 5 | 6 | module.exports = exec; 7 | -------------------------------------------------------------------------------- /utils/uploadAPKFile.js: -------------------------------------------------------------------------------- 1 | const AppInfoParser = require('app-info-parser'); 2 | const { renameSync } = require('node:fs'); 3 | function uploadAPKFile(req, res, ws) { 4 | const file = req.files.apk; 5 | 6 | file.mv('./revanced/temp.apk', async (err) => { 7 | if (err) { 8 | for (const websocket of ws) { 9 | websocket.send( 10 | JSON.stringify({ 11 | event: 'error', 12 | error: err 13 | }) 14 | ); 15 | } 16 | } 17 | 18 | const app = new AppInfoParser('./revanced/temp.apk'); 19 | const resp = await app.parse(); 20 | const { package, versionName, icon } = resp; 21 | const appName = resp.application.label[0]; 22 | 23 | await renameSync('./revanced/temp.apk', `./revanced/${package}.apk`); 24 | 25 | global.jarNames.selectedApp = { 26 | packageName: package, 27 | uploaded: true 28 | }; 29 | 30 | for (const websocket of ws) { 31 | websocket.send( 32 | JSON.stringify({ 33 | event: 'apkUploaded', 34 | package, 35 | versionName, 36 | appName, 37 | icon 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | return res.sendStatus(204); 44 | } 45 | 46 | module.exports = uploadAPKFile; 47 | -------------------------------------------------------------------------------- /wsEvents/checkFileAlreadyExists.js: -------------------------------------------------------------------------------- 1 | const { join } = require('node:path'); 2 | const { existsSync } = require('node:fs'); 3 | 4 | /** 5 | * @param {import('ws').WebSocket} ws 6 | */ 7 | module.exports = function checkFileAlreadyExists(ws) { 8 | ws.send( 9 | JSON.stringify({ 10 | event: existsSync( 11 | join( 12 | global.revancedDir, 13 | `${global.jarNames.selectedApp.packageName}.apk` 14 | ) 15 | ) 16 | ? 'fileExists' 17 | : 'fileDoesntExist', 18 | isRooted: global.jarNames.isRooted 19 | }) 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /wsEvents/checkForUpdates.js: -------------------------------------------------------------------------------- 1 | const { getDownloadLink } = require('../utils/FileDownloader.js'); 2 | 3 | const currentVersion = 'v3.18.5'; 4 | 5 | /** 6 | * @param {import('ws').WebSocket} ws 7 | */ 8 | module.exports = async function checkForUpdates(ws) { 9 | const builderVersion = ( 10 | await getDownloadLink({ owner: 'inotia00', repo: 'rvx-builder' }) 11 | ).version; 12 | 13 | if (builderVersion !== currentVersion) 14 | ws.send( 15 | JSON.stringify({ 16 | event: 'notUpToDate', 17 | builderVersion, 18 | currentVersion 19 | }) 20 | ); 21 | else { 22 | ws.send( 23 | JSON.stringify({ 24 | event: 'upToDate', 25 | currentVersion 26 | }) 27 | ); 28 | } 29 | }; 30 | 31 | module.exports.currentVersion = currentVersion; 32 | -------------------------------------------------------------------------------- /wsEvents/getApp.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const { existsSync, readFileSync } = require('node:fs'); 3 | const exec = require('../utils/promisifiedExec.js'); 4 | 5 | async function fetchPackages(packages) { 6 | const request = await fetch( 7 | `https://www.apkmirror.com/wp-json/apkm/v1/app_exists/`, 8 | { 9 | method: 'POST', 10 | body: JSON.stringify({ pnames: packages }), 11 | headers: { 12 | Accept: 'application/json', 13 | 'Content-Type': 'application/json', 14 | 'User-Agent': 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.231 Mobile Safari/537.36', 15 | Authorization: 16 | 'Basic YXBpLXRvb2xib3gtZm9yLWdvb2dsZS1wbGF5OkNiVVcgQVVMZyBNRVJXIHU4M3IgS0s0SCBEbmJL' 17 | } 18 | } 19 | ); 20 | 21 | try { 22 | const response = await request.json(); 23 | return response.data; 24 | } catch (e) { 25 | const response = []; 26 | return response; 27 | } 28 | } 29 | /** 30 | * @param {import('ws').WebSocket} ws 31 | */ 32 | module.exports = async function getPatches(ws) { 33 | const patchesJson = `${global.jarNames.patchesList}`; 34 | 35 | const java = `${global.javaCmd}`; 36 | const cli = `${global.jarNames.cli}`; 37 | const patches = `${global.jarNames.patchesJar}`; 38 | const command = `${java} -jar "${cli}" patches --path="${patchesJson}" "${patches}"`; 39 | 40 | try { 41 | const patchesList = JSON.parse( 42 | readFileSync(global.jarNames.patchesList, 'utf8') 43 | ); 44 | for (const patches of patchesList) { 45 | if (patches.compatiblePackages === null) continue; 46 | for (const packages of patches.compatiblePackages) { 47 | } 48 | } 49 | } catch (e) { 50 | await exec(command); 51 | } 52 | 53 | const patchesList = JSON.parse( 54 | readFileSync(global.jarNames.patchesList, 'utf8') 55 | ); 56 | const appsList = []; 57 | const list = []; 58 | 59 | for (const patches of patchesList) { 60 | if (patches.compatiblePackages === null) continue; 61 | for (const packages of patches.compatiblePackages) { 62 | if (!appsList.some((el) => el.pname === packages.name)) 63 | appsList.push({ pname: packages.name }); 64 | } 65 | } 66 | 67 | const apps = await fetchPackages(appsList); 68 | for (const app of apps) { 69 | if (!app.exists) continue; 70 | 71 | list.push({ 72 | appName: app.app.name, 73 | appPackage: app.pname, 74 | link: app.app.link 75 | }); 76 | } 77 | 78 | ws.send( 79 | JSON.stringify({ 80 | event: 'appList', 81 | list 82 | }) 83 | ); 84 | 85 | return; 86 | }; 87 | -------------------------------------------------------------------------------- /wsEvents/getAppVersion.js: -------------------------------------------------------------------------------- 1 | const exec = require('../utils/promisifiedExec.js'); 2 | 3 | const fetch = require('node-fetch'); 4 | const { load } = require('cheerio'); 5 | const semver = require('semver'); 6 | const { join: joinPath } = require('path'); 7 | 8 | const { getAppVersion: getAppVersion_ } = require('../utils/getAppVersion.js'); 9 | const { downloadApp: downloadApp_ } = require('../utils/downloadApp.js'); 10 | const fetchWithUserAgent = require('../utils/fetchWithUserAgent.js'); 11 | const getDeviceArch = require('../utils/getDeviceArch.js'); 12 | 13 | const APKMIRROR_UPLOAD_BASE = (page) => 14 | `https://www.apkmirror.com/uploads/page/${page}/?appcategory=`; 15 | 16 | /** 17 | * @param {string} ver 18 | */ 19 | const sanitizeVersion = (ver) => { 20 | while (ver.match(/\./g).length > 2) ver = ver.replace(/.([^.]*)$/, '$1'); 21 | 22 | return ver 23 | .replace(/\.0+(\d+)/g, '.$1') // replace leading zeroes with a single period 24 | .replace(/^(\d+)\.(\d+)$/g, '$1.$2.0'); // add trailing ".0" if needed 25 | 26 | }; 27 | 28 | /** 29 | * @param {string} url 30 | * @returns 31 | */ 32 | async function getPage(url) { 33 | return fetchWithUserAgent(url).then((res) => res.text()); 34 | } 35 | 36 | async function installRecommendedStock(ws, dId) { 37 | try { 38 | const latestVer = global.versions[global.versions.length - 1]; 39 | global.apkInfo.version = latestVer; 40 | ws.send( 41 | JSON.stringify({ 42 | event: 'installingStockApp', 43 | status: 'DOWNLOAD_STARTED' 44 | }) 45 | ); 46 | await downloadApp_(ws); 47 | const downloadedApkPath = `${joinPath( 48 | global.revancedDir, 49 | global.jarNames.selectedApp.packageName 50 | )}.apk`; 51 | ws.send( 52 | JSON.stringify({ 53 | event: 'installingStockApp', 54 | status: 'DOWNLOAD_COMPLETE' 55 | }) 56 | ); 57 | if (dId === 'CURRENT_DEVICE') { 58 | await exec( 59 | `su -c pm uninstall ${global.jarNames.selectedApp.packageName}` 60 | ); 61 | ws.send( 62 | JSON.stringify({ 63 | event: 'installingStockApp', 64 | status: 'UNINSTALL_COMPLETE' 65 | }) 66 | ); 67 | await exec(`su -c pm install ${downloadedApkPath}`); 68 | } else { 69 | ws.send( 70 | JSON.stringify({ 71 | event: 'installingStockApp', 72 | status: 'UNINSTALL_COMPLETE' 73 | }) 74 | ); 75 | await exec( 76 | `adb -s ${dId} uninstall ${global.jarNames.selectedApp.packageName}` 77 | ); 78 | await exec(`adb -s ${dId} install ${downloadedApkPath}`); 79 | } 80 | ws.send( 81 | JSON.stringify({ 82 | event: 'installingStockApp', 83 | status: 'ALL_DONE' 84 | }) 85 | ); 86 | } catch (_) { 87 | return ws.send( 88 | JSON.stringify({ 89 | event: 'error', 90 | error: `An error occured while trying to install the stock app${dId !== 'CURRENT_DEVICE' ? ` for device ID ${dId}` : '' 91 | }.\nPlease install the recommended version manually and run Builder again.` 92 | }) 93 | ); 94 | } 95 | } 96 | 97 | async function downloadApp(ws, message) { 98 | if (message.useVer) return await downloadApp_(ws); 99 | else if (message.checkVer) { 100 | if (global.versions.includes(global.apkInfo.version)) 101 | return await downloadApp_(ws); 102 | 103 | return ws.send( 104 | JSON.stringify({ 105 | event: 'askRootVersion' 106 | }) 107 | ); 108 | } else if (message.installLatestRecommended) { 109 | const useAdb = 110 | process.platform !== 'android' && global.jarNames?.devices.length !== 0; 111 | if (useAdb) { 112 | for (const id of global.jarNames.devices) installRecommendedStock(ws, id); 113 | } else installRecommendedStock(ws, 'CURRENT_DEVICE'); 114 | } else 115 | return ws.send( 116 | JSON.stringify({ 117 | event: 'error', 118 | error: 119 | 'You did not choose to use the non recommended version. Please downgrade.' 120 | }) 121 | ); 122 | } 123 | 124 | /** 125 | * @param {import('ws').WebSocket} ws 126 | */ 127 | module.exports = async function getAppVersion(ws, message) { 128 | let versionsList = await getPage( 129 | `${APKMIRROR_UPLOAD_BASE(message.page || 1)}${global.jarNames.selectedApp.link.split('/')[3] 130 | }` 131 | ); 132 | 133 | if (global.jarNames.isRooted) { 134 | if (process.platform !== 'android') { 135 | if (!(global.jarNames.devices && global.jarNames.devices[0])) { 136 | ws.send( 137 | JSON.stringify({ 138 | event: 'error', 139 | error: 140 | "You either don't have a device plugged in or don't have ADB installed." 141 | }) 142 | ); 143 | 144 | return; 145 | } 146 | 147 | try { 148 | for (const deviceId of global.jarNames.devices) { 149 | await exec(`adb -s ${deviceId} shell su -c exit`); 150 | } 151 | } catch { 152 | ws.send( 153 | JSON.stringify({ 154 | event: 'error', 155 | error: 156 | 'The plugged in device is not rooted or Shell was denied root access. If you didn\'t intend on doing a rooted build, include all "Needed for non-root building" patches' 157 | }) 158 | ); 159 | 160 | return; 161 | } 162 | } 163 | 164 | const appVersion = await getAppVersion_( 165 | global.jarNames.selectedApp.packageName, 166 | ws, 167 | true 168 | ); 169 | 170 | if ( 171 | global.jarNames.selectedApp.packageName === 172 | 'com.google.android.apps.youtube.music' 173 | ) { 174 | const arch = await getDeviceArch(ws); 175 | 176 | global.apkInfo = { 177 | version: appVersion, 178 | arch 179 | }; 180 | 181 | return downloadApp(ws, message); 182 | } else { 183 | global.apkInfo = { 184 | version: appVersion, 185 | arch: null 186 | }; 187 | 188 | return downloadApp(ws, message); 189 | } 190 | } 191 | 192 | /** @type {{ version: string; recommended: boolean; beta: boolean }[]} */ 193 | const versionList = []; 194 | const $ = load(versionsList); 195 | const link = global.jarNames.selectedApp.link; 196 | const regex = new RegExp( 197 | `(?<=${link}${link.split('/')[3]}-)(.*)(?=-release/)` 198 | ); 199 | const regexTwitter = new RegExp( 200 | `(?<=/apk/x-corp/twitter/x-previously-twitter-)(.*)(?=-release/)` 201 | ); 202 | for (const version of $( 203 | '#primary h5.appRowTitle.wrapText.marginZero.block-on-mobile' 204 | ).get()) { 205 | const versionTitle = version.attribs.title.toLowerCase(); 206 | const versionNameRaw = version.children[0].next.attribs.href; 207 | const versionNameMatch = versionNameRaw.match(regex); 208 | let versionName; 209 | if (global.jarNames.selectedApp.packageName === 'com.twitter.android' && 210 | versionNameMatch === null 211 | ) { 212 | versionName = versionNameRaw 213 | .match(regexTwitter)[0] 214 | .replace(/-/g, '.'); 215 | } else { 216 | versionName = versionNameRaw 217 | .match(regex)[0] 218 | .replace(/-/g, '.'); 219 | } 220 | 221 | if ( 222 | (global.jarNames.selectedApp.packageName === 'com.twitter.android' && 223 | !versionTitle.includes('release')) || 224 | versionTitle.includes('(Wear OS)') || 225 | versionTitle.includes('-car_release') 226 | ) 227 | continue; 228 | 229 | versionList.push({ 230 | version: versionName, 231 | recommended: 232 | global.versions !== 'NOREC' 233 | ? global.versions.includes(versionName) 234 | : 'NOREC', 235 | beta: versionTitle.includes('beta') 236 | }); 237 | } 238 | if (versionList.every((el) => /^[0-9.]*$/g.test(el.version))) { 239 | versionList.sort((a, b) => 240 | semver.lt(sanitizeVersion(a.version), sanitizeVersion(b.version)) ? 1 : -1 241 | ); 242 | } 243 | 244 | ws.send( 245 | JSON.stringify({ 246 | event: 'appVersions', 247 | versionList, 248 | page: message.page || 1, 249 | selectedApp: global.jarNames.selectedApp.packageName, 250 | foundDevice: 251 | global.jarNames.devices && global.jarNames.devices[0] 252 | ? true 253 | : process.platform === 'android', 254 | isRooted: global.jarNames.isRooted, 255 | supported: global.versions[global.versions.length - 1] 256 | }) 257 | ); 258 | }; 259 | -------------------------------------------------------------------------------- /wsEvents/getDevices.js: -------------------------------------------------------------------------------- 1 | const { EOL } = require('node:os'); 2 | 3 | const exec = require('../utils/promisifiedExec.js'); 4 | 5 | const getDeviceID = require('../utils/getDeviceID.js'); 6 | 7 | /** 8 | * @param {import('ws').WebSocket} ws 9 | */ 10 | module.exports = async function getDevices(ws) { 11 | const deviceIds = await getDeviceID(); 12 | /** @type {{ id: string; model: string }[]} */ 13 | const devices = []; 14 | 15 | for (const device of deviceIds) { 16 | const deviceModel = await exec( 17 | `adb -s ${device} shell getprop ro.product.model` 18 | ).catch(() => null); 19 | 20 | devices.push({ 21 | id: device, 22 | model: 23 | deviceModel !== null 24 | ? deviceModel.stdout.replace(EOL, '') 25 | : 'Could not get device model (Unauthorized?)' 26 | }); 27 | } 28 | 29 | ws.send( 30 | JSON.stringify({ 31 | event: 'devices', 32 | devices 33 | }) 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /wsEvents/getPatches.js: -------------------------------------------------------------------------------- 1 | const exec = require('../utils/promisifiedExec.js'); 2 | 3 | const { getPatchList } = require('../utils/Settings.js'); 4 | const parsePatch = require('../utils/PatchesParser.js'); 5 | 6 | /** 7 | * @param {import('ws').WebSocket} ws 8 | */ 9 | module.exports = async function getPatches(ws, message) { 10 | let hasRoot = true; 11 | 12 | if (process.platform === 'android') 13 | await exec('su -c exit').catch((err) => { 14 | const error = err.stderr || err.stdout; 15 | 16 | if ( 17 | error.includes('No su program found on this device.') || 18 | error.includes('Permission denied') 19 | ) 20 | hasRoot = false; 21 | }); 22 | 23 | const rememberedPatchList = getPatchList( 24 | global.jarNames.selectedApp.packageName 25 | ); 26 | 27 | ws.send( 28 | JSON.stringify({ 29 | event: 'patchList', 30 | patchList: await parsePatch( 31 | global.jarNames.selectedApp.packageName, 32 | hasRoot, 33 | message.showUniversalPatches 34 | ), 35 | rememberedPatchList, 36 | uploadedApk: global.jarNames.selectedApp.uploaded 37 | }) 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /wsEvents/getSettings.js: -------------------------------------------------------------------------------- 1 | const { getSources } = require('../utils/Settings.js'); 2 | /** 3 | * @param {import('ws').WebSocket} ws 4 | */ 5 | 6 | module.exports = function getSettings(ws) { 7 | const settings = getSources(); 8 | 9 | ws.send( 10 | JSON.stringify({ 11 | event: 'settings', 12 | settings 13 | }) 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /wsEvents/index.js: -------------------------------------------------------------------------------- 1 | const checkFileAlreadyExists = require('./checkFileAlreadyExists.js'); 2 | const checkForUpdates = require('./checkForUpdates.js'); 3 | const getApp = require('./getApp.js'); 4 | const getAppVersion = require('./getAppVersion.js'); 5 | const getDevices = require('./getDevices.js'); 6 | const getPatches = require('./getPatches.js'); 7 | const getSettings = require('./getSettings.js'); 8 | const installReVanced = require('./installReVanced.js'); 9 | const patchApp = require('./patchApp.js'); 10 | const resetPatchOptions = require('./resetPatchOptions.js'); 11 | const resetSettings = require('./resetSettings.js'); 12 | const selectApp = require('./selectApp.js'); 13 | const selectAppVersion = require('./selectAppVersion.js'); 14 | const selectPatches = require('./selectPatches.js'); 15 | const setDevice = require('./setDevice.js'); 16 | const setSettings = require('./setSettings.js'); 17 | const updateFiles = require('./updateFiles.js'); 18 | 19 | module.exports = { 20 | checkFileAlreadyExists, 21 | checkForUpdates, 22 | getApp, 23 | getAppVersion, 24 | getDevices, 25 | getPatches, 26 | getSettings, 27 | installReVanced, 28 | patchApp, 29 | resetPatchOptions, 30 | resetSettings, 31 | selectApp, 32 | selectAppVersion, 33 | selectPatches, 34 | setDevice, 35 | setSettings, 36 | updateFiles 37 | }; 38 | -------------------------------------------------------------------------------- /wsEvents/installReVanced.js: -------------------------------------------------------------------------------- 1 | const { join: joinPath } = require('node:path'); 2 | 3 | const exec = require('../utils/promisifiedExec.js'); 4 | 5 | const { getAppVersion_ } = require('../utils/getAppVersion.js'); 6 | const { getDownloadLink } = require('../utils/FileDownloader.js'); 7 | const { getSources } = require('../utils/Settings.js'); 8 | const mountReVancedInstaller = require('../utils/mountReVancedInstaller.js'); 9 | 10 | module.exports = async function installReVanced(ws) { 11 | if ( 12 | !global.jarNames.isRooted && 13 | global.jarNames.devices && 14 | global.jarNames.devices[0] 15 | ) { 16 | for (const deviceId of global.jarNames.devices) { 17 | ws.send( 18 | JSON.stringify({ 19 | event: 'patchLog', 20 | log: `INFO: Installing ReVanced (non-root) for ${deviceId}.` 21 | }) 22 | ); 23 | try { 24 | await exec( 25 | `adb -s ${deviceId} install "${joinPath( 26 | global.revancedDir, 27 | global.outputName 28 | )}"` 29 | ); 30 | 31 | ws.send( 32 | JSON.stringify({ 33 | event: 'patchLog', 34 | log: `INFO: Installed ReVanced (non-root) for ${deviceId}.` 35 | }) 36 | ); 37 | } catch (e) { 38 | ws.send( 39 | JSON.stringify({ 40 | event: 'patchLog', 41 | log: `SEVERE: Could not install ReVanced to ${deviceId}. Please check the console for more info.` 42 | }) 43 | ); 44 | 45 | throw e; 46 | } 47 | } 48 | } else if ( 49 | global.jarNames.isRooted && 50 | global.jarNames.devices && 51 | global.jarNames.devices[0] 52 | ) { 53 | for (const deviceId of global.jarNames.devices) { 54 | ws.send( 55 | JSON.stringify({ 56 | event: 'patchLog', 57 | log: `INFO: Installing ReVanced (root) for ${deviceId}.` 58 | }) 59 | ); 60 | try { 61 | mountReVancedInstaller(deviceId); 62 | 63 | ws.send( 64 | JSON.stringify({ 65 | event: 'patchLog', 66 | log: `INFO: Installed ReVanced (root) for ${deviceId}.` 67 | }) 68 | ); 69 | } catch (e) { 70 | ws.send( 71 | JSON.stringify({ 72 | event: 'patchLog', 73 | log: `SEVERE: Could not install ReVanced to ${deviceId}. Please check the console for more info.` 74 | }) 75 | ); 76 | 77 | throw e; 78 | } 79 | } 80 | } 81 | 82 | if ( 83 | !global.jarNames.isRooted && 84 | global.jarNames.devices && 85 | global.jarNames.devices[0] && 86 | (global.jarNames.selectedApp.packageName === 'com.google.android.youtube' || 87 | global.jarNames.selectedApp.packageName === 88 | 'com.google.android.apps.youtube.music') 89 | ) { 90 | const source = getSources(); 91 | const microg = source.microg.split('/'); 92 | const currentMicroGVersion = ( 93 | await getDownloadLink({ owner: microg[0], repo: microg[1] }) 94 | ).version 95 | .replace('v', '') 96 | .split('-')[0]; 97 | 98 | for (const deviceId of global.jarNames.devices) { 99 | const microGVersion = await getAppVersion_( 100 | 'app.revanced.android.gms', 101 | null, 102 | false, 103 | deviceId 104 | ); 105 | 106 | if (!microGVersion || microGVersion !== currentMicroGVersion) { 107 | try { 108 | await exec(`adb -s ${deviceId} install "${global.jarNames.microG}"`); 109 | 110 | ws.send( 111 | JSON.stringify({ 112 | event: 'patchLog', 113 | log: `INFO: MicroG has been ${ 114 | !microGVersion ? 'installed' : 'updated' 115 | } for device ${deviceId}.` 116 | }) 117 | ); 118 | } catch (e) { 119 | ws.send( 120 | JSON.stringify({ 121 | event: 'patchLog', 122 | log: `SEVERE: Could not ${ 123 | !microGVersion ? 'install' : 'update' 124 | } MicroG for device ${deviceId}. Please check the console for more info.` 125 | }) 126 | ); 127 | 128 | throw e; 129 | } 130 | } else 131 | ws.send( 132 | JSON.stringify({ 133 | event: 'patchLog', 134 | log: `MicroG is already up to date for device ${deviceId}` 135 | }) 136 | ); 137 | } 138 | } 139 | 140 | return ws.send( 141 | JSON.stringify({ 142 | event: 'buildFinished' 143 | }) 144 | ); 145 | }; 146 | -------------------------------------------------------------------------------- /wsEvents/patchApp.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('node:child_process'); 2 | const { version } = require('node:os'); 3 | const { existsSync, renameSync } = require('node:fs'); 4 | const { join, sep: separator } = require('node:path'); 5 | const { getSources } = require('../utils/Settings.js'); 6 | 7 | const exec = require('../utils/promisifiedExec.js'); 8 | 9 | const resetPatchOptions = require('./resetPatchOptions.js'); 10 | const mountReVanced = require('../utils/mountReVanced.js'); 11 | 12 | /** 13 | * @param {import('ws').WebSocket} ws 14 | */ 15 | async function mount(ws) { 16 | ws.send( 17 | JSON.stringify({ 18 | event: 'patchLog', 19 | log: 'Trying to mount ReVanced...' 20 | }) 21 | ); 22 | 23 | await mountReVanced(global.jarNames.selectedApp.packageName, ws); 24 | } 25 | 26 | /** 27 | * @param {import('ws').WebSocket} ws 28 | */ 29 | async function afterBuild(ws) { 30 | outputName(); 31 | renameSync( 32 | join(global.revancedDir, 'revanced.apk'), 33 | join(global.revancedDir, global.outputName) 34 | ); 35 | 36 | if (!global.jarNames.isRooted && process.platform === 'android') { 37 | await exec( 38 | `cp "${join( 39 | global.revancedDir, 40 | global.outputName 41 | )}" "/storage/emulated/0/${global.outputName}"` 42 | ); 43 | await exec(`cp "${global.jarNames.microG}" /storage/emulated/0/microg.apk`); 44 | 45 | ws.send( 46 | JSON.stringify({ 47 | event: 'patchLog', 48 | log: `Copied files over to /storage/emulated/0/!` 49 | }) 50 | ); 51 | ws.send( 52 | JSON.stringify({ 53 | event: 'patchLog', 54 | log: `Please install ReVanced, its located in /storage/emulated/0/${global.outputName}` 55 | }) 56 | ); 57 | } else if (process.platform === 'android') { 58 | try { 59 | await exec( 60 | `su -c pm install -r "${join( 61 | global.revancedDir, 62 | global.jarNames.selectedApp.packageName 63 | )}.apk"` 64 | ); 65 | } catch {} 66 | await mount(ws); 67 | } else if (!(global.jarNames.devices && global.jarNames.devices[0])) { 68 | ws.send( 69 | JSON.stringify({ 70 | event: 'patchLog', 71 | log: `ReVanced has been built!` 72 | }) 73 | ); 74 | ws.send( 75 | JSON.stringify({ 76 | event: 'patchLog', 77 | log: `Please transfer over revanced/${global.outputName} and install them!` 78 | }) 79 | ); 80 | } 81 | 82 | if (global.jarNames.devices && global.jarNames.devices[0]) { 83 | ws.send(JSON.stringify({ event: 'buildFinished', install: true })); 84 | } else ws.send(JSON.stringify({ event: 'buildFinished' })); 85 | } 86 | 87 | async function reinstallReVanced() { 88 | let pkgNameToGetUninstalled; 89 | 90 | switch (global.jarNames.selectedApp.packageName) { 91 | case 'com.google.android.youtube': 92 | if (!global.jarNames.isRooted) 93 | pkgNameToGetUninstalled = 'app.rvx.android.youtube'; 94 | break; 95 | case 'com.google.android.apps.youtube.music': 96 | if (!global.jarNames.isRooted) 97 | pkgNameToGetUninstalled = 'app.rvx.android.apps.youtube.music'; 98 | break; 99 | } 100 | 101 | await exec( 102 | `adb -s ${global.jarNames.deviceID} uninstall ${pkgNameToGetUninstalled}` 103 | ); 104 | await exec( 105 | `adb -s ${global.jarNames.deviceID} install ${join( 106 | global.revancedDir, 107 | global.outputName 108 | )}` 109 | ); 110 | } 111 | 112 | function outputName() { 113 | const part1 = 'ReVanced'; 114 | let part2 = global.jarNames?.selectedApp?.appName 115 | ? global.jarNames.selectedApp.appName.replace(/[^a-zA-Z0-9\\.\\-]/g, '') 116 | : global?.jarNames?.packageName 117 | ? global.jarNames.packageName.replace(/\./g, '') 118 | : ''; // make the app name empty if we cannot detect it 119 | 120 | // TODO: If the existing input APK is used from revanced/ without downloading, version and arch aren't set 121 | const part3 = global?.apkInfo?.version ? `v${global.apkInfo.version}` : ''; 122 | const part4 = global?.apkInfo?.arch; 123 | const part5 = `cli_${global.jarNames.cli 124 | .split(separator) 125 | .at(-1) 126 | .replace('revanced-cli-', '') 127 | .replace('.jar', '')}`; 128 | const part6 = `patches_${global.jarNames.patchesJar 129 | .split(separator) 130 | .at(-1) 131 | .replace('revanced-patches-', '') 132 | .replace('.jar', '') 133 | .replace('.rvp', '')}`; 134 | 135 | // Filename: ReVanced---[Arch]-cli_-patches_.apk 136 | let outputName = ''; 137 | 138 | for (const part of [part1, part2, part3, part4, part5, part6]) 139 | if (part) outputName += `-${part}`; 140 | 141 | outputName += '.apk'; 142 | 143 | global.outputName = outputName.substring(1); 144 | } 145 | 146 | /** 147 | * @param {string[]} args 148 | * @param {import('ws').WebSocket} ws 149 | */ 150 | function reportSys(args, ws) { 151 | ws.send( 152 | JSON.stringify({ 153 | event: 'error', 154 | error: 155 | 'An error occured while starting the patching process. Please see the server console.' 156 | }) 157 | ); 158 | 159 | console.log( 160 | '[builder] Please report these informations to https://github.com/inotia00/rvx-builder/issues' 161 | ); 162 | console.log( 163 | `OS: ${process.platform}\nArguements: ${args.join( 164 | ', ' 165 | )}\n OS Version${version()}` 166 | ); 167 | } 168 | 169 | /** 170 | * @param {import('ws').WebSocket} ws 171 | */ 172 | module.exports = async function patchApp(ws, message) { 173 | if (!existsSync('options.json')) { 174 | resetPatchOptions(ws); 175 | } 176 | 177 | const source = getSources(); 178 | const cli4 = source.cli4 == 'true'; 179 | 180 | /** @type {string[]} */ 181 | const args = [ 182 | '-jar', 183 | global.jarNames.cli, 184 | 'patch', 185 | `${join(global.revancedDir, global.jarNames.selectedApp.packageName)}.apk`, 186 | '-f', 187 | '--purge', 188 | '-o', 189 | join(global.revancedDir, 'revanced.apk') 190 | ]; 191 | 192 | if (cli4) { 193 | args.push('-b'); 194 | args.push(global.jarNames.patchesJar); 195 | args.push('-m'); 196 | args.push(global.jarNames.integrations); 197 | args.push('--options'); 198 | } else { 199 | args.push('-p'); 200 | args.push(global.jarNames.patchesJar); 201 | args.push('--legacy-options'); 202 | } 203 | 204 | args.push('./options.json'); 205 | 206 | if (process.platform === 'android') { 207 | args.push('--custom-aapt2-binary'); 208 | args.push(join(global.revancedDir, 'aapt2')); 209 | 210 | if (message.ripLibs) { 211 | switch (process.arch) { 212 | case 'arm': 213 | args.push('--rip-lib=arm64-v8a'); 214 | args.push('--rip-lib=x86'); 215 | args.push('--rip-lib=x86_64'); 216 | break; 217 | case 'arm64': 218 | args.push('--rip-lib=armeabi-v7a'); 219 | args.push('--rip-lib=x86'); 220 | args.push('--rip-lib=x86_64'); 221 | break; 222 | case 'ia32': 223 | args.push('--rip-lib=armeabi-v7a'); 224 | args.push('--rip-lib=arm64-v8a'); 225 | args.push('--rip-lib=x86_64'); 226 | break; 227 | case 'x64': 228 | args.push('--rip-lib=armeabi-v7a'); 229 | args.push('--rip-lib=arm64-v8a'); 230 | args.push('--rip-lib=x86'); 231 | break; 232 | } 233 | } 234 | } 235 | 236 | if (cli4) { 237 | args.push(...global.jarNames.includedPatches); 238 | args.push(...global.jarNames.excludedPatches); 239 | } else { 240 | const patchesArray = []; 241 | for (const patch of global.jarNames.excludedPatches) { 242 | patchesArray.push(patch.replace('-e', '-d')); 243 | } 244 | for (const patch of global.jarNames.includedPatches) { 245 | patchesArray.push(patch.replace('-i', '-e')); 246 | } 247 | args.push(...patchesArray); 248 | } 249 | const buildProcess = spawn(global.javaCmd, args); 250 | 251 | buildProcess.stdout.on('data', async (data) => { 252 | ws.send( 253 | JSON.stringify({ 254 | event: 'patchLog', 255 | log: data.toString() 256 | }) 257 | ); 258 | 259 | if (data.toString().includes('Purged') || data.toString().includes('purge')) await afterBuild(ws); 260 | 261 | if (data.toString().includes('INSTALL_FAILED_UPDATE_INCOMPATIBLE')) { 262 | await reinstallReVanced(ws); 263 | await afterBuild(ws); 264 | } 265 | 266 | if (data.toString().includes('Unmatched')) reportSys(args, ws); 267 | }); 268 | }; 269 | -------------------------------------------------------------------------------- /wsEvents/resetPatchOptions.js: -------------------------------------------------------------------------------- 1 | const exec = require('../utils/promisifiedExec.js'); 2 | 3 | /** 4 | * @param {import('ws').WebSocket} ws 5 | */ 6 | module.exports = async function resetPatchOptions(ws) { 7 | const java = `${global.javaCmd}`; 8 | const cli = `${global.jarNames.cli}`; 9 | const patches = `${global.jarNames.patchesJar}`; 10 | 11 | await exec( 12 | `${java} -jar "${cli}" options --overwrite "${patches}"` 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /wsEvents/resetSettings.js: -------------------------------------------------------------------------------- 1 | const { resetPatchesSources } = require('../utils/Settings.js'); 2 | 3 | /** 4 | * @param {import('ws').WebSocket} ws 5 | */ 6 | 7 | module.exports = function resetSettings(ws) { 8 | resetPatchesSources(ws); 9 | }; 10 | -------------------------------------------------------------------------------- /wsEvents/selectApp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Record} message 3 | */ 4 | module.exports = function selectApp(message) { 5 | global.jarNames.selectedApp = message.selectedApp; 6 | }; 7 | -------------------------------------------------------------------------------- /wsEvents/selectAppVersion.js: -------------------------------------------------------------------------------- 1 | const { downloadApp } = require('../utils/downloadApp.js'); 2 | const getDeviceArch = require('../utils/getDeviceArch.js'); 3 | 4 | /** 5 | * @param {Record} message 6 | * @param {import('ws').WebSocket} ws 7 | */ 8 | module.exports = async function selectAppVersion(message, ws) { 9 | let arch = message.arch; 10 | 11 | if ( 12 | (global.jarNames.selectedApp.packageName === 13 | 'com.google.android.apps.youtube.music' && 14 | global.jarNames.devices && 15 | global.jarNames.devices[0]) || 16 | process.platform === 'android' 17 | ) { 18 | arch = await getDeviceArch(ws); 19 | } 20 | 21 | global.apkInfo = { 22 | version: message.versionChoosen, 23 | arch 24 | }; 25 | 26 | await downloadApp(ws); 27 | }; 28 | -------------------------------------------------------------------------------- /wsEvents/selectPatches.js: -------------------------------------------------------------------------------- 1 | const { writePatches } = require('../utils/Settings.js'); 2 | 3 | /** 4 | * @param {Record} message 5 | */ 6 | module.exports = function selectPatches(message) { 7 | global.jarNames.includedPatches = []; 8 | global.jarNames.excludedPatches = []; 9 | 10 | writePatches(global.jarNames.selectedApp, message.selectedPatches); 11 | 12 | /** @type {string[]} */ 13 | const includedPatchesArray = []; 14 | 15 | for (const patch of message.selectedPatches) { 16 | const patchName = patch.replace(/\|.+$/, ''); 17 | 18 | includedPatchesArray.push(patchName); 19 | 20 | global.jarNames.includedPatches.push(`-i`); 21 | global.jarNames.includedPatches.push(patchName); 22 | } 23 | 24 | global.jarNames.isRooted = false; 25 | 26 | for (const patch of message.excludedPatches) { 27 | const patchName = patch.replace(/\|.+$/, ''); 28 | 29 | if (includedPatchesArray.includes(patchName)) continue; 30 | 31 | if (patch.includes('MicroG support') || patch.includes('GmsCore support')) { 32 | global.jarNames.isRooted = true; 33 | } 34 | 35 | global.jarNames.excludedPatches.push(`-e`); 36 | global.jarNames.excludedPatches.push(patchName); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /wsEvents/setDevice.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {Record} message 3 | */ 4 | module.exports = function setDevice(message) { 5 | global.jarNames.devices = message.devices; 6 | }; 7 | -------------------------------------------------------------------------------- /wsEvents/setSettings.js: -------------------------------------------------------------------------------- 1 | const { writeSources } = require('../utils/Settings.js'); 2 | /** 3 | * @param {Record} message 4 | */ 5 | 6 | module.exports = function setSettings(message) { 7 | writeSources(message.settings); 8 | }; 9 | -------------------------------------------------------------------------------- /wsEvents/updateFiles.js: -------------------------------------------------------------------------------- 1 | const { existsSync, mkdirSync, rmSync } = require('node:fs'); 2 | const { join: joinPath } = require('node:path'); 3 | const { getSources } = require('../utils/Settings.js'); 4 | const { downloadFiles } = require('../utils/FileDownloader.js'); 5 | const checkJDKAndAapt2 = require('../utils/checkJDKAndAapt2.js'); 6 | const checkJDkAndADB = require('../utils/checkJDKAndADB.js'); 7 | 8 | global.revancedDir = joinPath(process.cwd(), 'revanced'); 9 | global.javaCmd = 'java'; 10 | global.jarNames = { 11 | cli: '', 12 | patchesJar: global.revancedDir, 13 | integrations: global.revancedDir, 14 | microG: global.revancedDir, 15 | apkEditor: global.revancedDir, 16 | patchesList: global.revancedDir, 17 | selectedApp: '', 18 | patches: '', 19 | isRooted: false, 20 | deviceID: '' 21 | }; 22 | 23 | /** 24 | * @param {import('ws').WebSocket} ws 25 | */ 26 | module.exports = async function updateFiles(ws) { 27 | const source = getSources(); 28 | const cli = source.cli.split('/'); 29 | const patches = source.patches.split('/'); 30 | const integrations = source.integrations.split('/'); 31 | const microg = source.microg.split('/'); 32 | const preReleases = source.prereleases == 'true'; 33 | const cli4 = source.cli4 == 'true'; 34 | 35 | if (!existsSync(global.revancedDir)) mkdirSync(global.revancedDir); 36 | 37 | const filesToDownloadCli4 = [ 38 | { 39 | owner: cli[0], 40 | repo: cli[1] 41 | }, 42 | { 43 | owner: patches[0], 44 | repo: patches[1] 45 | }, 46 | { 47 | owner: integrations[0], 48 | repo: integrations[1] 49 | }, 50 | { 51 | owner: microg[0], 52 | repo: microg[1] 53 | }, 54 | { 55 | owner: 'REAndroid', 56 | repo: 'APKEditor' 57 | } 58 | ]; 59 | const filesToDownloadCli5 = [ 60 | { 61 | owner: cli[0], 62 | repo: cli[1] 63 | }, 64 | { 65 | owner: patches[0], 66 | repo: patches[1] 67 | }, 68 | { 69 | owner: microg[0], 70 | repo: microg[1] 71 | }, 72 | { 73 | owner: 'REAndroid', 74 | repo: 'APKEditor' 75 | } 76 | ]; 77 | 78 | if ( 79 | typeof global.downloadFinished !== 'undefined' && 80 | !global.downloadFinished 81 | ) { 82 | ws.send( 83 | JSON.stringify({ 84 | event: 'error', 85 | error: 86 | "Downloading process hasn't finished and you tried to download again." 87 | }) 88 | ); 89 | 90 | return; 91 | } 92 | 93 | global.downloadFinished = false; 94 | if (cli4) await downloadFiles(filesToDownloadCli4, preReleases, cli4, ws); 95 | else await downloadFiles(filesToDownloadCli5, preReleases, cli4, ws); 96 | 97 | if (process.platform === 'android') await checkJDKAndAapt2(ws); 98 | else await checkJDkAndADB(ws); 99 | 100 | global.downloadFinished = true; 101 | 102 | ws.send( 103 | JSON.stringify({ 104 | event: 'finished' 105 | }) 106 | ); 107 | }; 108 | --------------------------------------------------------------------------------