Compare commits
No commits in common. "main" and "v250.0" have entirely different histories.
39 changed files with 732 additions and 3456 deletions
137
.github/workflows/build.yml
vendored
Normal file
137
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
ipa_url:
|
||||||
|
description: "Direct link to the decrypted IPA"
|
||||||
|
default: ""
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-tweak:
|
||||||
|
runs-on: macos-14
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
|
||||||
|
- name: Download Tweak
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
|
||||||
|
release_info=$(gh api --header 'Accept: application/vnd.github+json' repos/${{ github.repository }}/releases/latest)
|
||||||
|
status_code=$(echo $release_info | jq -r ".status")
|
||||||
|
|
||||||
|
if [ "$status_code" != "null" ]; then
|
||||||
|
echo "No releases found or request failed, status code: $status_code"
|
||||||
|
echo "DEB_DOWNLOADED=false" >> $GITHUB_ENV
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
release_version=$(echo "$release_info" | jq -r '.assets[] | select(.name | contains("iphoneos-arm.deb")) | .name' | grep -o '_[0-9.]\+_' | tr -d '_')
|
||||||
|
control_version=$(grep '^Version:' control | cut -d ' ' -f 2)
|
||||||
|
|
||||||
|
if [ "$release_version" = "$control_version" ]; then
|
||||||
|
echo "Versions match. Downloading DEB files..."
|
||||||
|
echo "$release_info" | jq -r '.assets[] | select(.name | endswith(".deb")) | .browser_download_url' | xargs -I {} curl -L -O {}
|
||||||
|
echo "DEB_DOWNLOADED=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "Versions do not match. No files will be downloaded."
|
||||||
|
echo "DEB_DOWNLOADED=false" >> $GITHUB_ENV
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check cache
|
||||||
|
if: env.DEB_DOWNLOADED == 'false'
|
||||||
|
run: echo upstream_heads=`git ls-remote https://github.com/theos/theos | head -n 1 | cut -f 1`-`git ls-remote https://github.com/theos/sdks | head -n 1 | cut -f 1` >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Use cache
|
||||||
|
if: env.DEB_DOWNLOADED == 'false'
|
||||||
|
id: cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ github.workspace }}/theos
|
||||||
|
key: ${{ runner.os }}-${{ env.upstream_heads }}
|
||||||
|
|
||||||
|
- name: Prepare Theos
|
||||||
|
if: env.DEB_DOWNLOADED == 'false'
|
||||||
|
uses: Randomblock1/theos-action@v1
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
if: env.DEB_DOWNLOADED == 'false'
|
||||||
|
run: make package FINALPACKAGE=1 && make package FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless
|
||||||
|
|
||||||
|
- name: Set DEB path
|
||||||
|
run: |
|
||||||
|
if [ "${{ env.DEB_DOWNLOADED }}" == "true" ]; then
|
||||||
|
echo "DEB_PATH=${{ github.workspace }}/*-arm.deb" >> $GITHUB_ENV
|
||||||
|
echo "ROOTLESS_DEB_PATH=${{ github.workspace }}/*-arm64.deb" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "DEB_PATH=${{ github.workspace }}/packages/*-arm.deb" >> $GITHUB_ENV
|
||||||
|
echo "ROOTLESS_DEB_PATH=${{ github.workspace }}/packages/*-arm64.deb" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload package
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: package
|
||||||
|
path: ${{ env.DEB_PATH }}
|
||||||
|
|
||||||
|
- name: Upload rootless package
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rootless package
|
||||||
|
path: ${{ env.ROOTLESS_DEB_PATH }}
|
||||||
|
|
||||||
|
build-ipa:
|
||||||
|
runs-on: macos-14
|
||||||
|
needs: build-tweak
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download build artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Download Discord IPA
|
||||||
|
run: |
|
||||||
|
curl -L -o discord.ipa ${{ github.event.inputs.ipa_url }}
|
||||||
|
|
||||||
|
- name: Download IPA icons
|
||||||
|
run: |
|
||||||
|
curl -L -o ipa-icons.zip https://raw.githubusercontent.com/pyoncord/assets/main/ipa-icons.zip
|
||||||
|
unzip ipa-icons.zip
|
||||||
|
|
||||||
|
- name: Extract Values
|
||||||
|
run: |
|
||||||
|
NAME=$(grep '^Name:' control | cut -d ' ' -f 2)
|
||||||
|
PACKAGE=$(grep '^Package:' control | cut -d ' ' -f 2)
|
||||||
|
VERSION=$(grep '^Version:' control | cut -d ' ' -f 2)
|
||||||
|
DEB_FILE_NAME="${PACKAGE}_${VERSION}_iphoneos-arm.deb"
|
||||||
|
echo "DEB_FILE_NAME=$DEB_FILE_NAME" >> $GITHUB_ENV
|
||||||
|
echo "APP_NAME=$NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Install cyan
|
||||||
|
run: pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip Pillow
|
||||||
|
|
||||||
|
- name: Inject tweak
|
||||||
|
run: |
|
||||||
|
cyan -n ${{ env.APP_NAME }} -k PyoncordIcon60x60@2x.png -d -i discord.ipa -o ${{ github.workspace }}/${{ env.APP_NAME }}.ipa -f ${{ github.workspace }}/${{ env.DEB_FILE_NAME }} -uws
|
||||||
|
|
||||||
|
- name: Upload ipa as artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ipa
|
||||||
|
path: ${{ github.workspace }}/${{ env.APP_NAME }}.ipa
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
name: Deploy
|
name: Release
|
||||||
run-name: ${{ github.event.inputs.release == 'true' && 'Release' || 'Build' }} for ${{ github.event.inputs.ipa_url }}
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
@ -9,15 +8,10 @@ on:
|
||||||
default: ""
|
default: ""
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
release:
|
|
||||||
description: 'Release the build'
|
|
||||||
required: true
|
|
||||||
type: boolean
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-tweak:
|
build-tweak:
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
|
|
@ -72,10 +66,6 @@ jobs:
|
||||||
if: env.DEB_DOWNLOADED == 'false'
|
if: env.DEB_DOWNLOADED == 'false'
|
||||||
uses: Randomblock1/theos-action@v1
|
uses: Randomblock1/theos-action@v1
|
||||||
|
|
||||||
- name: Set GNU Make path
|
|
||||||
run: |
|
|
||||||
echo "PATH=$(brew --prefix make)/libexec/gnubin:$PATH" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
if: env.DEB_DOWNLOADED == 'false'
|
if: env.DEB_DOWNLOADED == 'false'
|
||||||
run: make package FINALPACKAGE=1 && make package FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless
|
run: make package FINALPACKAGE=1 && make package FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless
|
||||||
|
|
@ -122,6 +112,7 @@ jobs:
|
||||||
- name: Download IPA icons
|
- name: Download IPA icons
|
||||||
run: |
|
run: |
|
||||||
curl -L -o ipa-icons.zip https://raw.githubusercontent.com/pyoncord/assets/main/ipa-icons.zip
|
curl -L -o ipa-icons.zip https://raw.githubusercontent.com/pyoncord/assets/main/ipa-icons.zip
|
||||||
|
unzip ipa-icons.zip
|
||||||
|
|
||||||
- name: Extract Values
|
- name: Extract Values
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -133,24 +124,13 @@ jobs:
|
||||||
ROOTLESS_DEB_FILE_NAME="${PACKAGE}_${VERSION}_iphoneos-arm64.deb"
|
ROOTLESS_DEB_FILE_NAME="${PACKAGE}_${VERSION}_iphoneos-arm64.deb"
|
||||||
echo "ROOTLESS_DEB_FILE_NAME=$ROOTLESS_DEB_FILE_NAME" >> $GITHUB_ENV
|
echo "ROOTLESS_DEB_FILE_NAME=$ROOTLESS_DEB_FILE_NAME" >> $GITHUB_ENV
|
||||||
echo "APP_NAME=$NAME" >> $GITHUB_ENV
|
echo "APP_NAME=$NAME" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Run patcher
|
|
||||||
run: |
|
|
||||||
curl -L -o patcher https://github.com/bunny-mod/ipa-patcher/releases/download/release-pyon/patcher.mac-amd64
|
|
||||||
echo "08a92b45bb00cea91c2d08d4ac48f7ef3f6bf352fe1e3aa204f953ec2db9766e patcher" > patcher.sha256
|
|
||||||
if ! shasum -a 256 -c patcher.sha256; then
|
|
||||||
echo "SHA256 verification failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
chmod +x patcher
|
|
||||||
./patcher -d discord.ipa -o discord.ipa -i ./ipa-icons.zip
|
|
||||||
|
|
||||||
- name: Install cyan
|
- name: Install cyan
|
||||||
run: pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip Pillow
|
run: pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip Pillow
|
||||||
|
|
||||||
- name: Inject tweak
|
- name: Inject tweak
|
||||||
run: |
|
run: |
|
||||||
cyan -duws -i discord.ipa -o ${{ github.workspace }}/${{ env.APP_NAME }}.ipa -f ${{ github.workspace }}/${{ env.DEB_FILE_NAME }}
|
cyan -n ${{ env.APP_NAME }} -k PyoncordIcon60x60@2x.png -d -i discord.ipa -o ${{ github.workspace }}/${{ env.APP_NAME }}.ipa -f ${{ github.workspace }}/${{ env.DEB_FILE_NAME }} -uws
|
||||||
|
|
||||||
- name: Upload ipa as artifact
|
- name: Upload ipa as artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
@ -159,7 +139,6 @@ jobs:
|
||||||
path: ${{ github.workspace }}/${{ env.APP_NAME }}.ipa
|
path: ${{ github.workspace }}/${{ env.APP_NAME }}.ipa
|
||||||
|
|
||||||
release-app:
|
release-app:
|
||||||
if: ${{ github.event.inputs.release == 'true' }}
|
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
needs: build-ipa
|
needs: build-ipa
|
||||||
permissions:
|
permissions:
|
||||||
|
|
@ -206,7 +185,6 @@ jobs:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
app-repo:
|
app-repo:
|
||||||
if: ${{ github.event.inputs.release == 'true' }}
|
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
needs: release-app
|
needs: release-app
|
||||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
|
|
@ -4,11 +4,5 @@
|
||||||
"MD033": false,
|
"MD033": false,
|
||||||
"MD045": false,
|
"MD045": false,
|
||||||
},
|
},
|
||||||
"VsCodeTaskButtons.tasks": [
|
"deepscan.ignoreConfirmWarning": true
|
||||||
{
|
|
||||||
"label": "$(tools) Make and Copy",
|
|
||||||
"task": "Make and Copy Tweak",
|
|
||||||
"tooltip": "Make and copy Tweak"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
35
.vscode/tasks.json
vendored
35
.vscode/tasks.json
vendored
|
|
@ -1,35 +0,0 @@
|
||||||
{
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"type": "shell",
|
|
||||||
"label": "Make and Copy Tweak",
|
|
||||||
"command": "zsh",
|
|
||||||
"args": [
|
|
||||||
"-c",
|
|
||||||
"rm -rf packages && make clean && make package FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless && cp ./packages/app.pyoncord_*arm64.deb ~/Library/Mobile\\ Documents/com~apple~CloudDocs/"
|
|
||||||
],
|
|
||||||
"problemMatcher": {
|
|
||||||
"owner": "cpp",
|
|
||||||
"pattern": {
|
|
||||||
"regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
|
|
||||||
"file": 1,
|
|
||||||
"line": 2,
|
|
||||||
"column": 3,
|
|
||||||
"severity": 4,
|
|
||||||
"message": 5
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"group": {
|
|
||||||
"kind": "build",
|
|
||||||
"isDefault": true
|
|
||||||
},
|
|
||||||
"presentation": {
|
|
||||||
"panel": "shared",
|
|
||||||
"showReuseMessage": false,
|
|
||||||
"clear": true,
|
|
||||||
"close": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
101
AUDIT.md
101
AUDIT.md
|
|
@ -1,101 +0,0 @@
|
||||||
# iOS parity audit (2026-08-03)
|
|
||||||
|
|
||||||
Revenge dropped official iOS support; this repo was archived upstream in Nov 2024
|
|
||||||
(`revenge-mod/revenge-tweak`, last commit `35ad28b`) while the Android loader
|
|
||||||
(`revenge-mod/revenge-xposed`) kept shipping. This audit compares the two and fixes
|
|
||||||
what could be fixed without a jailbroken device to test against.
|
|
||||||
|
|
||||||
## Fixed in this fork
|
|
||||||
|
|
||||||
- **`Sources/Sideloading.x`**: `BunnyLog(...)` referenced a macro that no longer
|
|
||||||
exists (the project was renamed Bunny → Revenge; only `RevengeLog` is defined in
|
|
||||||
`Headers/Logger.h`). This made the file **fail to compile**. Fixed by using
|
|
||||||
`RevengeLog`.
|
|
||||||
- **`Sources/LoaderConfig.m`**: `-loadConfig` and `+defaultConfig` were each defined
|
|
||||||
twice in the same `@implementation` (leftover merge conflict). Removed the
|
|
||||||
duplicates.
|
|
||||||
- **`.github/workflows/deploy.yml`**: the CI patcher step pointed at
|
|
||||||
`Cristiandis/ipa-patcher` release `release-pyon`, which 404s — that repo no longer
|
|
||||||
exists. Repointed at the still-live fork `bunny-mod/ipa-patcher` (same tag,
|
|
||||||
release build) and pinned it with a verified SHA-256 checksum.
|
|
||||||
(These three match the fixes proposed in the never-merged upstream PR #8, which
|
|
||||||
the sole maintainer confirmed worked but never landed before the repo was archived.)
|
|
||||||
|
|
||||||
## The real blocker: this tweak's hook point is very likely dead
|
|
||||||
|
|
||||||
`Sources/Tweak.x` hooks `RCTCxxBridge -executeApplicationScript:url:async:` — the
|
|
||||||
script-injection point for React Native's **legacy bridge**. Evidence points to
|
|
||||||
Discord having moved off that architecture entirely:
|
|
||||||
|
|
||||||
- `revenge-bundle`'s `package.json` pins `react-native: 0.81.4`, which defaults to
|
|
||||||
the New Architecture ("Bridgeless mode").
|
|
||||||
- `revenge-xposed` (the actively-maintained Android loader) hooks
|
|
||||||
`com.facebook.react.runtime.ReactInstance$loadJSBundle$1` — the Bridgeless-mode
|
|
||||||
class — not the legacy `CatalystInstanceImpl`, and its `DiscordDevSupport.kt`
|
|
||||||
explicitly targets `com.facebook.react.devsupport.BridgelessDevSupportManager`.
|
|
||||||
|
|
||||||
Cross-platform RN apps built from one codebase almost always enable/disable the New
|
|
||||||
Architecture together, so if Android Discord is Bridgeless, iOS Discord almost
|
|
||||||
certainly is too — meaning `RCTCxxBridge` is likely never instantiated on current
|
|
||||||
Discord iOS builds, and the tweak's only injection point silently never fires.
|
|
||||||
|
|
||||||
**This is now confirmed, not just theorized.** [RainTweak](https://codeberg.org/raincord/RainTweak)
|
|
||||||
is the iOS loader for `raincord/rain`, a sibling Revenge/Bunny-derived mod that is
|
|
||||||
*actively shipping* (last commit 2026-07-17, vs. this repo's Nov 2024). Its `%ctor`
|
|
||||||
explicitly checks `RCTNewArchEnabled` from Info.plist and refuses to run with
|
|
||||||
"Please update Discord to v332 or above" if it's off — i.e. current Discord iOS is
|
|
||||||
Bridgeless-only, exactly as the Android-side evidence predicted, and RainTweak has
|
|
||||||
real users validating this on-device.
|
|
||||||
|
|
||||||
**Fix applied**, ported from RainTweak's verified mechanism (better than the
|
|
||||||
`RCTInstance -_loadScriptFromSource:` + fake-`RCTSource` approach this fork
|
|
||||||
originally tried — see below for why that was replaced):
|
|
||||||
|
|
||||||
- `%hook RCTHost` / `-instance:didInitializeRuntime:` (`Headers/RCTHost.h`,
|
|
||||||
`Sources/Tweak.xm`) — fires once the Bridgeless JS runtime is ready. Discord's own
|
|
||||||
bundle is left completely untouched via `%orig` first; Revenge's own scripts
|
|
||||||
(loader payload, downloaded bundle, theme, preloads) are then run directly against
|
|
||||||
the live `facebook::jsi::Runtime` via `[JSI evaluate:tag:runtime:]`
|
|
||||||
(`Headers/JSI.h` / `Sources/JSI.mm`, ported near-verbatim from RainTweak's
|
|
||||||
`JSI.mm`) instead of being smuggled in as fake bundle loads.
|
|
||||||
- This sidesteps the problem the first attempt ran into: `RCTSource` (what
|
|
||||||
`-_loadScriptFromSource:` takes) has no public initializer, so faking one meant
|
|
||||||
poking private ivars — fragile and, per RainTweak's actual production code, not
|
|
||||||
even the path they use. Evaluating directly against the runtime via JSI is both
|
|
||||||
simpler and how the proven-working reference implementation does it.
|
|
||||||
- The legacy `RCTCxxBridge -executeApplicationScript:...` hook is kept as a
|
|
||||||
no-cost fallback for any host still on the old bridge (Logos skips hooking
|
|
||||||
classes that aren't present at runtime, and a `didInject` guard stops both hooks
|
|
||||||
from double-firing if they somehow both resolve).
|
|
||||||
- `Headers/jsi/{jsi.h,jsi-inl.h,instrumentation.h}` are vendored copies of Meta's
|
|
||||||
JSI headers (MIT-licensed, same as RN itself) — Theos doesn't ship them, and
|
|
||||||
RainTweak vendors the identical files for the same reason.
|
|
||||||
|
|
||||||
**Still unverified against a real device or the current Discord binary** — I don't
|
|
||||||
have a jailbreak or Xcode/Theos here. But unlike the first attempt, this is no
|
|
||||||
longer a guess extrapolated from RN's public source: the mechanism (`RCTHost`
|
|
||||||
hook + JSI evaluation) is the exact one running in a comparable, currently-shipping
|
|
||||||
mod. Confidence is meaningfully higher; it should still be tested before relying on
|
|
||||||
it, since Discord's specific build could in principle differ.
|
|
||||||
|
|
||||||
## Remaining feature gap vs. Android (not attempted here)
|
|
||||||
|
|
||||||
Beyond the injection hook, `revenge-xposed` has grown a much larger feature set
|
|
||||||
that iOS has no equivalent of. Porting these is real native engineering work, not
|
|
||||||
something to guess blind without a device to test each step against:
|
|
||||||
|
|
||||||
| Feature | Android | iOS |
|
|
||||||
|---|---|---|
|
|
||||||
| Script injection | `RevengeScriptLoader` + Bridgeless hook | Fixed above (unverified) |
|
|
||||||
| Theme/font/sideloading | `themes`/`fonts`/`sysColors`, resource-ID fix | `Theme.m`/`Fonts.x`/`Sideloading.x` (present, untouched) |
|
|
||||||
| **Native↔JS RPC bridge** | `RevengeBridge` — bidirectional calls smuggled through existing exposed RN native modules (`RNSVGRenderableManager.getBBox`, `FileReaderModule.readAsDataURL`) | **none** |
|
|
||||||
| **Plugin system** | External plugin loading + repos (`PluginLoader`, `RepoStore`, `RepoInstall`, `Resolver`, ...) | **none** |
|
|
||||||
| Internal plugins | Recovery mode, no-track, OTA-update prevention, example plugin | **none** |
|
|
||||||
| Dev support | LogBox + shake-to-recovery wired into RN's dev menu | **none** |
|
|
||||||
| Caches/lifecycle management | `Caches.kt`, `LifecycleSupport.kt` | **none** |
|
|
||||||
|
|
||||||
The bridge + plugin system is the biggest gap and the most architecturally novel
|
|
||||||
part of the Android side — it's what everything else (internal plugins, dev
|
|
||||||
support) is built on. Recommend treating it as its own follow-up project once the
|
|
||||||
injection hook above is confirmed working on-device, rather than attempting it
|
|
||||||
speculatively.
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
extern NSMutableDictionary<NSString *, NSString *> *fontMap;
|
|
||||||
void patchFonts(NSDictionary<NSString *, NSString *> *mainFonts, NSString *fontDefName);
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <jsi/jsi.h>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimal JSI helper: evaluates a script directly against a live `facebook::jsi::Runtime`,
|
|
||||||
* instead of re-entering React Native's own bundle-loading machinery. Ported from RainTweak
|
|
||||||
* (codeberg.org/raincord/RainTweak), an actively-maintained loader for a sibling Revenge/Bunny
|
|
||||||
* fork that verified this approach works on current (Bridgeless) Discord iOS builds.
|
|
||||||
*/
|
|
||||||
@interface JSI : NSObject
|
|
||||||
|
|
||||||
+ (void)evaluate:(NSData *)scriptData tag:(NSString *)tag runtime:(facebook::jsi::Runtime &)runtime;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
@interface LoaderConfig : NSObject
|
|
||||||
@property (nonatomic, assign) BOOL customLoadUrlEnabled;
|
|
||||||
@property (nonatomic, strong) NSURL *customLoadUrl;
|
|
||||||
+ (instancetype)defaultConfig;
|
|
||||||
+ (instancetype)getLoaderConfig;
|
|
||||||
- (instancetype)init;
|
|
||||||
- (BOOL)loadConfig;
|
|
||||||
@end
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
#define LOG_PREFIX @"[Revenge]"
|
|
||||||
#define RevengeLog(fmt, ...) NSLog((LOG_PREFIX @" " fmt), ##__VA_ARGS__)
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <jsi/jsi.h>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Forward declaration of React Native's Bridgeless-mode host class, just enough of it for Logos
|
|
||||||
* to hook `-instance:didInitializeRuntime:`. React Native ships no public header exposing this
|
|
||||||
* (it's an internal `ReactCommon/react/runtime` class), so the signature is declared by hand here -
|
|
||||||
* matches `RCTHost.h` in RainTweak (codeberg.org/raincord/RainTweak), which hooks the same method
|
|
||||||
* on a currently-shipping Discord iOS build.
|
|
||||||
*/
|
|
||||||
@interface RCTHost : NSObject
|
|
||||||
|
|
||||||
- (void)instance:(id)instance didInitializeRuntime:(facebook::jsi::Runtime &)runtime;
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
void swizzleDCDThemeColor(NSDictionary<NSString *, NSArray<NSString *> *> *semanticColors);
|
|
||||||
void swizzleUIColor(NSDictionary<NSString *, NSString *> *rawColors);
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
|
|
||||||
NSURL *getPyoncordDirectory(void);
|
|
||||||
UIColor *hexToUIColor(NSString *hex);
|
|
||||||
void showErrorAlert(NSString *title, NSString *message);
|
|
||||||
|
|
||||||
/** Detects the Hermes bytecode magic header ("Herm"), as opposed to plain-text JS. */
|
|
||||||
BOOL isHermesBytecode(NSData *data);
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <chrono>
|
|
||||||
#include <iosfwd>
|
|
||||||
#include <string>
|
|
||||||
#include <tuple>
|
|
||||||
#include <unordered_map>
|
|
||||||
|
|
||||||
#include <jsi/jsi.h>
|
|
||||||
|
|
||||||
namespace facebook {
|
|
||||||
namespace jsi {
|
|
||||||
|
|
||||||
/// Methods for starting and collecting instrumentation, an \c Instrumentation
|
|
||||||
/// instance is associated with a particular \c Runtime instance, which it
|
|
||||||
/// controls the instrumentation of.
|
|
||||||
/// None of these functions should return newly created jsi values, nor should
|
|
||||||
/// it modify the values of any jsi values in the heap (although GCs are fine).
|
|
||||||
class JSI_EXPORT Instrumentation {
|
|
||||||
public:
|
|
||||||
/// Additional options controlling what to include when capturing a heap
|
|
||||||
/// snapshot.
|
|
||||||
struct HeapSnapshotOptions {
|
|
||||||
bool captureNumericValue{false};
|
|
||||||
};
|
|
||||||
|
|
||||||
virtual ~Instrumentation() = default;
|
|
||||||
|
|
||||||
/// Returns GC statistics as a JSON-encoded string, with an object containing
|
|
||||||
/// "type" and "version" fields outermost. "type" is a string, unique to a
|
|
||||||
/// particular implementation of \c jsi::Instrumentation, and "version" is a
|
|
||||||
/// number to indicate any revision to that implementation and its output
|
|
||||||
/// format.
|
|
||||||
///
|
|
||||||
/// \pre This call can only be made on the instrumentation instance of a
|
|
||||||
/// runtime initialised to collect GC statistics.
|
|
||||||
///
|
|
||||||
/// \post All cumulative measurements mentioned in the output are accumulated
|
|
||||||
/// across the entire lifetime of the Runtime.
|
|
||||||
///
|
|
||||||
/// \return the GC statistics collected so far, as a JSON-encoded string.
|
|
||||||
virtual std::string getRecordedGCStats() = 0;
|
|
||||||
|
|
||||||
/// Request statistics about the current state of the runtime's heap. This
|
|
||||||
/// function can be called at any time, and should produce information that is
|
|
||||||
/// correct at the instant it is called (i.e, not stale).
|
|
||||||
///
|
|
||||||
/// \return a map from a string key to a number associated with that
|
|
||||||
/// statistic.
|
|
||||||
virtual std::unordered_map<std::string, int64_t> getHeapInfo(
|
|
||||||
bool includeExpensive) = 0;
|
|
||||||
|
|
||||||
/// Perform a full garbage collection.
|
|
||||||
/// \param cause The cause of this collection, as it should be reported in
|
|
||||||
/// logs.
|
|
||||||
virtual void collectGarbage(std::string cause) = 0;
|
|
||||||
|
|
||||||
/// A HeapStatsUpdate is a tuple of the fragment index, the number of objects
|
|
||||||
/// in that fragment, and the number of bytes used by those objects.
|
|
||||||
/// A "fragment" is a view of all objects allocated within a time slice.
|
|
||||||
using HeapStatsUpdate = std::tuple<uint64_t, uint64_t, uint64_t>;
|
|
||||||
|
|
||||||
/// Start capturing JS stack-traces for all JS heap allocated objects. These
|
|
||||||
/// can be accessed via \c ::createSnapshotToFile().
|
|
||||||
/// \param fragmentCallback If present, invoke this callback every so often
|
|
||||||
/// with the most recently seen object ID, and a list of fragments that have
|
|
||||||
/// been updated. This callback will be invoked on the same thread that the
|
|
||||||
/// runtime is using.
|
|
||||||
virtual void startTrackingHeapObjectStackTraces(
|
|
||||||
std::function<void(
|
|
||||||
uint64_t lastSeenObjectID,
|
|
||||||
std::chrono::microseconds timestamp,
|
|
||||||
std::vector<HeapStatsUpdate> stats)> fragmentCallback) = 0;
|
|
||||||
|
|
||||||
/// Stop capture JS stack-traces for JS heap allocated objects.
|
|
||||||
virtual void stopTrackingHeapObjectStackTraces() = 0;
|
|
||||||
|
|
||||||
/// Start a heap sampling profiler that will sample heap allocations, and the
|
|
||||||
/// stack trace they were allocated at. Reports a summary of which functions
|
|
||||||
/// allocated the most.
|
|
||||||
/// \param samplingInterval The number of bytes allocated to wait between
|
|
||||||
/// samples. This will be used as the expected value of a poisson
|
|
||||||
/// distribution.
|
|
||||||
virtual void startHeapSampling(size_t samplingInterval) = 0;
|
|
||||||
|
|
||||||
/// Turns off the heap sampling profiler previously enabled via
|
|
||||||
/// \c startHeapSampling. Writes the output of the sampling heap profiler to
|
|
||||||
/// \p os. The output is a JSON formatted string.
|
|
||||||
virtual void stopHeapSampling(std::ostream& os) = 0;
|
|
||||||
|
|
||||||
/// Captures the heap to a file
|
|
||||||
///
|
|
||||||
/// \param path to save the heap capture.
|
|
||||||
/// \param options additional options for what to capture.
|
|
||||||
virtual void createSnapshotToFile(
|
|
||||||
const std::string& path,
|
|
||||||
const HeapSnapshotOptions& options = {false}) = 0;
|
|
||||||
|
|
||||||
/// Captures the heap to an output stream
|
|
||||||
///
|
|
||||||
/// \param os output stream to write to.
|
|
||||||
/// \param options additional options for what to capture.
|
|
||||||
virtual void createSnapshotToStream(
|
|
||||||
std::ostream& os,
|
|
||||||
const HeapSnapshotOptions& options = {false}) = 0;
|
|
||||||
|
|
||||||
/// If the runtime has been created to trace to a temp file, flush
|
|
||||||
/// any unwritten parts of the trace of bridge traffic to the file,
|
|
||||||
/// and return the name of the file. Otherwise, return the empty string.
|
|
||||||
/// Tracing is disabled after this call.
|
|
||||||
virtual std::string flushAndDisableBridgeTrafficTrace() = 0;
|
|
||||||
|
|
||||||
/// Write basic block profile trace to the given file name.
|
|
||||||
virtual void writeBasicBlockProfileTraceToFile(
|
|
||||||
const std::string& fileName) const = 0;
|
|
||||||
|
|
||||||
/// Write the opcode stats to the given stream.
|
|
||||||
virtual void dumpOpcodeStats(std::ostream& os) const = 0;
|
|
||||||
|
|
||||||
/// Dump external profiler symbols to the given file name.
|
|
||||||
virtual void dumpProfilerSymbolsToFile(const std::string& fileName) const = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace jsi
|
|
||||||
} // namespace facebook
|
|
||||||
|
|
@ -1,405 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#pragma once
|
|
||||||
|
|
||||||
namespace facebook {
|
|
||||||
namespace jsi {
|
|
||||||
namespace detail {
|
|
||||||
|
|
||||||
inline Value toValue(Runtime&, std::nullptr_t) {
|
|
||||||
return Value::null();
|
|
||||||
}
|
|
||||||
inline Value toValue(Runtime&, bool b) {
|
|
||||||
return Value(b);
|
|
||||||
}
|
|
||||||
inline Value toValue(Runtime&, double d) {
|
|
||||||
return Value(d);
|
|
||||||
}
|
|
||||||
inline Value toValue(Runtime&, float f) {
|
|
||||||
return Value(static_cast<double>(f));
|
|
||||||
}
|
|
||||||
inline Value toValue(Runtime&, int i) {
|
|
||||||
return Value(i);
|
|
||||||
}
|
|
||||||
inline Value toValue(Runtime& runtime, const char* str) {
|
|
||||||
return String::createFromAscii(runtime, str);
|
|
||||||
}
|
|
||||||
inline Value toValue(Runtime& runtime, const std::string& str) {
|
|
||||||
return String::createFromUtf8(runtime, str);
|
|
||||||
}
|
|
||||||
template <typename T>
|
|
||||||
inline Value toValue(Runtime& runtime, const T& other) {
|
|
||||||
static_assert(
|
|
||||||
std::is_base_of<Pointer, T>::value,
|
|
||||||
"This type cannot be converted to Value");
|
|
||||||
return Value(runtime, other);
|
|
||||||
}
|
|
||||||
inline Value toValue(Runtime& runtime, const Value& value) {
|
|
||||||
return Value(runtime, value);
|
|
||||||
}
|
|
||||||
inline Value&& toValue(Runtime&, Value&& value) {
|
|
||||||
return std::move(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline PropNameID toPropNameID(Runtime& runtime, const char* name) {
|
|
||||||
return PropNameID::forAscii(runtime, name);
|
|
||||||
}
|
|
||||||
inline PropNameID toPropNameID(Runtime& runtime, const std::string& name) {
|
|
||||||
return PropNameID::forUtf8(runtime, name);
|
|
||||||
}
|
|
||||||
inline PropNameID&& toPropNameID(Runtime&, PropNameID&& name) {
|
|
||||||
return std::move(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper to throw while still compiling with exceptions turned off.
|
|
||||||
template <typename E, typename... Args>
|
|
||||||
[[noreturn]] inline void throwOrDie(Args&&... args) {
|
|
||||||
std::rethrow_exception(
|
|
||||||
std::make_exception_ptr(E{std::forward<Args>(args)...}));
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace detail
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
inline T Runtime::make(Runtime::PointerValue* pv) {
|
|
||||||
return T(pv);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Runtime::PointerValue* Runtime::getPointerValue(jsi::Pointer& pointer) {
|
|
||||||
return pointer.ptr_;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline const Runtime::PointerValue* Runtime::getPointerValue(
|
|
||||||
const jsi::Pointer& pointer) {
|
|
||||||
return pointer.ptr_;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline const Runtime::PointerValue* Runtime::getPointerValue(
|
|
||||||
const jsi::Value& value) {
|
|
||||||
return value.data_.pointer.ptr_;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Runtime::setRuntimeData(
|
|
||||||
const UUID& uuid,
|
|
||||||
const std::shared_ptr<void>& data) {
|
|
||||||
auto* dataPtr = new std::shared_ptr<void>(data);
|
|
||||||
setRuntimeDataImpl(uuid, dataPtr, [](const void* data) {
|
|
||||||
delete (const std::shared_ptr<void>*)data;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
inline std::shared_ptr<void> Runtime::getRuntimeData(const UUID& uuid) {
|
|
||||||
auto* data = (const std::shared_ptr<void>*)getRuntimeDataImpl(uuid);
|
|
||||||
return data ? *data : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
Value Object::getPrototype(Runtime& runtime) const {
|
|
||||||
return runtime.getPrototypeOf(*this);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value Object::getProperty(Runtime& runtime, const char* name) const {
|
|
||||||
return getProperty(runtime, String::createFromAscii(runtime, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value Object::getProperty(Runtime& runtime, const String& name) const {
|
|
||||||
return runtime.getProperty(*this, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value Object::getProperty(Runtime& runtime, const PropNameID& name)
|
|
||||||
const {
|
|
||||||
return runtime.getProperty(*this, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value Object::getProperty(Runtime& runtime, const Value& name) const {
|
|
||||||
return runtime.getProperty(*this, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool Object::hasProperty(Runtime& runtime, const char* name) const {
|
|
||||||
return hasProperty(runtime, String::createFromAscii(runtime, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool Object::hasProperty(Runtime& runtime, const String& name) const {
|
|
||||||
return runtime.hasProperty(*this, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool Object::hasProperty(Runtime& runtime, const PropNameID& name)
|
|
||||||
const {
|
|
||||||
return runtime.hasProperty(*this, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool Object::hasProperty(Runtime& runtime, const Value& name) const {
|
|
||||||
return runtime.hasProperty(*this, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
void Object::setProperty(Runtime& runtime, const char* name, T&& value) const {
|
|
||||||
setProperty(
|
|
||||||
runtime, String::createFromAscii(runtime, name), std::forward<T>(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
void Object::setProperty(Runtime& runtime, const String& name, T&& value)
|
|
||||||
const {
|
|
||||||
setPropertyValue(
|
|
||||||
runtime, name, detail::toValue(runtime, std::forward<T>(value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
void Object::setProperty(Runtime& runtime, const PropNameID& name, T&& value)
|
|
||||||
const {
|
|
||||||
setPropertyValue(
|
|
||||||
runtime, name, detail::toValue(runtime, std::forward<T>(value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
void Object::setProperty(Runtime& runtime, const Value& name, T&& value) const {
|
|
||||||
setPropertyValue(
|
|
||||||
runtime, name, detail::toValue(runtime, std::forward<T>(value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Object::deleteProperty(Runtime& runtime, const char* name) const {
|
|
||||||
deleteProperty(runtime, String::createFromAscii(runtime, name));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Object::deleteProperty(Runtime& runtime, const String& name) const {
|
|
||||||
runtime.deleteProperty(*this, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Object::deleteProperty(Runtime& runtime, const PropNameID& name)
|
|
||||||
const {
|
|
||||||
runtime.deleteProperty(*this, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Object::deleteProperty(Runtime& runtime, const Value& name) const {
|
|
||||||
runtime.deleteProperty(*this, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Array Object::getArray(Runtime& runtime) const& {
|
|
||||||
assert(runtime.isArray(*this));
|
|
||||||
(void)runtime; // when assert is disabled we need to mark this as used
|
|
||||||
return Array(runtime.cloneObject(ptr_));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Array Object::getArray(Runtime& runtime) && {
|
|
||||||
assert(runtime.isArray(*this));
|
|
||||||
(void)runtime; // when assert is disabled we need to mark this as used
|
|
||||||
Runtime::PointerValue* value = ptr_;
|
|
||||||
ptr_ = nullptr;
|
|
||||||
return Array(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline ArrayBuffer Object::getArrayBuffer(Runtime& runtime) const& {
|
|
||||||
assert(runtime.isArrayBuffer(*this));
|
|
||||||
(void)runtime; // when assert is disabled we need to mark this as used
|
|
||||||
return ArrayBuffer(runtime.cloneObject(ptr_));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline ArrayBuffer Object::getArrayBuffer(Runtime& runtime) && {
|
|
||||||
assert(runtime.isArrayBuffer(*this));
|
|
||||||
(void)runtime; // when assert is disabled we need to mark this as used
|
|
||||||
Runtime::PointerValue* value = ptr_;
|
|
||||||
ptr_ = nullptr;
|
|
||||||
return ArrayBuffer(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Function Object::getFunction(Runtime& runtime) const& {
|
|
||||||
assert(runtime.isFunction(*this));
|
|
||||||
return Function(runtime.cloneObject(ptr_));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Function Object::getFunction(Runtime& runtime) && {
|
|
||||||
assert(runtime.isFunction(*this));
|
|
||||||
(void)runtime; // when assert is disabled we need to mark this as used
|
|
||||||
Runtime::PointerValue* value = ptr_;
|
|
||||||
ptr_ = nullptr;
|
|
||||||
return Function(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
inline bool Object::isHostObject(Runtime& runtime) const {
|
|
||||||
return runtime.isHostObject(*this) &&
|
|
||||||
std::dynamic_pointer_cast<T>(runtime.getHostObject(*this));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline bool Object::isHostObject<HostObject>(Runtime& runtime) const {
|
|
||||||
return runtime.isHostObject(*this);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
inline std::shared_ptr<T> Object::getHostObject(Runtime& runtime) const {
|
|
||||||
assert(isHostObject<T>(runtime));
|
|
||||||
return std::static_pointer_cast<T>(runtime.getHostObject(*this));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
inline std::shared_ptr<T> Object::asHostObject(Runtime& runtime) const {
|
|
||||||
if (!isHostObject<T>(runtime)) {
|
|
||||||
detail::throwOrDie<JSINativeException>(
|
|
||||||
"Object is not a HostObject of desired type");
|
|
||||||
}
|
|
||||||
return std::static_pointer_cast<T>(runtime.getHostObject(*this));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline std::shared_ptr<HostObject> Object::getHostObject<HostObject>(
|
|
||||||
Runtime& runtime) const {
|
|
||||||
assert(runtime.isHostObject(*this));
|
|
||||||
return runtime.getHostObject(*this);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
inline bool Object::hasNativeState(Runtime& runtime) const {
|
|
||||||
return runtime.hasNativeState(*this) &&
|
|
||||||
std::dynamic_pointer_cast<T>(runtime.getNativeState(*this));
|
|
||||||
}
|
|
||||||
|
|
||||||
template <>
|
|
||||||
inline bool Object::hasNativeState<NativeState>(Runtime& runtime) const {
|
|
||||||
return runtime.hasNativeState(*this);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
inline std::shared_ptr<T> Object::getNativeState(Runtime& runtime) const {
|
|
||||||
assert(hasNativeState<T>(runtime));
|
|
||||||
return std::static_pointer_cast<T>(runtime.getNativeState(*this));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Object::setNativeState(
|
|
||||||
Runtime& runtime,
|
|
||||||
std::shared_ptr<NativeState> state) const {
|
|
||||||
runtime.setNativeState(*this, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline void Object::setExternalMemoryPressure(Runtime& runtime, size_t amt)
|
|
||||||
const {
|
|
||||||
runtime.setExternalMemoryPressure(*this, amt);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Array Object::getPropertyNames(Runtime& runtime) const {
|
|
||||||
return runtime.getPropertyNames(*this);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value WeakObject::lock(Runtime& runtime) const {
|
|
||||||
return runtime.lockWeakObject(*this);
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
void Array::setValueAtIndex(Runtime& runtime, size_t i, T&& value) const {
|
|
||||||
setValueAtIndexImpl(
|
|
||||||
runtime, i, detail::toValue(runtime, std::forward<T>(value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value Array::getValueAtIndex(Runtime& runtime, size_t i) const {
|
|
||||||
return runtime.getValueAtIndex(*this, i);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Function Function::createFromHostFunction(
|
|
||||||
Runtime& runtime,
|
|
||||||
const jsi::PropNameID& name,
|
|
||||||
unsigned int paramCount,
|
|
||||||
jsi::HostFunctionType func) {
|
|
||||||
return runtime.createFunctionFromHostFunction(
|
|
||||||
name, paramCount, std::move(func));
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value Function::call(Runtime& runtime, const Value* args, size_t count)
|
|
||||||
const {
|
|
||||||
return runtime.call(*this, Value::undefined(), args, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value Function::call(Runtime& runtime, std::initializer_list<Value> args)
|
|
||||||
const {
|
|
||||||
return call(runtime, args.begin(), args.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
inline Value Function::call(Runtime& runtime, Args&&... args) const {
|
|
||||||
// A more awesome version of this would be able to create raw values
|
|
||||||
// which can be used directly without wrapping and unwrapping, but
|
|
||||||
// this will do for now.
|
|
||||||
return call(runtime, {detail::toValue(runtime, std::forward<Args>(args))...});
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value Function::callWithThis(
|
|
||||||
Runtime& runtime,
|
|
||||||
const Object& jsThis,
|
|
||||||
const Value* args,
|
|
||||||
size_t count) const {
|
|
||||||
return runtime.call(*this, Value(runtime, jsThis), args, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value Function::callWithThis(
|
|
||||||
Runtime& runtime,
|
|
||||||
const Object& jsThis,
|
|
||||||
std::initializer_list<Value> args) const {
|
|
||||||
return callWithThis(runtime, jsThis, args.begin(), args.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
inline Value Function::callWithThis(
|
|
||||||
Runtime& runtime,
|
|
||||||
const Object& jsThis,
|
|
||||||
Args&&... args) const {
|
|
||||||
// A more awesome version of this would be able to create raw values
|
|
||||||
// which can be used directly without wrapping and unwrapping, but
|
|
||||||
// this will do for now.
|
|
||||||
return callWithThis(
|
|
||||||
runtime, jsThis, {detail::toValue(runtime, std::forward<Args>(args))...});
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
inline Array Array::createWithElements(Runtime& runtime, Args&&... args) {
|
|
||||||
return createWithElements(
|
|
||||||
runtime, {detail::toValue(runtime, std::forward<Args>(args))...});
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
inline std::vector<PropNameID> PropNameID::names(
|
|
||||||
Runtime& runtime,
|
|
||||||
Args&&... args) {
|
|
||||||
return names({detail::toPropNameID(runtime, std::forward<Args>(args))...});
|
|
||||||
}
|
|
||||||
|
|
||||||
template <size_t N>
|
|
||||||
inline std::vector<PropNameID> PropNameID::names(
|
|
||||||
PropNameID (&&propertyNames)[N]) {
|
|
||||||
std::vector<PropNameID> result;
|
|
||||||
result.reserve(N);
|
|
||||||
for (auto& name : propertyNames) {
|
|
||||||
result.push_back(std::move(name));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value Function::callAsConstructor(
|
|
||||||
Runtime& runtime,
|
|
||||||
const Value* args,
|
|
||||||
size_t count) const {
|
|
||||||
return runtime.callAsConstructor(*this, args, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline Value Function::callAsConstructor(
|
|
||||||
Runtime& runtime,
|
|
||||||
std::initializer_list<Value> args) const {
|
|
||||||
return callAsConstructor(runtime, args.begin(), args.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
template <typename... Args>
|
|
||||||
inline Value Function::callAsConstructor(Runtime& runtime, Args&&... args)
|
|
||||||
const {
|
|
||||||
return callAsConstructor(
|
|
||||||
runtime, {detail::toValue(runtime, std::forward<Args>(args))...});
|
|
||||||
}
|
|
||||||
|
|
||||||
String BigInt::toString(Runtime& runtime, int radix) const {
|
|
||||||
return runtime.bigintToString(*this, radix);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace jsi
|
|
||||||
} // namespace facebook
|
|
||||||
1867
Headers/jsi/jsi.h
1867
Headers/jsi/jsi.h
File diff suppressed because it is too large
Load diff
36
Makefile
36
Makefile
|
|
@ -1,31 +1,19 @@
|
||||||
TARGET := iphone:clang:latest:14.0
|
ifeq ($(THEOS_PACKAGE_SCHEME),rootless)
|
||||||
ARCHS = arm64
|
TARGET := iphone:clang:latest:15.0
|
||||||
INSTALL_TARGET_PROCESSES = Discord
|
else
|
||||||
|
TARGET := iphone:clang:latest:12.2
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
include $(THEOS)/makefiles/common.mk
|
include $(THEOS)/makefiles/common.mk
|
||||||
|
|
||||||
TWEAK_NAME = Revenge
|
TWEAK_NAME = RevengeTweak
|
||||||
BUNDLE_NAME = RevengeResources
|
|
||||||
|
|
||||||
Revenge_FILES = $(wildcard Sources/*.x Sources/*.xm Sources/*.m Sources/*.mm Sources/**/*.x Sources/**/*.xm Sources/**/*.m Sources/**/*.mm)
|
RevengeTweak_FILES = $(shell find Sources/RevengeTweak -name '*.swift') $(shell find Sources/RevengeTweakC -name '*.m' -o -name '*.c' -o -name '*.mm' -o -name '*.cpp')
|
||||||
Revenge_CFLAGS = -fobjc-arc -DPACKAGE_VERSION='@"$(THEOS_PACKAGE_BASE_VERSION)"' -I$(THEOS_PROJECT_DIR)/Headers
|
RevengeTweak_SWIFTFLAGS = -ISources/RevengeTweakC/include
|
||||||
Revenge_CCFLAGS = -std=c++17 -Wno-c++11-narrowing -I$(THEOS_PROJECT_DIR)/Headers
|
RevengeTweak_CFLAGS = -fobjc-arc -ISources/RevengeTweakC/include
|
||||||
Revenge_FRAMEWORKS = Foundation UIKit CoreGraphics CoreText CoreFoundation
|
|
||||||
|
|
||||||
RevengeResources_INSTALL_PATH = "/Library/Application\ Support/"
|
RevengeTweak_BUNDLE_NAME = RevengePatches
|
||||||
RevengeResources_RESOURCE_DIRS = Resources
|
RevengeTweak_BUNDLE_RESOURCE_DIRS = Resources
|
||||||
|
|
||||||
include $(THEOS_MAKE_PATH)/tweak.mk
|
include $(THEOS_MAKE_PATH)/tweak.mk
|
||||||
include $(THEOS_MAKE_PATH)/bundle.mk
|
|
||||||
|
|
||||||
before-all::
|
|
||||||
$(ECHO_NOTHING)mkdir -p Resources$(ECHO_END)
|
|
||||||
$(ECHO_NOTHING)sed -e 's/@PACKAGE_VERSION@/$(THEOS_PACKAGE_BASE_VERSION)/g' \
|
|
||||||
-e 's/@TWEAK_NAME@/$(TWEAK_NAME)/g' \
|
|
||||||
Sources/payload-base.template.js > Resources/payload-base.js$(ECHO_END)
|
|
||||||
|
|
||||||
after-stage::
|
|
||||||
$(ECHO_NOTHING)find $(THEOS_STAGING_DIR) -name ".DS_Store" -delete$(ECHO_END)
|
|
||||||
|
|
||||||
after-package::
|
|
||||||
$(ECHO_NOTHING)rm -rf Resources$(ECHO_END)
|
|
||||||
|
|
|
||||||
88
Package.swift
Normal file
88
Package.swift
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
// swift-tools-version:5.2
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
let projectDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
|
||||||
|
|
||||||
|
@dynamicMemberLookup struct TheosConfiguration {
|
||||||
|
private let dict: [String: String]
|
||||||
|
init(at path: String) {
|
||||||
|
let configURL = URL(fileURLWithPath: path, relativeTo: projectDir)
|
||||||
|
guard let infoString = try? String(contentsOf: configURL) else {
|
||||||
|
fatalError("""
|
||||||
|
Could not find Theos SPM config. Have you run `make spm` yet?
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
let pairs = infoString.split(separator: "\n").map {
|
||||||
|
$0.split(
|
||||||
|
separator: "=", maxSplits: 1,
|
||||||
|
omittingEmptySubsequences: false
|
||||||
|
).map(String.init)
|
||||||
|
}.map { ($0[0], $0[1]) }
|
||||||
|
dict = Dictionary(uniqueKeysWithValues: pairs)
|
||||||
|
}
|
||||||
|
subscript(
|
||||||
|
key: String,
|
||||||
|
or defaultValue: @autoclosure () -> String? = nil
|
||||||
|
) -> String {
|
||||||
|
if let value = dict[key] {
|
||||||
|
return value
|
||||||
|
} else if let def = defaultValue() {
|
||||||
|
return def
|
||||||
|
} else {
|
||||||
|
fatalError("""
|
||||||
|
Could not get value of key '\(key)' from Theos SPM config. \
|
||||||
|
Try running `make spm` again.
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subscript(dynamicMember key: String) -> String { self[key] }
|
||||||
|
}
|
||||||
|
let conf = TheosConfiguration(at: ".theos/spm_config")
|
||||||
|
|
||||||
|
let theosPath = conf.theos
|
||||||
|
let sdk = conf.sdk
|
||||||
|
let resourceDir = conf.swiftResourceDir
|
||||||
|
let deploymentTarget = conf.deploymentTarget
|
||||||
|
let triple = "arm64-apple-ios\(deploymentTarget)"
|
||||||
|
|
||||||
|
let libFlags: [String] = [
|
||||||
|
"-F\(theosPath)/vendor/lib", "-F\(theosPath)/lib",
|
||||||
|
"-I\(theosPath)/vendor/include", "-I\(theosPath)/include"
|
||||||
|
]
|
||||||
|
|
||||||
|
let cFlags: [String] = libFlags + [
|
||||||
|
"-target", triple, "-isysroot", sdk,
|
||||||
|
"-Wno-unused-command-line-argument", "-Qunused-arguments",
|
||||||
|
]
|
||||||
|
|
||||||
|
let cxxFlags: [String] = [
|
||||||
|
]
|
||||||
|
|
||||||
|
let swiftFlags: [String] = libFlags + [
|
||||||
|
"-target", triple, "-sdk", sdk, "-resource-dir", resourceDir,
|
||||||
|
]
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "RevengeTweak",
|
||||||
|
platforms: [.iOS(deploymentTarget)],
|
||||||
|
products: [
|
||||||
|
.library(
|
||||||
|
name: "RevengeTweak",
|
||||||
|
targets: ["RevengeTweak"]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "RevengeTweakC",
|
||||||
|
cSettings: [.unsafeFlags(cFlags)],
|
||||||
|
cxxSettings: [.unsafeFlags(cxxFlags)]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "RevengeTweak",
|
||||||
|
dependencies: ["RevengeTweakC"],
|
||||||
|
swiftSettings: [.unsafeFlags(swiftFlags)]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
7
Resources/payload-base.js
Normal file
7
Resources/payload-base.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
globalThis.__PYON_LOADER__ = {
|
||||||
|
loaderName: "RevengeTweak",
|
||||||
|
loaderVersion: "1.0.0",
|
||||||
|
hasThemeSupport: true,
|
||||||
|
storedTheme: null,
|
||||||
|
fontPatch: 2,
|
||||||
|
};
|
||||||
150
Sources/Fonts.x
150
Sources/Fonts.x
|
|
@ -1,150 +0,0 @@
|
||||||
#import "Fonts.h"
|
|
||||||
#import "Utils.h"
|
|
||||||
#import "Logger.h"
|
|
||||||
#import <CoreText/CoreText.h>
|
|
||||||
#import <CoreGraphics/CoreGraphics.h>
|
|
||||||
#import <CoreFoundation/CoreFoundation.h>
|
|
||||||
|
|
||||||
NSMutableDictionary<NSString *, NSString *> *fontMap;
|
|
||||||
|
|
||||||
%hook UIFont
|
|
||||||
|
|
||||||
+ (UIFont *)fontWithName:(NSString *)name size:(CGFloat)size {
|
|
||||||
NSString *replacementName = fontMap[name];
|
|
||||||
if (replacementName) {
|
|
||||||
UIFontDescriptor *replacementDescriptor = [UIFontDescriptor fontDescriptorWithName:replacementName size:size];
|
|
||||||
UIFontDescriptor *fallbackDescriptor = [replacementDescriptor fontDescriptorByAddingAttributes:@{UIFontDescriptorNameAttribute: @[name]}];
|
|
||||||
UIFontDescriptor *finalDescriptor = [replacementDescriptor fontDescriptorByAddingAttributes:@{UIFontDescriptorCascadeListAttribute: @[fallbackDescriptor]}];
|
|
||||||
|
|
||||||
return [UIFont fontWithDescriptor:finalDescriptor size:size];
|
|
||||||
}
|
|
||||||
return %orig;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (UIFont *)fontWithDescriptor:(UIFontDescriptor *)descriptor size:(CGFloat)size {
|
|
||||||
NSString *replacementName = fontMap[descriptor.postscriptName];
|
|
||||||
if (replacementName) {
|
|
||||||
UIFontDescriptor *replacementDescriptor = [UIFontDescriptor fontDescriptorWithName:replacementName size:size];
|
|
||||||
UIFontDescriptor *finalDescriptor = [replacementDescriptor fontDescriptorByAddingAttributes:@{UIFontDescriptorCascadeListAttribute: @[descriptor]}];
|
|
||||||
|
|
||||||
return [UIFont fontWithDescriptor:finalDescriptor size:size];
|
|
||||||
}
|
|
||||||
return %orig;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (UIFont *)systemFontOfSize:(CGFloat)size {
|
|
||||||
NSString *replacementName = fontMap[@"systemFont"];
|
|
||||||
if (replacementName) {
|
|
||||||
return [UIFont fontWithName:replacementName size:size];
|
|
||||||
}
|
|
||||||
return %orig;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (UIFont *)preferredFontForTextStyle:(UIFontTextStyle)style {
|
|
||||||
NSString *replacementName = fontMap[@"systemFont"];
|
|
||||||
if (replacementName) {
|
|
||||||
return [UIFont fontWithName:replacementName size:[UIFont systemFontSize]];
|
|
||||||
}
|
|
||||||
return %orig;
|
|
||||||
}
|
|
||||||
|
|
||||||
%end
|
|
||||||
|
|
||||||
void patchFonts(NSDictionary<NSString *, NSString *> *mainFonts, NSString *fontDefName) {
|
|
||||||
RevengeLog(@"patchFonts called with fonts: %@ and def name: %@", mainFonts, fontDefName);
|
|
||||||
|
|
||||||
if (!fontMap) {
|
|
||||||
RevengeLog(@"Creating new fontMap");
|
|
||||||
fontMap = [NSMutableDictionary dictionary];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *fontJson = [NSString stringWithContentsOfURL:[getPyoncordDirectory() URLByAppendingPathComponent:@"fonts.json"]
|
|
||||||
encoding:NSUTF8StringEncoding
|
|
||||||
error:nil];
|
|
||||||
if (fontJson) {
|
|
||||||
RevengeLog(@"Found existing fonts.json: %@", fontJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (NSString *fontName in mainFonts) {
|
|
||||||
NSString *url = mainFonts[fontName];
|
|
||||||
RevengeLog(@"Replacing font %@ with URL: %@", fontName, url);
|
|
||||||
|
|
||||||
NSURL *fontURL = [NSURL URLWithString:url];
|
|
||||||
NSString *fontExtension = fontURL.pathExtension;
|
|
||||||
|
|
||||||
NSURL *fontCachePath = [[[getPyoncordDirectory()
|
|
||||||
URLByAppendingPathComponent:@"downloads" isDirectory:YES]
|
|
||||||
URLByAppendingPathComponent:@"fonts" isDirectory:YES]
|
|
||||||
URLByAppendingPathComponent:fontDefName isDirectory:YES];
|
|
||||||
|
|
||||||
fontCachePath = [fontCachePath URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.%@", fontName, fontExtension]];
|
|
||||||
|
|
||||||
NSURL *parentDir = [fontCachePath URLByDeletingLastPathComponent];
|
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:parentDir.path]) {
|
|
||||||
RevengeLog(@"Creating parent directory: %@", parentDir.path);
|
|
||||||
[[NSFileManager defaultManager] createDirectoryAtURL:parentDir
|
|
||||||
withIntermediateDirectories:YES
|
|
||||||
attributes:nil
|
|
||||||
error:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (![[NSFileManager defaultManager] fileExistsAtPath:fontCachePath.path]) {
|
|
||||||
RevengeLog(@"Downloading font %@ from %@", fontName, url);
|
|
||||||
NSData *data = [NSData dataWithContentsOfURL:fontURL];
|
|
||||||
if (data) {
|
|
||||||
RevengeLog(@"Writing font data to: %@", fontCachePath.path);
|
|
||||||
[data writeToURL:fontCachePath atomically:YES];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *fontData = [NSData dataWithContentsOfURL:fontCachePath];
|
|
||||||
if (fontData) {
|
|
||||||
RevengeLog(@"Registering font %@ with provider", fontName);
|
|
||||||
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)fontData);
|
|
||||||
CGFontRef font = CGFontCreateWithDataProvider(provider);
|
|
||||||
|
|
||||||
if (font) {
|
|
||||||
CFStringRef postScriptName = CGFontCopyPostScriptName(font);
|
|
||||||
|
|
||||||
CTFontRef existingFont = CTFontCreateWithName(postScriptName, 0, NULL);
|
|
||||||
if (existingFont) {
|
|
||||||
CFErrorRef unregisterError = NULL;
|
|
||||||
if (!CTFontManagerUnregisterGraphicsFont(font, &unregisterError)) {
|
|
||||||
RevengeLog(@"Failed to deregister font %@: %@", (__bridge NSString *)postScriptName,
|
|
||||||
unregisterError ? (__bridge NSString *)CFErrorCopyDescription(unregisterError) : @"Unknown error");
|
|
||||||
if (unregisterError) CFRelease(unregisterError);
|
|
||||||
}
|
|
||||||
CFRelease(existingFont);
|
|
||||||
}
|
|
||||||
|
|
||||||
CFErrorRef error = NULL;
|
|
||||||
if (CTFontManagerRegisterGraphicsFont(font, &error)) {
|
|
||||||
fontMap[fontName] = (__bridge NSString *)postScriptName;
|
|
||||||
RevengeLog(@"Successfully registered font %@ to %@", fontName, (__bridge NSString *)postScriptName);
|
|
||||||
|
|
||||||
NSError *jsonError;
|
|
||||||
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:fontMap options:0 error:&jsonError];
|
|
||||||
if (!jsonError) {
|
|
||||||
[jsonData writeToURL:[getPyoncordDirectory() URLByAppendingPathComponent:@"fontMap.json"] atomically:YES];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NSString *errorDesc = error ? (__bridge NSString *)CFErrorCopyDescription(error) : @"Unknown error";
|
|
||||||
RevengeLog(@"Failed to register font %@: %@", fontName, errorDesc);
|
|
||||||
if (error) CFRelease(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
CFRelease(postScriptName);
|
|
||||||
CFRelease(font);
|
|
||||||
}
|
|
||||||
CGDataProviderRelease(provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
%ctor {
|
|
||||||
@autoreleasepool {
|
|
||||||
fontMap = [NSMutableDictionary dictionary];
|
|
||||||
RevengeLog(@"Font hooks initialized");
|
|
||||||
%init;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
#import "JSI.h"
|
|
||||||
#import "Logger.h"
|
|
||||||
#import "Utils.h"
|
|
||||||
|
|
||||||
using namespace facebook;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
class NSDataBuffer : public jsi::Buffer {
|
|
||||||
public:
|
|
||||||
explicit NSDataBuffer(NSData *data) : data_(data) {}
|
|
||||||
|
|
||||||
size_t size() const override {
|
|
||||||
return data_.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uint8_t *data() const override {
|
|
||||||
return static_cast<const uint8_t *>(data_.bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
NSData *data_;
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@implementation JSI
|
|
||||||
|
|
||||||
+ (void)evaluate:(NSData *)scriptData tag:(NSString *)tag runtime:(jsi::Runtime &)runtime {
|
|
||||||
if (scriptData.length == 0) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isHermesBytecode(scriptData)) {
|
|
||||||
auto buffer = std::make_shared<NSDataBuffer>(scriptData);
|
|
||||||
auto prepared = runtime.prepareJavaScript(buffer, std::string(tag.UTF8String));
|
|
||||||
runtime.evaluatePreparedJavaScript(prepared);
|
|
||||||
} else {
|
|
||||||
std::string source((const char *)scriptData.bytes, scriptData.length);
|
|
||||||
auto buffer = std::make_shared<jsi::StringBuffer>(std::move(source));
|
|
||||||
runtime.evaluateJavaScript(buffer, std::string(tag.UTF8String));
|
|
||||||
}
|
|
||||||
} catch (const jsi::JSError &e) {
|
|
||||||
RevengeLog(@"[JSI] evaluate error: JSError for '%@': %s", tag, e.what());
|
|
||||||
} catch (const std::exception &e) {
|
|
||||||
RevengeLog(@"[JSI] evaluate error: exception for '%@': %s", tag, e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
#import "LoaderConfig.h"
|
|
||||||
#import "Utils.h"
|
|
||||||
#import "Logger.h"
|
|
||||||
|
|
||||||
@implementation LoaderConfig
|
|
||||||
|
|
||||||
- (instancetype)init {
|
|
||||||
self = [super init];
|
|
||||||
if (self) {
|
|
||||||
self.customLoadUrlEnabled = NO;
|
|
||||||
self.customLoadUrl = [NSURL URLWithString:@"http://localhost:4040/revenge.js"];
|
|
||||||
}
|
|
||||||
return self;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)loadConfig {
|
|
||||||
NSURL *loaderConfigUrl = [getPyoncordDirectory() URLByAppendingPathComponent:@"loader.json"];
|
|
||||||
RevengeLog(@"Attempting to load config from: %@", loaderConfigUrl.path);
|
|
||||||
|
|
||||||
if ([[NSFileManager defaultManager] fileExistsAtPath:loaderConfigUrl.path]) {
|
|
||||||
NSError *error = nil;
|
|
||||||
NSData *data = [NSData dataWithContentsOfURL:loaderConfigUrl];
|
|
||||||
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
RevengeLog(@"Error parsing loader config: %@", error);
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (json) {
|
|
||||||
NSDictionary *customLoadUrl = json[@"customLoadUrl"];
|
|
||||||
if (customLoadUrl) {
|
|
||||||
self.customLoadUrlEnabled = [customLoadUrl[@"enabled"] boolValue];
|
|
||||||
NSString *urlString = customLoadUrl[@"url"];
|
|
||||||
if (urlString) {
|
|
||||||
self.customLoadUrl = [NSURL URLWithString:urlString];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RevengeLog(@"Loader config loaded - Custom URL %@: %@",
|
|
||||||
self.customLoadUrlEnabled ? @"enabled" : @"disabled",
|
|
||||||
self.customLoadUrl.absoluteString);
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RevengeLog(@"Using default loader config: %@", self.customLoadUrl.absoluteString);
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (instancetype)defaultConfig {
|
|
||||||
LoaderConfig *config = [[LoaderConfig alloc] init];
|
|
||||||
config.customLoadUrlEnabled = NO;
|
|
||||||
config.customLoadUrl = [NSURL URLWithString:@"http://localhost:4040/revenge.js"];
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
+ (instancetype)getLoaderConfig {
|
|
||||||
RevengeLog(@"Getting loader config");
|
|
||||||
|
|
||||||
NSURL *loaderConfigUrl = [getPyoncordDirectory() URLByAppendingPathComponent:@"loader.json"];
|
|
||||||
|
|
||||||
if ([[NSFileManager defaultManager] fileExistsAtPath:loaderConfigUrl.path]) {
|
|
||||||
NSError *error = nil;
|
|
||||||
NSData *data = [NSData dataWithContentsOfURL:loaderConfigUrl];
|
|
||||||
NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
|
||||||
|
|
||||||
if (json && !error) {
|
|
||||||
LoaderConfig *config = [[LoaderConfig alloc] init];
|
|
||||||
NSDictionary *customLoadUrl = json[@"customLoadUrl"];
|
|
||||||
if (customLoadUrl) {
|
|
||||||
config.customLoadUrlEnabled = [customLoadUrl[@"enabled"] boolValue];
|
|
||||||
NSString *urlString = customLoadUrl[@"url"];
|
|
||||||
if (urlString) {
|
|
||||||
config.customLoadUrl = [NSURL URLWithString:urlString];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RevengeLog(@"Couldn't get loader config");
|
|
||||||
return [LoaderConfig defaultConfig];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (BOOL)saveConfig {
|
|
||||||
NSURL *loaderConfigUrl = [getPyoncordDirectory() URLByAppendingPathComponent:@"loader.json"];
|
|
||||||
NSDictionary *json = @{
|
|
||||||
@"customLoadUrl": @{
|
|
||||||
@"enabled": @(self.customLoadUrlEnabled),
|
|
||||||
@"url": self.customLoadUrl.absoluteString
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
NSData *data = [NSJSONSerialization dataWithJSONObject:json options:0 error:nil];
|
|
||||||
return [data writeToURL:loaderConfigUrl atomically:YES];
|
|
||||||
}
|
|
||||||
|
|
||||||
@end
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
#import "Logger.h"
|
|
||||||
99
Sources/RevengeTweak/Fonts.x.swift
Normal file
99
Sources/RevengeTweak/Fonts.x.swift
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import RevengeTweakC
|
||||||
|
import Orion
|
||||||
|
import UIKit
|
||||||
|
import os
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// This is not the full implementation, we only take what we need in here
|
||||||
|
// For full definition, find it from the core codebase (JS side)
|
||||||
|
struct FontDefinition: Codable {
|
||||||
|
let name: String
|
||||||
|
let main: [String: String]?
|
||||||
|
}
|
||||||
|
|
||||||
|
var fontMap: [String: String] = [:]
|
||||||
|
|
||||||
|
class FontsHook: ClassHook<UIFont> {
|
||||||
|
class func fontWithName(_ name: String, size: CGFloat) -> UIFont {
|
||||||
|
if let replacementName = fontMap[name] {
|
||||||
|
let replacementDescriptor = UIFontDescriptor(name: replacementName, size: size)
|
||||||
|
let fallbackDescriptor = replacementDescriptor.addingAttributes([.name: [name]])
|
||||||
|
let finalDescriptor = replacementDescriptor.addingAttributes([.cascadeList: [fallbackDescriptor]])
|
||||||
|
|
||||||
|
return orig.fontWithDescriptor(finalDescriptor, size: size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return orig.fontWithName(name, size: size)
|
||||||
|
}
|
||||||
|
class func fontWithDescriptor(_ descriptor: UIFontDescriptor, size: CGFloat) -> UIFont {
|
||||||
|
if let replacementName = fontMap[descriptor.postscriptName] {
|
||||||
|
let replacementDescriptor = UIFontDescriptor(name: replacementName, size: size)
|
||||||
|
let finalDescriptor = replacementDescriptor.addingAttributes([.cascadeList: [descriptor]])
|
||||||
|
|
||||||
|
return orig.fontWithDescriptor(finalDescriptor, size: size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return orig.fontWithDescriptor(descriptor, size: size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func patchFonts(_ main: [String: String], fontDefName: String) {
|
||||||
|
for (fontName, url) in main {
|
||||||
|
os_log("Replacing font %{public}@ with URL: %{public}@", log: revengeLog, type: .info, fontName, url)
|
||||||
|
|
||||||
|
let fontExtension = URL(string: url)!.pathExtension
|
||||||
|
let fontCachePath = pyoncordDirectory
|
||||||
|
.appendingPathComponent("downloads", isDirectory: true)
|
||||||
|
.appendingPathComponent("fonts", isDirectory: true)
|
||||||
|
.appendingPathComponent(fontDefName, isDirectory: true)
|
||||||
|
.appendingPathComponent("\(fontName).\(fontExtension)")
|
||||||
|
|
||||||
|
do {
|
||||||
|
os_log("Attempting to register font %{public}@ from %{public}@", log: revengeLog, type: .info, fontName, url)
|
||||||
|
|
||||||
|
let parent = fontCachePath.deletingLastPathComponent()
|
||||||
|
if !FileManager.default.fileExists(atPath: parent.path) {
|
||||||
|
os_log("Creating parent directory: %{public}@", log: revengeLog, type: .debug, parent.path)
|
||||||
|
try FileManager.default.createDirectory(
|
||||||
|
at: parent, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JS side should download these already, but just in case...
|
||||||
|
if !FileManager.default.fileExists(atPath: fontCachePath.path) {
|
||||||
|
os_log("Downloading font %{public}@ from %{public}@", log: revengeLog, type: .debug, fontName, url)
|
||||||
|
if let data = try? Data(contentsOf: URL(string: url)!) {
|
||||||
|
os_log("Writing font data to: %{public}@", log: revengeLog, type: .debug, fontCachePath.path)
|
||||||
|
try? data.write(to: fontCachePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let data = try? Data(contentsOf: fontCachePath) {
|
||||||
|
os_log("Registering font %{public}@ with provider", log: revengeLog, type: .debug, fontName)
|
||||||
|
let provider = CGDataProvider(data: data as CFData)
|
||||||
|
let font = CGFont(provider!)
|
||||||
|
var error: Unmanaged<CFError>?
|
||||||
|
|
||||||
|
// This does not work with system/app fonts unfortunately. Throws a CTFontManagerError.systemRequired
|
||||||
|
if let existingFont = CGFont(font!.postScriptName!) {
|
||||||
|
var unregisterError: Unmanaged<CFError>?
|
||||||
|
if !CTFontManagerUnregisterGraphicsFont(existingFont, &unregisterError) {
|
||||||
|
os_log("Failed to deregister font %{public}@: %{public}@", log: revengeLog, type: .error,
|
||||||
|
font!.postScriptName! as String,
|
||||||
|
String(describing: unregisterError!.takeUnretainedValue()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if CTFontManagerRegisterGraphicsFont(font!, &error) {
|
||||||
|
fontMap[fontName] = font!.postScriptName! as String
|
||||||
|
os_log("Successfully registered font %{public}@ to %{public}@", log: revengeLog, type: .info, fontName, font!.postScriptName! as String)
|
||||||
|
} else {
|
||||||
|
os_log("Failed to register font %{public}@: %{public}@", log: revengeLog, type: .error, fontName, String(describing: error!.takeUnretainedValue()))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
os_log("Failed to read font data from: %{public}@", log: revengeLog, type: .error, fontCachePath.path)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
os_log("Failed to register font %{public}@: %{public}@", log: revengeLog, type: .error, fontName, error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
Sources/RevengeTweak/LoaderConfig.x.swift
Normal file
47
Sources/RevengeTweak/LoaderConfig.x.swift
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import Foundation
|
||||||
|
import os
|
||||||
|
|
||||||
|
enum LoaderConfigError: Error {
|
||||||
|
case doesNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CustomLoadUrl: Codable {
|
||||||
|
let enabled: Bool
|
||||||
|
let url: URL
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LoaderConfig: Codable {
|
||||||
|
let customLoadUrl: CustomLoadUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
let defaultLoaderConfig = LoaderConfig(
|
||||||
|
customLoadUrl: CustomLoadUrl(
|
||||||
|
enabled: false,
|
||||||
|
url: URL(string: "http://localhost:4040/revenge.js")!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let pyoncordDirectory = getPyoncordDirectory()
|
||||||
|
let loaderConfigUrl = pyoncordDirectory.appendingPathComponent("loader.json")
|
||||||
|
|
||||||
|
func getLoaderConfig() -> LoaderConfig {
|
||||||
|
os_log("Getting loader config", log: revengeLog, type: .debug)
|
||||||
|
let fileManager = FileManager.default
|
||||||
|
|
||||||
|
do {
|
||||||
|
if fileManager.fileExists(atPath: loaderConfigUrl.path) {
|
||||||
|
let data = try Data(contentsOf: loaderConfigUrl)
|
||||||
|
let loaderConfig = try JSONDecoder().decode(LoaderConfig.self, from: data)
|
||||||
|
|
||||||
|
os_log("Got loader config", log: revengeLog, type: .debug)
|
||||||
|
|
||||||
|
return loaderConfig
|
||||||
|
} else {
|
||||||
|
throw LoaderConfigError.doesNotExist
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
os_log("Couldn't get loader config", log: revengeLog, type: .error)
|
||||||
|
|
||||||
|
return defaultLoaderConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
152
Sources/RevengeTweak/Themes.x.swift
Normal file
152
Sources/RevengeTweak/Themes.x.swift
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
import Orion
|
||||||
|
import UIKit
|
||||||
|
import RevengeTweakC
|
||||||
|
import os
|
||||||
|
|
||||||
|
struct Author: Codable {
|
||||||
|
let name: String
|
||||||
|
let id: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ThemeData: Codable {
|
||||||
|
let name: String
|
||||||
|
let description: String?
|
||||||
|
let authors: [Author]?
|
||||||
|
let spec: Int
|
||||||
|
let semanticColors: [String: [String]]?
|
||||||
|
let rawColors: [String: String]?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Theme: Codable {
|
||||||
|
let id: String
|
||||||
|
let selected: Bool
|
||||||
|
let data: ThemeData
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexToUIColor(_ hex: String) -> UIColor? {
|
||||||
|
let r: CGFloat
|
||||||
|
let g: CGFloat
|
||||||
|
let b: CGFloat
|
||||||
|
let a: CGFloat
|
||||||
|
|
||||||
|
if hex.hasPrefix("#") {
|
||||||
|
let start = hex.index(hex.startIndex, offsetBy: 1)
|
||||||
|
var hexColor = String(hex[start...])
|
||||||
|
|
||||||
|
if hexColor.count == 6 {
|
||||||
|
hexColor.append("ff")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hexColor.count == 8 {
|
||||||
|
let scanner = Scanner(string: hexColor)
|
||||||
|
var hexNumber: UInt64 = 0
|
||||||
|
|
||||||
|
if scanner.scanHexInt64(&hexNumber) {
|
||||||
|
r = CGFloat((hexNumber & 0xff00_0000) >> 24) / 255
|
||||||
|
g = CGFloat((hexNumber & 0x00ff_0000) >> 16) / 255
|
||||||
|
b = CGFloat((hexNumber & 0x0000_ff00) >> 8) / 255
|
||||||
|
a = CGFloat(hexNumber & 0x0000_00ff) / 255
|
||||||
|
|
||||||
|
return UIColor(red: r, green: g, blue: b, alpha: a)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func swizzleDCDThemeColor(_ semanticColors: [String: [String]]) {
|
||||||
|
os_log("Swizzling DCDThemeColor", log: revengeLog, type: .info)
|
||||||
|
|
||||||
|
let DCDTheme: AnyClass = NSClassFromString("DCDTheme")!
|
||||||
|
let dcdThemeTarget: AnyClass = object_getClass(DCDTheme)!
|
||||||
|
|
||||||
|
let themeIndexSelector = NSSelectorFromString("themeIndex")
|
||||||
|
let themeIndexMethod = class_getClassMethod(dcdThemeTarget, themeIndexSelector)!
|
||||||
|
let themeIndexImpl = method_getImplementation(themeIndexMethod)
|
||||||
|
typealias ThemeIndexType = @convention(c) (AnyObject, Selector) -> Int
|
||||||
|
let themeIndex: ThemeIndexType = unsafeBitCast(themeIndexImpl, to: ThemeIndexType.self)
|
||||||
|
|
||||||
|
let DCDThemeColor: AnyClass = NSClassFromString("DCDThemeColor")!
|
||||||
|
os_log("Found instance of DCDThemeColor", log: revengeLog, type: .debug)
|
||||||
|
let target: AnyClass = object_getClass(DCDThemeColor)!
|
||||||
|
|
||||||
|
os_log("Found DCDThemeColor", log: revengeLog, type: .debug)
|
||||||
|
|
||||||
|
var methodCount: UInt32 = 0
|
||||||
|
let methods = class_copyMethodList(target, &methodCount)!
|
||||||
|
|
||||||
|
os_log("DCDThemeColor has %{public}d methods", log: revengeLog, type: .debug, methodCount)
|
||||||
|
|
||||||
|
for i in 0..<Int(methodCount) {
|
||||||
|
let method = methods[i]
|
||||||
|
let selector = method_getName(method)
|
||||||
|
let methodName = NSStringFromSelector(selector)
|
||||||
|
os_log(
|
||||||
|
"Found method %{public}@", log: revengeLog, type: .debug, methodName)
|
||||||
|
if let semanticColor = semanticColors[methodName] {
|
||||||
|
os_log(
|
||||||
|
"Swizzling %{public}@", log: revengeLog, type: .debug, methodName)
|
||||||
|
|
||||||
|
let originalImpl = method_getImplementation(method)
|
||||||
|
typealias OriginalType = @convention(c) (AnyObject, Selector) -> UIColor
|
||||||
|
let original = unsafeBitCast(originalImpl, to: OriginalType.self)
|
||||||
|
let semanticColorBlock: @convention(block) (AnyObject) -> UIColor = {
|
||||||
|
(self: AnyObject) -> UIColor in
|
||||||
|
let themeIndexVal = themeIndex(dcdThemeTarget, themeIndexSelector)
|
||||||
|
if semanticColor.count - 1 >= themeIndexVal {
|
||||||
|
if let semanticUIColor = hexToUIColor(
|
||||||
|
semanticColor[themeIndexVal])
|
||||||
|
{
|
||||||
|
return semanticUIColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return original(target, selector)
|
||||||
|
}
|
||||||
|
let semanticColorImplementation = imp_implementationWithBlock(
|
||||||
|
unsafeBitCast(semanticColorBlock, to: AnyObject.self))
|
||||||
|
method_setImplementation(
|
||||||
|
method, semanticColorImplementation)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(methods)
|
||||||
|
}
|
||||||
|
|
||||||
|
func swizzleUIColor(_ rawColors: [String: String]) {
|
||||||
|
os_log("Swizzling UIColor", log: revengeLog, type: .info)
|
||||||
|
|
||||||
|
let UIColor: AnyClass = NSClassFromString("UIColor")!
|
||||||
|
os_log("Found instance of UIColor", log: revengeLog, type: .debug)
|
||||||
|
let target: AnyClass = object_getClass(UIColor)!
|
||||||
|
|
||||||
|
os_log("Found UIColor", log: revengeLog, type: .debug)
|
||||||
|
|
||||||
|
var methodCount: UInt32 = 0
|
||||||
|
let methods = class_copyMethodList(target, &methodCount)!
|
||||||
|
|
||||||
|
os_log("UIColor has %{public}d methods", log: revengeLog, type: .debug, methodCount)
|
||||||
|
|
||||||
|
for i in 0..<Int(methodCount) {
|
||||||
|
let method = methods[i]
|
||||||
|
let selector = method_getName(method)
|
||||||
|
let methodName = NSStringFromSelector(selector)
|
||||||
|
os_log(
|
||||||
|
"Found method %{public}@", log: revengeLog, type: .debug, methodName)
|
||||||
|
if let rawColor = rawColors[methodName] {
|
||||||
|
os_log(
|
||||||
|
"Swizzling %{public}@", log: revengeLog, type: .debug, methodName)
|
||||||
|
let rawColorBlock: @convention(block) (AnyObject) -> UIColor = {
|
||||||
|
(self: AnyObject) -> UIColor in
|
||||||
|
return hexToUIColor(rawColor)!
|
||||||
|
}
|
||||||
|
let rawColorImplementation = imp_implementationWithBlock(
|
||||||
|
unsafeBitCast(rawColorBlock, to: AnyObject.self))
|
||||||
|
method_setImplementation(
|
||||||
|
method, rawColorImplementation)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
free(methods)
|
||||||
|
}
|
||||||
143
Sources/RevengeTweak/Tweak.x.swift
Normal file
143
Sources/RevengeTweak/Tweak.x.swift
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
import Orion
|
||||||
|
import RevengeTweakC
|
||||||
|
import os
|
||||||
|
|
||||||
|
let revengeLog = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "revenge")
|
||||||
|
let source = URL(string: "revenge")!
|
||||||
|
|
||||||
|
let install_prefix = String(cString: get_install_prefix())
|
||||||
|
let isJailbroken = FileManager.default.fileExists(atPath: "\(install_prefix)/Library/Application Support/RevengeTweak/RevengePatches.bundle")
|
||||||
|
|
||||||
|
let revengePatchesBundlePath = isJailbroken ? "\(install_prefix)/Library/Application Support/RevengeTweak/RevengePatches.bundle" : "\(Bundle.main.bundleURL.path)/RevengePatches.bundle"
|
||||||
|
|
||||||
|
class FileManagerLoadHook: ClassHook<FileManager> {
|
||||||
|
func containerURLForSecurityApplicationGroupIdentifier(_ groupIdentifier: NSString?) -> URL? {
|
||||||
|
os_log("containerURLForSecurityApplicationGroupIdentifier called! %{public}@ groupIdentifier", log: revengeLog, type: .debug, groupIdentifier ?? "nil")
|
||||||
|
|
||||||
|
if (isJailbroken) {
|
||||||
|
return orig.containerURLForSecurityApplicationGroupIdentifier(groupIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
||||||
|
let lastPath = paths.last!
|
||||||
|
return lastPath.appendingPathComponent("AppGroup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadHook: ClassHook<RCTCxxBridge> {
|
||||||
|
func executeApplicationScript(_ script: Data, url: URL, async: Bool) {
|
||||||
|
os_log("executeApplicationScript called!", log: revengeLog, type: .debug)
|
||||||
|
|
||||||
|
let loaderConfig = getLoaderConfig()
|
||||||
|
|
||||||
|
let revengePatchesBundle = Bundle(path: revengePatchesBundlePath)!
|
||||||
|
|
||||||
|
if let patchPath = revengePatchesBundle.url(forResource: "payload-base", withExtension: "js") {
|
||||||
|
let patchData = try! Data(contentsOf: patchPath)
|
||||||
|
os_log("Executing payload base", log: revengeLog, type: .debug)
|
||||||
|
orig.executeApplicationScript(patchData, url: source, async: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let pyoncordDirectory = getPyoncordDirectory()
|
||||||
|
|
||||||
|
var bundle = try? Data(contentsOf: pyoncordDirectory.appendingPathComponent("bundle.js"))
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
|
||||||
|
group.enter()
|
||||||
|
var bundleUrl: URL
|
||||||
|
if loaderConfig.customLoadUrl.enabled {
|
||||||
|
os_log(
|
||||||
|
"Custom load URL enabled, with URL %{public}@ ", log: revengeLog, type: .info,
|
||||||
|
loaderConfig.customLoadUrl.url.absoluteString)
|
||||||
|
bundleUrl = loaderConfig.customLoadUrl.url
|
||||||
|
} else {
|
||||||
|
bundleUrl = URL(
|
||||||
|
string: "https://github.com/revenge-mod/revenge-bundle/releases/latest/download/revenge.min.js")!
|
||||||
|
}
|
||||||
|
|
||||||
|
os_log("Fetching JS bundle", log: revengeLog, type: .info)
|
||||||
|
var bundleRequest = URLRequest(
|
||||||
|
url: bundleUrl, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 3.0)
|
||||||
|
|
||||||
|
if let bundleEtag = try? String(
|
||||||
|
contentsOf: pyoncordDirectory.appendingPathComponent("etag.txt")), bundle != nil
|
||||||
|
{
|
||||||
|
bundleRequest.addValue(bundleEtag, forHTTPHeaderField: "If-None-Match")
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchTask = URLSession.shared.dataTask(with: bundleRequest) { data, response, error in
|
||||||
|
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
|
||||||
|
os_log("Successfully fetched JS Bundle", log: revengeLog, type: .debug)
|
||||||
|
bundle = data
|
||||||
|
try? bundle?.write(to: pyoncordDirectory.appendingPathComponent("bundle.js"))
|
||||||
|
|
||||||
|
let etag = httpResponse.allHeaderFields["Etag"] as? String
|
||||||
|
try? etag?.write(
|
||||||
|
to: pyoncordDirectory.appendingPathComponent("etag.txt"), atomically: true,
|
||||||
|
encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchTask.resume()
|
||||||
|
group.wait()
|
||||||
|
|
||||||
|
if let themeString = try? String(
|
||||||
|
contentsOf: pyoncordDirectory.appendingPathComponent("current-theme.json"))
|
||||||
|
{
|
||||||
|
orig.executeApplicationScript(
|
||||||
|
"globalThis.__PYON_LOADER__.storedTheme=\(themeString)".data(using: .utf8)!, url: source, async: async)
|
||||||
|
}
|
||||||
|
|
||||||
|
let preloadsDirectory = pyoncordDirectory.appendingPathComponent("preloads")
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: preloadsDirectory.path) {
|
||||||
|
do {
|
||||||
|
let contents = try FileManager.default.contentsOfDirectory(
|
||||||
|
at: preloadsDirectory, includingPropertiesForKeys: nil, options: [])
|
||||||
|
|
||||||
|
for fileURL in contents {
|
||||||
|
if fileURL.pathExtension == "js" {
|
||||||
|
os_log(
|
||||||
|
"Executing preload JS file %{public}@ ", log: revengeLog, type: .info, fileURL.absoluteString)
|
||||||
|
|
||||||
|
if let data = try? Data(contentsOf: fileURL) {
|
||||||
|
orig.executeApplicationScript(data, url: source, async: async)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
os_log("Error reading contents of preloads directory", log: revengeLog, type: .error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if bundle != nil {
|
||||||
|
os_log("Executing JS bundle", log: revengeLog, type: .info)
|
||||||
|
orig.executeApplicationScript(bundle!, url: source, async: async)
|
||||||
|
} else {
|
||||||
|
os_log("Unable to fetch JS bundle", log: revengeLog, type: .error)
|
||||||
|
}
|
||||||
|
|
||||||
|
os_log("Executing original script", log: revengeLog, type: .info)
|
||||||
|
orig.executeApplicationScript(script, url: url, async: async)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RevengeTweak: Tweak {
|
||||||
|
func tweakDidActivate() {
|
||||||
|
if let themeData = try? Data(
|
||||||
|
contentsOf: pyoncordDirectory.appendingPathComponent("current-theme.json")) {
|
||||||
|
let theme = try? JSONDecoder().decode(Theme.self, from: themeData)
|
||||||
|
if let semanticColors = theme?.data.semanticColors { swizzleDCDThemeColor(semanticColors) }
|
||||||
|
if let rawColors = theme?.data.rawColors { swizzleUIColor(rawColors) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if let fontData = try? Data(
|
||||||
|
contentsOf: pyoncordDirectory.appendingPathComponent("fonts.json")) {
|
||||||
|
let fonts = try? JSONDecoder().decode(FontDefinition.self, from: fontData)
|
||||||
|
if let main = fonts?.main { patchFonts(main, fontDefName: fonts!.name) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
Sources/RevengeTweak/Utils.x.swift
Normal file
16
Sources/RevengeTweak/Utils.x.swift
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
func getPyoncordDirectory() -> URL {
|
||||||
|
let documentDirectoryURL = try! FileManager.default.url(
|
||||||
|
for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
|
||||||
|
|
||||||
|
let pyoncordFolderURL = documentDirectoryURL.appendingPathComponent("pyoncord")
|
||||||
|
|
||||||
|
// Create the "pyoncord" folder if it doesn't exist
|
||||||
|
if !FileManager.default.fileExists(atPath: pyoncordFolderURL.path) {
|
||||||
|
try! FileManager.default.createDirectory(
|
||||||
|
at: pyoncordFolderURL, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pyoncordFolderURL
|
||||||
|
}
|
||||||
11
Sources/RevengeTweakC/Tweak.m
Normal file
11
Sources/RevengeTweakC/Tweak.m
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
#import <Orion/Orion.h>
|
||||||
|
|
||||||
|
const char * const _Nonnull get_install_prefix(void) {
|
||||||
|
return THEOS_PACKAGE_INSTALL_PREFIX;
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((constructor)) static void init() {
|
||||||
|
// Initialize Orion - do not remove this line.
|
||||||
|
orion_init();
|
||||||
|
// Custom initialization code goes here.
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
@import Foundation;
|
||||||
|
|
||||||
@interface RCTCxxBridge : NSObject
|
@interface RCTCxxBridge : NSObject
|
||||||
|
|
||||||
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async;
|
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async;
|
||||||
@end
|
|
||||||
|
@end
|
||||||
3
Sources/RevengeTweakC/include/Tweak.h
Normal file
3
Sources/RevengeTweakC/include/Tweak.h
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#import "RCTCxxBridge.h"
|
||||||
|
|
||||||
|
const char * const _Nonnull get_install_prefix(void);
|
||||||
4
Sources/RevengeTweakC/include/module.modulemap
Normal file
4
Sources/RevengeTweakC/include/module.modulemap
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
module RevengeTweakC {
|
||||||
|
umbrella "."
|
||||||
|
export *
|
||||||
|
}
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
#import <dlfcn.h>
|
|
||||||
#import "Logger.h"
|
|
||||||
|
|
||||||
#define DISCORD_BUNDLE_ID @"com.hammerandchisel.discord"
|
|
||||||
#define DISCORD_NAME @"Discord"
|
|
||||||
|
|
||||||
static NSString *getAccessGroupID(void) {
|
|
||||||
NSDictionary *query = @{
|
|
||||||
(__bridge NSString *)kSecClass: (__bridge NSString *)kSecClassGenericPassword,
|
|
||||||
(__bridge NSString *)kSecAttrAccount: @"bundleSeedID",
|
|
||||||
(__bridge NSString *)kSecAttrService: @"",
|
|
||||||
(__bridge NSString *)kSecReturnAttributes: @YES
|
|
||||||
};
|
|
||||||
|
|
||||||
CFDictionaryRef result = NULL;
|
|
||||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
|
|
||||||
|
|
||||||
if (status == errSecItemNotFound) {
|
|
||||||
status = SecItemAdd((__bridge CFDictionaryRef)query, (CFTypeRef *)&result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status != errSecSuccess) return nil;
|
|
||||||
|
|
||||||
NSString *accessGroup = [(__bridge NSDictionary *)result objectForKey:(__bridge NSString *)kSecAttrAccessGroup];
|
|
||||||
if (result) CFRelease(result);
|
|
||||||
|
|
||||||
return accessGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
static BOOL isSelfCall(void) {
|
|
||||||
NSArray *address = [NSThread callStackReturnAddresses];
|
|
||||||
Dl_info info = {0};
|
|
||||||
if (dladdr((void *)[address[2] longLongValue], &info) == 0) return NO;
|
|
||||||
NSString *path = [NSString stringWithUTF8String:info.dli_fname];
|
|
||||||
return [path hasPrefix:NSBundle.mainBundle.bundlePath];
|
|
||||||
}
|
|
||||||
|
|
||||||
%group Sideloading
|
|
||||||
|
|
||||||
%hook NSBundle
|
|
||||||
- (NSString *)bundleIdentifier {
|
|
||||||
return isSelfCall() ? DISCORD_BUNDLE_ID : %orig;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (NSDictionary *)infoDictionary {
|
|
||||||
if (!isSelfCall()) return %orig;
|
|
||||||
|
|
||||||
NSMutableDictionary *info = [%orig mutableCopy];
|
|
||||||
info[@"CFBundleIdentifier"] = DISCORD_BUNDLE_ID;
|
|
||||||
info[@"CFBundleDisplayName"] = DISCORD_NAME;
|
|
||||||
info[@"CFBundleName"] = DISCORD_NAME;
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (id)objectForInfoDictionaryKey:(NSString *)key {
|
|
||||||
if (!isSelfCall()) return %orig;
|
|
||||||
|
|
||||||
if ([key isEqualToString:@"CFBundleIdentifier"]) return DISCORD_BUNDLE_ID;
|
|
||||||
if ([key isEqualToString:@"CFBundleDisplayName"] ||
|
|
||||||
[key isEqualToString:@"CFBundleName"]) return DISCORD_NAME;
|
|
||||||
return %orig;
|
|
||||||
}
|
|
||||||
%end
|
|
||||||
|
|
||||||
%hook NSFileManager
|
|
||||||
- (NSURL *)containerURLForSecurityApplicationGroupIdentifier:(NSString *)groupIdentifier {
|
|
||||||
RevengeLog(@"containerURLForSecurityApplicationGroupIdentifier called! %@", groupIdentifier ?: @"nil");
|
|
||||||
|
|
||||||
NSArray *paths = [self URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask];
|
|
||||||
NSURL *lastPath = [paths lastObject];
|
|
||||||
return [lastPath URLByAppendingPathComponent:@"AppGroup"];
|
|
||||||
}
|
|
||||||
%end
|
|
||||||
|
|
||||||
%hook UIPasteboard
|
|
||||||
- (NSString *)_accessGroup {
|
|
||||||
return getAccessGroupID();
|
|
||||||
}
|
|
||||||
%end
|
|
||||||
|
|
||||||
%end
|
|
||||||
|
|
||||||
%ctor {
|
|
||||||
BOOL isAppStoreApp = [[NSFileManager defaultManager] fileExistsAtPath:[[NSBundle mainBundle] appStoreReceiptURL].path];
|
|
||||||
if (!isAppStoreApp) %init(Sideloading);
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
#import "Theme.h"
|
|
||||||
#import "Utils.h"
|
|
||||||
#import "Logger.h"
|
|
||||||
#import <objc/runtime.h>
|
|
||||||
|
|
||||||
void swizzleDCDThemeColor(NSDictionary<NSString *, NSArray<NSString *> *> *semanticColors) {
|
|
||||||
RevengeLog(@"Swizzling DCDThemeColor");
|
|
||||||
|
|
||||||
Class DCDTheme = NSClassFromString(@"DCDTheme");
|
|
||||||
Class dcdThemeTarget = object_getClass(DCDTheme);
|
|
||||||
|
|
||||||
SEL themeIndexSelector = NSSelectorFromString(@"themeIndex");
|
|
||||||
Method themeIndexMethod = class_getClassMethod(dcdThemeTarget, themeIndexSelector);
|
|
||||||
IMP themeIndexImpl = method_getImplementation(themeIndexMethod);
|
|
||||||
int (*themeIndex)(id, SEL) = (int (*)(id, SEL))themeIndexImpl;
|
|
||||||
|
|
||||||
Class DCDThemeColor = NSClassFromString(@"DCDThemeColor");
|
|
||||||
Class target = object_getClass(DCDThemeColor);
|
|
||||||
|
|
||||||
unsigned int methodCount;
|
|
||||||
Method *methods = class_copyMethodList(target, &methodCount);
|
|
||||||
|
|
||||||
for (unsigned int i = 0; i < methodCount; i++) {
|
|
||||||
Method method = methods[i];
|
|
||||||
SEL selector = method_getName(method);
|
|
||||||
NSString *methodName = NSStringFromSelector(selector);
|
|
||||||
|
|
||||||
NSArray<NSString *> *semanticColor = semanticColors[methodName];
|
|
||||||
if (semanticColor) {
|
|
||||||
RevengeLog(@"Swizzling %@", methodName);
|
|
||||||
|
|
||||||
IMP originalImpl = method_getImplementation(method);
|
|
||||||
UIColor *(*original)(id, SEL) = (UIColor *(*)(id, SEL))originalImpl;
|
|
||||||
|
|
||||||
id block = ^UIColor *(id self) {
|
|
||||||
int themeIndexVal = themeIndex(dcdThemeTarget, themeIndexSelector);
|
|
||||||
if (semanticColor.count - 1 >= themeIndexVal) {
|
|
||||||
UIColor *semanticUIColor = hexToUIColor(semanticColor[themeIndexVal]);
|
|
||||||
if (semanticUIColor) {
|
|
||||||
return semanticUIColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return original(target, selector);
|
|
||||||
};
|
|
||||||
|
|
||||||
IMP newImpl = imp_implementationWithBlock(block);
|
|
||||||
method_setImplementation(method, newImpl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(methods);
|
|
||||||
}
|
|
||||||
|
|
||||||
void swizzleUIColor(NSDictionary<NSString *, NSString *> *rawColors) {
|
|
||||||
RevengeLog(@"Swizzling UIColor");
|
|
||||||
|
|
||||||
Class UIColorClass = NSClassFromString(@"UIColor");
|
|
||||||
Class target = object_getClass(UIColorClass);
|
|
||||||
|
|
||||||
unsigned int methodCount;
|
|
||||||
Method *methods = class_copyMethodList(target, &methodCount);
|
|
||||||
|
|
||||||
for (unsigned int i = 0; i < methodCount; i++) {
|
|
||||||
Method method = methods[i];
|
|
||||||
SEL selector = method_getName(method);
|
|
||||||
NSString *methodName = NSStringFromSelector(selector);
|
|
||||||
|
|
||||||
NSString *rawColor = rawColors[methodName];
|
|
||||||
if (rawColor) {
|
|
||||||
RevengeLog(@"Swizzling %@", methodName);
|
|
||||||
|
|
||||||
id block = ^UIColor *(id self) {
|
|
||||||
return hexToUIColor(rawColor);
|
|
||||||
};
|
|
||||||
|
|
||||||
IMP newImpl = imp_implementationWithBlock(block);
|
|
||||||
method_setImplementation(method, newImpl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
free(methods);
|
|
||||||
}
|
|
||||||
227
Sources/Tweak.xm
227
Sources/Tweak.xm
|
|
@ -1,227 +0,0 @@
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
#import <jsi/jsi.h>
|
|
||||||
#import "Utils.h"
|
|
||||||
#import "Logger.h"
|
|
||||||
#import "Theme.h"
|
|
||||||
#import "Fonts.h"
|
|
||||||
#import "LoaderConfig.h"
|
|
||||||
#import "JSI.h"
|
|
||||||
#import "RCTHost.h"
|
|
||||||
|
|
||||||
using namespace facebook;
|
|
||||||
|
|
||||||
static NSURL *source;
|
|
||||||
static BOOL isJailbroken;
|
|
||||||
static NSString *revengePatchesBundlePath;
|
|
||||||
static NSURL *pyoncordDirectory;
|
|
||||||
static LoaderConfig *loaderConfig;
|
|
||||||
static BOOL didInject = NO;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs the full Revenge injection sequence (loader payload, downloaded bundle, stored theme,
|
|
||||||
* fonts, preloads) and hands each script's data off to `execute`.
|
|
||||||
*
|
|
||||||
* Historically this fed straight into `RCTCxxBridge -executeApplicationScript:url:async:`
|
|
||||||
* (React Native's legacy bridge), which is what `execute` still does below for old-arch hosts.
|
|
||||||
* On current Discord iOS builds (Bridgeless / New Architecture, confirmed via RainTweak -
|
|
||||||
* codeberg.org/raincord/RainTweak, an actively-shipping loader for a sibling Revenge/Bunny fork
|
|
||||||
* that explicitly requires "Discord v332 or above" for Bridgeless), that hook point never fires
|
|
||||||
* at all, so `execute` is instead wired to `JSI evaluate:` against the live runtime - see the
|
|
||||||
* `RCTHost` hook below, which is the verified entry point.
|
|
||||||
*/
|
|
||||||
static BOOL runRevengeInjection(BOOL async, void (^execute)(NSData *data, BOOL async)) {
|
|
||||||
NSBundle *revengePatchesBundle = [NSBundle bundleWithPath:revengePatchesBundlePath];
|
|
||||||
if (!revengePatchesBundle) {
|
|
||||||
RevengeLog(@"Failed to load RevengePatches bundle from path: %@", revengePatchesBundlePath);
|
|
||||||
showErrorAlert(@"Loader Error", @"Failed to initialize mod loader. Please reinstall the tweak.");
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSURL *patchPath = [revengePatchesBundle URLForResource:@"payload-base" withExtension:@"js"];
|
|
||||||
if (!patchPath) {
|
|
||||||
RevengeLog(@"Failed to find payload-base.js in bundle");
|
|
||||||
showErrorAlert(@"Loader Error", @"Failed to initialize mod loader. Please reinstall the tweak.");
|
|
||||||
return NO;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *patchData = [NSData dataWithContentsOfURL:patchPath];
|
|
||||||
RevengeLog(@"Injecting loader");
|
|
||||||
execute(patchData, YES);
|
|
||||||
|
|
||||||
__block NSData *bundle = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"bundle.js"]];
|
|
||||||
|
|
||||||
dispatch_group_t group = dispatch_group_create();
|
|
||||||
dispatch_group_enter(group);
|
|
||||||
|
|
||||||
NSURL *bundleUrl;
|
|
||||||
if (loaderConfig.customLoadUrlEnabled && loaderConfig.customLoadUrl) {
|
|
||||||
bundleUrl = loaderConfig.customLoadUrl;
|
|
||||||
RevengeLog(@"Using custom load URL: %@", bundleUrl.absoluteString);
|
|
||||||
} else {
|
|
||||||
bundleUrl = [NSURL URLWithString:@"https://github.com/revenge-mod/revenge-bundle/releases/latest/download/revenge.min.js"];
|
|
||||||
RevengeLog(@"Using default bundle URL: %@", bundleUrl.absoluteString);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSMutableURLRequest *bundleRequest = [NSMutableURLRequest requestWithURL:bundleUrl
|
|
||||||
cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData
|
|
||||||
timeoutInterval:3.0];
|
|
||||||
|
|
||||||
NSString *bundleEtag = [NSString stringWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"etag.txt"]
|
|
||||||
encoding:NSUTF8StringEncoding
|
|
||||||
error:nil];
|
|
||||||
if (bundleEtag && bundle) {
|
|
||||||
[bundleRequest setValue:bundleEtag forHTTPHeaderField:@"If-None-Match"];
|
|
||||||
}
|
|
||||||
|
|
||||||
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
|
|
||||||
[[session dataTaskWithRequest:bundleRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
|
||||||
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
|
|
||||||
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
|
||||||
if (httpResponse.statusCode == 200) {
|
|
||||||
bundle = data;
|
|
||||||
[bundle writeToURL:[pyoncordDirectory URLByAppendingPathComponent:@"bundle.js"] atomically:YES];
|
|
||||||
|
|
||||||
NSString *etag = [httpResponse.allHeaderFields objectForKey:@"Etag"];
|
|
||||||
if (etag) {
|
|
||||||
[etag writeToURL:[pyoncordDirectory URLByAppendingPathComponent:@"etag.txt"]
|
|
||||||
atomically:YES
|
|
||||||
encoding:NSUTF8StringEncoding
|
|
||||||
error:nil];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dispatch_group_leave(group);
|
|
||||||
}] resume];
|
|
||||||
|
|
||||||
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
|
|
||||||
|
|
||||||
NSString *themeString = [NSString stringWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"current-theme.json"]
|
|
||||||
encoding:NSUTF8StringEncoding
|
|
||||||
error:nil];
|
|
||||||
if (themeString) {
|
|
||||||
NSString *jsCode = [NSString stringWithFormat:@"globalThis.__PYON_LOADER__.storedTheme=%@", themeString];
|
|
||||||
execute([jsCode dataUsingEncoding:NSUTF8StringEncoding], async);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSData *fontData = [NSData dataWithContentsOfURL:[pyoncordDirectory URLByAppendingPathComponent:@"fonts.json"]];
|
|
||||||
if (fontData) {
|
|
||||||
NSError *jsonError;
|
|
||||||
NSDictionary *fontDict = [NSJSONSerialization JSONObjectWithData:fontData options:0 error:&jsonError];
|
|
||||||
if (!jsonError && fontDict[@"main"]) {
|
|
||||||
RevengeLog(@"Found font configuration, applying...");
|
|
||||||
patchFonts(fontDict[@"main"], fontDict[@"name"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bundle) {
|
|
||||||
RevengeLog(@"Executing JS bundle");
|
|
||||||
execute(bundle, async);
|
|
||||||
}
|
|
||||||
|
|
||||||
NSURL *preloadsDirectory = [pyoncordDirectory URLByAppendingPathComponent:@"preloads"];
|
|
||||||
if ([[NSFileManager defaultManager] fileExistsAtPath:preloadsDirectory.path]) {
|
|
||||||
NSError *error = nil;
|
|
||||||
NSArray *contents = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:preloadsDirectory
|
|
||||||
includingPropertiesForKeys:nil
|
|
||||||
options:0
|
|
||||||
error:&error];
|
|
||||||
if (!error) {
|
|
||||||
for (NSURL *fileURL in contents) {
|
|
||||||
if ([[fileURL pathExtension] isEqualToString:@"js"]) {
|
|
||||||
RevengeLog(@"Executing preload JS file %@", fileURL.absoluteString);
|
|
||||||
NSData *data = [NSData dataWithContentsOfURL:fileURL];
|
|
||||||
if (data) {
|
|
||||||
execute(data, async);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
RevengeLog(@"Error reading contents of preloads directory");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return YES;
|
|
||||||
}
|
|
||||||
|
|
||||||
%hook RCTHost
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bridgeless-mode entry point (React Native's New Architecture) - fires once the JS runtime is
|
|
||||||
* ready, letting Discord's own bundle load completely untouched via %orig first. Revenge's own
|
|
||||||
* scripts are then evaluated directly against the same runtime via JSI, rather than trying to
|
|
||||||
* masquerade as another RCTSource bundle load (which has no public constructor). This mirrors
|
|
||||||
* RainTweak's verified approach - see comment on `runRevengeInjection` above.
|
|
||||||
*/
|
|
||||||
- (void)instance:(id)instance didInitializeRuntime:(jsi::Runtime &)runtime {
|
|
||||||
%orig;
|
|
||||||
|
|
||||||
if (didInject) return;
|
|
||||||
didInject = YES;
|
|
||||||
|
|
||||||
runRevengeInjection(YES, ^(NSData *data, BOOL async) {
|
|
||||||
[JSI evaluate:data tag:@"revenge" runtime:runtime];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
%end
|
|
||||||
|
|
||||||
%hook RCTCxxBridge
|
|
||||||
|
|
||||||
// Legacy-bridge entry point. Fires when the host app runs React Native's old (non-Bridgeless)
|
|
||||||
// bridge. Kept for hosts that haven't moved to Bridgeless; harmless if RCTCxxBridge is never
|
|
||||||
// instantiated (Logos skips hooking classes that aren't present at runtime).
|
|
||||||
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async {
|
|
||||||
if (![url.absoluteString containsString:@"main.jsbundle"] || didInject) {
|
|
||||||
return %orig;
|
|
||||||
}
|
|
||||||
didInject = YES;
|
|
||||||
|
|
||||||
BOOL injected = runRevengeInjection(async, ^(NSData *data, BOOL scriptAsync) {
|
|
||||||
%orig(data, source, scriptAsync);
|
|
||||||
});
|
|
||||||
if (!injected) {
|
|
||||||
didInject = NO;
|
|
||||||
return %orig;
|
|
||||||
}
|
|
||||||
|
|
||||||
%orig(script, url, async);
|
|
||||||
}
|
|
||||||
|
|
||||||
%end
|
|
||||||
|
|
||||||
%ctor {
|
|
||||||
@autoreleasepool {
|
|
||||||
source = [NSURL URLWithString:@"revenge"];
|
|
||||||
|
|
||||||
NSString *install_prefix = @"/var/jb";
|
|
||||||
isJailbroken = [[NSFileManager defaultManager] fileExistsAtPath:install_prefix];
|
|
||||||
|
|
||||||
NSString *bundlePath = [NSString stringWithFormat:@"%@/Library/Application Support/RevengeResources.bundle", install_prefix];
|
|
||||||
RevengeLog(@"Is jailbroken: %d", isJailbroken);
|
|
||||||
RevengeLog(@"Bundle path for jailbroken: %@", bundlePath);
|
|
||||||
|
|
||||||
NSString *jailedPath = [[NSBundle mainBundle].bundleURL.path stringByAppendingPathComponent:@"RevengeResources.bundle"];
|
|
||||||
RevengeLog(@"Bundle path for jailed: %@", jailedPath);
|
|
||||||
|
|
||||||
revengePatchesBundlePath = isJailbroken ? bundlePath : jailedPath;
|
|
||||||
RevengeLog(@"Selected bundle path: %@", revengePatchesBundlePath);
|
|
||||||
|
|
||||||
BOOL bundleExists = [[NSFileManager defaultManager] fileExistsAtPath:revengePatchesBundlePath];
|
|
||||||
RevengeLog(@"Bundle exists at path: %d", bundleExists);
|
|
||||||
|
|
||||||
NSError *error = nil;
|
|
||||||
NSArray *bundleContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:revengePatchesBundlePath error:&error];
|
|
||||||
if (error) {
|
|
||||||
RevengeLog(@"Error listing bundle contents: %@", error);
|
|
||||||
} else {
|
|
||||||
RevengeLog(@"Bundle contents: %@", bundleContents);
|
|
||||||
}
|
|
||||||
|
|
||||||
pyoncordDirectory = getPyoncordDirectory();
|
|
||||||
loaderConfig = [[LoaderConfig alloc] init];
|
|
||||||
[loaderConfig loadConfig];
|
|
||||||
|
|
||||||
%init;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,76 +0,0 @@
|
||||||
#import "Utils.h"
|
|
||||||
|
|
||||||
BOOL isHermesBytecode(NSData *data) {
|
|
||||||
if (!data || data.length < 4) return NO;
|
|
||||||
const uint8_t *bytes = (const uint8_t *)data.bytes;
|
|
||||||
// Hermes bytecode magic: "Herm"
|
|
||||||
return bytes[0] == 0x48 && bytes[1] == 0x65 && bytes[2] == 0x72 && bytes[3] == 0x6d;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSURL *getPyoncordDirectory(void) {
|
|
||||||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
||||||
NSURL *documentDirectoryURL = [[fileManager URLsForDirectory:NSDocumentDirectory
|
|
||||||
inDomains:NSUserDomainMask] lastObject];
|
|
||||||
|
|
||||||
NSURL *pyoncordFolderURL = [documentDirectoryURL URLByAppendingPathComponent:@"pyoncord"];
|
|
||||||
|
|
||||||
if (![fileManager fileExistsAtPath:pyoncordFolderURL.path]) {
|
|
||||||
[fileManager createDirectoryAtURL:pyoncordFolderURL
|
|
||||||
withIntermediateDirectories:YES
|
|
||||||
attributes:nil
|
|
||||||
error:nil];
|
|
||||||
}
|
|
||||||
|
|
||||||
return pyoncordFolderURL;
|
|
||||||
}
|
|
||||||
|
|
||||||
UIColor *hexToUIColor(NSString *hex) {
|
|
||||||
if (![hex hasPrefix:@"#"]) {
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
NSString *hexColor = [hex substringFromIndex:1];
|
|
||||||
if (hexColor.length == 6) {
|
|
||||||
hexColor = [hexColor stringByAppendingString:@"ff"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hexColor.length == 8) {
|
|
||||||
unsigned int hexNumber;
|
|
||||||
NSScanner *scanner = [NSScanner scannerWithString:hexColor];
|
|
||||||
if ([scanner scanHexInt:&hexNumber]) {
|
|
||||||
CGFloat r = ((hexNumber & 0xFF000000) >> 24) / 255.0;
|
|
||||||
CGFloat g = ((hexNumber & 0x00FF0000) >> 16) / 255.0;
|
|
||||||
CGFloat b = ((hexNumber & 0x0000FF00) >> 8) / 255.0;
|
|
||||||
CGFloat a = (hexNumber & 0x000000FF) / 255.0;
|
|
||||||
|
|
||||||
return [UIColor colorWithRed:r green:g blue:b alpha:a];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil;
|
|
||||||
}
|
|
||||||
|
|
||||||
void showErrorAlert(NSString *title, NSString *message) {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title
|
|
||||||
message:message
|
|
||||||
preferredStyle:UIAlertControllerStyleAlert];
|
|
||||||
|
|
||||||
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"OK"
|
|
||||||
style:UIAlertActionStyleDefault
|
|
||||||
handler:nil];
|
|
||||||
|
|
||||||
[alert addAction:okAction];
|
|
||||||
|
|
||||||
UIWindow *window = nil;
|
|
||||||
NSArray *windows = [[UIApplication sharedApplication] windows];
|
|
||||||
for (UIWindow *w in windows) {
|
|
||||||
if (w.isKeyWindow) {
|
|
||||||
window = w;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[window.rootViewController presentViewController:alert animated:YES completion:nil];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
globalThis.__PYON_LOADER__ = {
|
|
||||||
loaderName: "@TWEAK_NAME@",
|
|
||||||
loaderVersion: "@PACKAGE_VERSION@",
|
|
||||||
hasThemeSupport: true,
|
|
||||||
storedTheme: null,
|
|
||||||
fontPatch: 2
|
|
||||||
}
|
|
||||||
|
|
@ -12,18 +12,6 @@
|
||||||
"subtitle": "A client mod for Discord",
|
"subtitle": "A client mod for Discord",
|
||||||
"tintColor": "7289da",
|
"tintColor": "7289da",
|
||||||
"versions": [
|
"versions": [
|
||||||
{
|
|
||||||
"version": "254.0",
|
|
||||||
"date": "2024-11-15",
|
|
||||||
"size": 98005042,
|
|
||||||
"downloadURL": "https://github.com/revenge-mod/revenge-tweak/releases/download/v254.0/Revenge.ipa"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"version": "250.0",
|
|
||||||
"date": "2024-10-20",
|
|
||||||
"size": 101218873,
|
|
||||||
"downloadURL": "https://github.com/revenge-mod/revenge-tweak/releases/download/v250.0/Revenge.ipa"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"version": "250.0",
|
"version": "250.0",
|
||||||
"date": "2024-10-17",
|
"date": "2024-10-17",
|
||||||
|
|
|
||||||
8
control
8
control
|
|
@ -1,11 +1,11 @@
|
||||||
Package: io.github.revenge-mod.app
|
Package: io.github.revenge-mod.app
|
||||||
Name: Revenge
|
Name: Revenge
|
||||||
Version: 2.0.1
|
Version: 1.0.0
|
||||||
Architecture: iphoneos-arm
|
Architecture: iphoneos-arm
|
||||||
Description: A mod for Discord's mobile apps.
|
Description: A mod for Discord's mobile apps.
|
||||||
Maintainer: Cristiandis
|
Maintainer: Cristiandis
|
||||||
Author: Pylix, Adrian Castro, FieryFlames, Cristiandis
|
Author: Pylix, FieryFlames, Cristiandis
|
||||||
Icon: https://avatars.githubusercontent.com/u/159218871
|
Icon: https://avatars.githubusercontent.com/u/159218871
|
||||||
Section: Tweaks
|
Section: Tweaks
|
||||||
Depends: mobilesubstrate (>= 0.9.5000)
|
Depends: ${ORION}, firmware (>= 13.0)
|
||||||
Conflicts: app.enmity, mod.vendetta, app.bunny
|
Conflicts: app.enmity, mod.vendetta, mod.bunny
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue