Compare commits
No commits in common. "main" and "v246.0" have entirely different histories.
43 changed files with 785 additions and 3548 deletions
135
.github/workflows/build.yml
vendored
Normal file
135
.github/workflows/build.yml
vendored
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
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: Inject tweak
|
||||
run: |
|
||||
bash -c "$(curl https://raw.githubusercontent.com/asdfzxcvbn/pyzule/main/install-pyzule.sh)"
|
||||
pyzule -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 }}
|
||||
|
||||
- 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
|
||||
run-name: ${{ github.event.inputs.release == 'true' && 'Release' || 'Build' }} for ${{ github.event.inputs.ipa_url }}
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
|
@ -9,15 +8,10 @@ on:
|
|||
default: ""
|
||||
required: true
|
||||
type: string
|
||||
release:
|
||||
description: 'Release the build'
|
||||
required: true
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
|
||||
jobs:
|
||||
build-tweak:
|
||||
runs-on: macos-14
|
||||
|
|
@ -72,10 +66,6 @@ jobs:
|
|||
if: env.DEB_DOWNLOADED == 'false'
|
||||
uses: Randomblock1/theos-action@v1
|
||||
|
||||
- name: Set GNU Make path
|
||||
run: |
|
||||
echo "PATH=$(brew --prefix make)/libexec/gnubin:$PATH" >> $GITHUB_ENV
|
||||
|
||||
- name: Build packages
|
||||
if: env.DEB_DOWNLOADED == 'false'
|
||||
run: make package FINALPACKAGE=1 && make package FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless
|
||||
|
|
@ -122,6 +112,7 @@ jobs:
|
|||
- 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: |
|
||||
|
|
@ -133,24 +124,11 @@ jobs:
|
|||
ROOTLESS_DEB_FILE_NAME="${PACKAGE}_${VERSION}_iphoneos-arm64.deb"
|
||||
echo "ROOTLESS_DEB_FILE_NAME=$ROOTLESS_DEB_FILE_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
|
||||
run: pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip Pillow
|
||||
|
||||
- name: Inject tweak
|
||||
run: |
|
||||
cyan -duws -i discord.ipa -o ${{ github.workspace }}/${{ env.APP_NAME }}.ipa -f ${{ github.workspace }}/${{ env.DEB_FILE_NAME }}
|
||||
bash -c "$(curl https://raw.githubusercontent.com/asdfzxcvbn/pyzule/main/install-pyzule.sh)"
|
||||
pyzule -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 }}
|
||||
|
||||
- name: Upload ipa as artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
|
|
@ -159,7 +137,6 @@ jobs:
|
|||
path: ${{ github.workspace }}/${{ env.APP_NAME }}.ipa
|
||||
|
||||
release-app:
|
||||
if: ${{ github.event.inputs.release == 'true' }}
|
||||
runs-on: macos-14
|
||||
needs: build-ipa
|
||||
permissions:
|
||||
|
|
@ -206,7 +183,6 @@ jobs:
|
|||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
app-repo:
|
||||
if: ${{ github.event.inputs.release == 'true' }}
|
||||
continue-on-error: true
|
||||
runs-on: macos-14
|
||||
needs: release-app
|
||||
|
|
@ -238,4 +214,4 @@ jobs:
|
|||
default_author: github_actions
|
||||
message: "chore: update app-repo.json"
|
||||
add: app-repo.json
|
||||
|
||||
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
|
|
@ -3,12 +3,5 @@
|
|||
"MD029": false,
|
||||
"MD033": false,
|
||||
"MD045": false,
|
||||
},
|
||||
"VsCodeTaskButtons.tasks": [
|
||||
{
|
||||
"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
|
||||
ARCHS = arm64
|
||||
INSTALL_TARGET_PROCESSES = Discord
|
||||
ifeq ($(THEOS_PACKAGE_SCHEME),rootless)
|
||||
TARGET := iphone:clang:latest:15.0
|
||||
else
|
||||
TARGET := iphone:clang:latest:12.2
|
||||
endif
|
||||
|
||||
|
||||
include $(THEOS)/makefiles/common.mk
|
||||
|
||||
TWEAK_NAME = Revenge
|
||||
BUNDLE_NAME = RevengeResources
|
||||
TWEAK_NAME = BunnyTweak
|
||||
|
||||
Revenge_FILES = $(wildcard Sources/*.x Sources/*.xm Sources/*.m Sources/*.mm Sources/**/*.x Sources/**/*.xm Sources/**/*.m Sources/**/*.mm)
|
||||
Revenge_CFLAGS = -fobjc-arc -DPACKAGE_VERSION='@"$(THEOS_PACKAGE_BASE_VERSION)"' -I$(THEOS_PROJECT_DIR)/Headers
|
||||
Revenge_CCFLAGS = -std=c++17 -Wno-c++11-narrowing -I$(THEOS_PROJECT_DIR)/Headers
|
||||
Revenge_FRAMEWORKS = Foundation UIKit CoreGraphics CoreText CoreFoundation
|
||||
BunnyTweak_FILES = $(shell find Sources/BunnyTweak -name '*.swift') $(shell find Sources/BunnyTweakC -name '*.m' -o -name '*.c' -o -name '*.mm' -o -name '*.cpp')
|
||||
BunnyTweak_SWIFTFLAGS = -ISources/BunnyTweakC/include
|
||||
BunnyTweak_CFLAGS = -fobjc-arc -ISources/BunnyTweakC/include
|
||||
|
||||
RevengeResources_INSTALL_PATH = "/Library/Application\ Support/"
|
||||
RevengeResources_RESOURCE_DIRS = Resources
|
||||
BunnyTweak_BUNDLE_NAME = BunnyPatches
|
||||
BunnyTweak_BUNDLE_RESOURCE_DIRS = Resources
|
||||
|
||||
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: "BunnyTweak",
|
||||
platforms: [.iOS(deploymentTarget)],
|
||||
products: [
|
||||
.library(
|
||||
name: "BunnyTweak",
|
||||
targets: ["BunnyTweak"]
|
||||
),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "BunnyTweakC",
|
||||
cSettings: [.unsafeFlags(cFlags)],
|
||||
cxxSettings: [.unsafeFlags(cxxFlags)]
|
||||
),
|
||||
.target(
|
||||
name: "BunnyTweak",
|
||||
dependencies: ["BunnyTweakC"],
|
||||
swiftSettings: [.unsafeFlags(swiftFlags)]
|
||||
),
|
||||
]
|
||||
)
|
||||
68
README.md
68
README.md
|
|
@ -1,54 +1,16 @@
|
|||
<p align="center">
|
||||
<picture>
|
||||
<source
|
||||
width="512px"
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="assets/wordmark/wordmark+slogan-dark.svg"
|
||||
>
|
||||
<img
|
||||
width="512px"
|
||||
src="assets/wordmark/wordmark+slogan-light.svg"
|
||||
>
|
||||
</picture>
|
||||
<br>
|
||||
<a href="https://revenge-mod.github.io/">
|
||||
<picture>
|
||||
<source height="32px" media="(prefers-color-scheme: dark)" srcset="assets/logo/logo-dark.svg" />
|
||||
<img height="32px" src="assets/logo/logo-light.svg" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://discord.com/invite/ddcQf3s2Uq">
|
||||
<picture>
|
||||
<source height="32px" media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
<img height="32px" src="https://user-images.githubusercontent.com/13122796/178032563-d4e084b7-244e-4358-af50-26bde6dd4996.png" />
|
||||
</picture>
|
||||
</a>
|
||||
<a href="https://github.com/revenge-mod">
|
||||
<picture>
|
||||
<source height="32px" media="(prefers-color-scheme: dark)" srcset="https://i.ibb.co/dMMmCrW/Git-Hub-Mark.png" />
|
||||
<img height="32px" src="https://i.ibb.co/9wV3HGF/Git-Hub-Mark-Light.png" />
|
||||
</picture>
|
||||
</a>
|
||||
# BunnyTweak
|
||||
|
||||
</a>
|
||||
</p>
|
||||
|
||||
# 💊 RevengeTweak
|
||||
|
||||

|
||||

|
||||
|
||||
Tweak to inject [Revenge](https://github.com/revenge-mod/Revenge) into Discord. Forked [BunnyTweak](https://github.com/pyoncord/BunnyTweak), modified to match with [RevengeXposed](https://github.com/revenge-mod/Revenge-xposed) behavior. There are still slight differences between these two, and this tweak may be missing some loader features.
|
||||
Tweak to inject [Bunny](https://github.com/pyoncord/Bunny) into Discord. Forked [VendettaTweak](https://github.com/vendetta-mod/VendettaTweak), modified to match with [BunnyXposed](https://github.com/pyoncord/BunnyXposed) behavior. There are still slight differences between these two, and this tweak may be missing some loader features.
|
||||
|
||||
> [!NOTE]
|
||||
> As of right now this tweak does not encompass some functionalities when running in a jailed environment with a wildcard certificate \
|
||||
> If you value these features sign the application with a local dev certificate:
|
||||
> - setAlternateAppIcon does not work, thus breaking dynamic app icons
|
||||
> - sharing files to the application/selecting items via the Files app does not work
|
||||
> - sharing files to the application/selecting items via the Files app does not work
|
||||
|
||||
## 🔽 Installation
|
||||
## Installation
|
||||
|
||||
Builds can be found in the [Releases](https://github.com/revenge-mod/revenge-tweak/releases/latest) tab.
|
||||
Builds can be found in the [Releases](https://github.com/pyoncord/BunnyTweak/releases/latest) tab.
|
||||
|
||||
> [!NOTE]
|
||||
> Raw decrypted IPAs which are used to provide prepatched IPAs are sourced from the [Enmity](https://github.com/enmity-mod/) community. These raw decrypted IPAs are also used throughout Enmity related projects such as [enmity-mod/tweak](https://github.com/enmity-mod/tweak/) and [acquitelol/rosiecord](https://github.com/acquitelol/rosiecord).\
|
||||
|
|
@ -57,19 +19,19 @@ Builds can be found in the [Releases](https://github.com/revenge-mod/revenge-twe
|
|||
### Jailbroken
|
||||
|
||||
1. Install the Orion runtime via your preferred package manager, by adding `https://repo.chariz.com/` to your sources, then finding `Orion Runtime`.
|
||||
1. Install Revenge by downloading the appropriate Debian package (or by building your own, see [Building RevengeTweak locally](#building-revengetweak-locally)) and adding it to your package manager. Use the file ending in `arm.deb` for rootful jailbreaks, and the file ending in `arm64.deb` for rootless jailbreaks.
|
||||
1. Install Bunny by downloading the appropriate Debian package (or by building your own, see [Building BunnyTweak locally](#building-bunnytweak-locally)) and adding it to your package manager. Use the file ending in `arm.deb` for rootful jailbreaks, and the file ending in `arm64.deb` for rootless jailbreaks.
|
||||
|
||||
### Jailed
|
||||
|
||||
<a href="https://tinyurl.com/2jyk87mz"><img src="https://i.imgur.com/dsbDLK9.png" width="230"></a>
|
||||
<a href="https://tinyurl.com/yjn8fpp3"><img src="https://i.imgur.com/46qhEAv.png" width="230"></a>
|
||||
<a href="https://tinyurl.com/24zjszuf"><img src="https://i.imgur.com/dsbDLK9.png" width="230"></a>
|
||||
<a href="https://tinyurl.com/yh455zk6"><img src="https://i.imgur.com/46qhEAv.png" width="230"></a>
|
||||
|
||||
> [!NOTE]
|
||||
> TrollStore may display an encryption warning, which you can disregard.
|
||||
|
||||
1. Download and install [Revenge.ipa](https://github.com/revenge-mod/revenge-tweak/releases/latest/download/Revenge.ipa) using your preferred sideloading method.
|
||||
1. Download and install [Bunny.ipa](https://github.com/pyoncord/BunnyTweak/releases/latest/download/Bunny.ipa) using your preferred sideloading method.
|
||||
|
||||
## 🛠️ Building RevengeTweak locally
|
||||
## Building BunnyTweak locally
|
||||
|
||||
> [!NOTE]
|
||||
> These steps assume you use MacOS.
|
||||
|
|
@ -80,7 +42,7 @@ Builds can be found in the [Releases](https://github.com/revenge-mod/revenge-twe
|
|||
|
||||
2. Install the required dependencies. You can do this by running `brew install make ldid` in your terminal. If you do not have brew installed, follow the instructions [here](https://brew.sh/).
|
||||
|
||||
3. Setup your path accordingly. We recommend you run the following before running the next commands, as well as any time you want to build RevengeTweak.
|
||||
3. Setup your path accordingly. We recommend you run the following before running the next commands, as well as any time you want to build BunnyTweak.
|
||||
|
||||
```bash
|
||||
export PATH="$(brew --prefix make)/libexec/gnubin:$PATH"
|
||||
|
|
@ -96,17 +58,17 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/theos/theos/master/bin/i
|
|||
|
||||
If you've already installed theos, you can run `$THEOS/bin/update-theos` to make sure it's up to date.
|
||||
|
||||
5. Clone this repository with `git clone git@github.com:revenge-mod/RevengeTweak.git` and `cd` into it. Replace the URL with your fork if you've forked this repository.
|
||||
5. Clone this repository with `git clone git@github.com:pyoncord/BunnyTweak.git` and `cd` into it. Replace the URL with your fork if you've forked this repository.
|
||||
|
||||
6. To build RevengeTweak, you can run `rm -rf packages && make clean && make package FINALPACKAGE=1 && make package FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless`. The first command will remove any previous packages, the second will clean the project, the third will build the rootful package (which is denoted by the `arm.deb` ending), and the fourth will build the rootless package (which is denoted by the `arm64.deb` ending).
|
||||
6. To build BunnyTweak, you can run `rm -rf packages && make clean && make package FINALPACKAGE=1 && make package FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless`. The first command will remove any previous packages, the second will clean the project, the third will build the rootful package (which is denoted by the `arm.deb` ending), and the fourth will build the rootless package (which is denoted by the `arm64.deb` ending).
|
||||
|
||||
The first time you run this, it might take a bit longer, but subsequent builds should be much faster.
|
||||
|
||||
The resulting `.deb` files will be in the `packages` folder. As a reminder, `*arm.deb` is for rootful jailbreaks and sideloading, and `*arm64.deb` is for rootless jailbreaks.
|
||||
|
||||
## 📙 Contributing
|
||||
## Contributing
|
||||
|
||||
If you want to contribute, you will basically need to follow the steps for [Building RevengeTweak locally](#building-revengetweak-locally), as well as run `make spm` for the Swift LSP to work.
|
||||
If you want to contribute, you will basically need to follow the steps for [Building BunnyTweak locally](#building-bunnytweak-locally), as well as run `make spm` for the Swift LSP to work.
|
||||
|
||||
<!-- @vladdy was here, battling all these steps so you don't have to. Have fun! :3 -->
|
||||
<!-- @castdrian also was here simplifying these steps immensely -->
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
globalThis.__PYON_LOADER__ = {
|
||||
loaderName: "@TWEAK_NAME@",
|
||||
loaderVersion: "@PACKAGE_VERSION@",
|
||||
loaderName: "BunnyTweak",
|
||||
loaderVersion: "0.3.1",
|
||||
hasThemeSupport: true,
|
||||
storedTheme: null,
|
||||
fontPatch: 2
|
||||
}
|
||||
}
|
||||
99
Sources/BunnyTweak/Fonts.x.swift
Normal file
99
Sources/BunnyTweak/Fonts.x.swift
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import BunnyTweakC
|
||||
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: bunnyLog, 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: bunnyLog, type: .info, fontName, url)
|
||||
|
||||
let parent = fontCachePath.deletingLastPathComponent()
|
||||
if !FileManager.default.fileExists(atPath: parent.path) {
|
||||
os_log("Creating parent directory: %{public}@", log: bunnyLog, 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: bunnyLog, type: .debug, fontName, url)
|
||||
if let data = try? Data(contentsOf: URL(string: url)!) {
|
||||
os_log("Writing font data to: %{public}@", log: bunnyLog, type: .debug, fontCachePath.path)
|
||||
try? data.write(to: fontCachePath)
|
||||
}
|
||||
}
|
||||
|
||||
if let data = try? Data(contentsOf: fontCachePath) {
|
||||
os_log("Registering font %{public}@ with provider", log: bunnyLog, 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: bunnyLog, 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: bunnyLog, type: .info, fontName, font!.postScriptName! as String)
|
||||
} else {
|
||||
os_log("Failed to register font %{public}@: %{public}@", log: bunnyLog, type: .error, fontName, String(describing: error!.takeUnretainedValue()))
|
||||
}
|
||||
} else {
|
||||
os_log("Failed to read font data from: %{public}@", log: bunnyLog, type: .error, fontCachePath.path)
|
||||
}
|
||||
} catch {
|
||||
os_log("Failed to register font %{public}@: %{public}@", log: bunnyLog, type: .error, fontName, error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
47
Sources/BunnyTweak/LoaderConfig.x.swift
Normal file
47
Sources/BunnyTweak/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/bunny.js")!
|
||||
)
|
||||
)
|
||||
|
||||
let pyoncordDirectory = getPyoncordDirectory()
|
||||
let loaderConfigUrl = pyoncordDirectory.appendingPathComponent("loader.json")
|
||||
|
||||
func getLoaderConfig() -> LoaderConfig {
|
||||
os_log("Getting loader config", log: bunnyLog, 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: bunnyLog, type: .debug)
|
||||
|
||||
return loaderConfig
|
||||
} else {
|
||||
throw LoaderConfigError.doesNotExist
|
||||
}
|
||||
} catch {
|
||||
os_log("Couldn't get loader config", log: bunnyLog, type: .error)
|
||||
|
||||
return defaultLoaderConfig
|
||||
}
|
||||
}
|
||||
152
Sources/BunnyTweak/Themes.x.swift
Normal file
152
Sources/BunnyTweak/Themes.x.swift
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import Orion
|
||||
import UIKit
|
||||
import BunnyTweakC
|
||||
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: bunnyLog, 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: bunnyLog, type: .debug)
|
||||
let target: AnyClass = object_getClass(DCDThemeColor)!
|
||||
|
||||
os_log("Found DCDThemeColor", log: bunnyLog, type: .debug)
|
||||
|
||||
var methodCount: UInt32 = 0
|
||||
let methods = class_copyMethodList(target, &methodCount)!
|
||||
|
||||
os_log("DCDThemeColor has %{public}d methods", log: bunnyLog, 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: bunnyLog, type: .debug, methodName)
|
||||
if let semanticColor = semanticColors[methodName] {
|
||||
os_log(
|
||||
"Swizzling %{public}@", log: bunnyLog, 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: bunnyLog, type: .info)
|
||||
|
||||
let UIColor: AnyClass = NSClassFromString("UIColor")!
|
||||
os_log("Found instance of UIColor", log: bunnyLog, type: .debug)
|
||||
let target: AnyClass = object_getClass(UIColor)!
|
||||
|
||||
os_log("Found UIColor", log: bunnyLog, type: .debug)
|
||||
|
||||
var methodCount: UInt32 = 0
|
||||
let methods = class_copyMethodList(target, &methodCount)!
|
||||
|
||||
os_log("UIColor has %{public}d methods", log: bunnyLog, 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: bunnyLog, type: .debug, methodName)
|
||||
if let rawColor = rawColors[methodName] {
|
||||
os_log(
|
||||
"Swizzling %{public}@", log: bunnyLog, 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/BunnyTweak/Tweak.x.swift
Normal file
143
Sources/BunnyTweak/Tweak.x.swift
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import Orion
|
||||
import BunnyTweakC
|
||||
import os
|
||||
|
||||
let bunnyLog = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "bunny")
|
||||
let source = URL(string: "bunny")!
|
||||
|
||||
let install_prefix = String(cString: get_install_prefix())
|
||||
let isJailbroken = FileManager.default.fileExists(atPath: "\(install_prefix)/Library/Application Support/BunnyTweak/BunnyPatches.bundle")
|
||||
|
||||
let bunnyPatchesBundlePath = isJailbroken ? "\(install_prefix)/Library/Application Support/BunnyTweak/BunnyPatches.bundle" : "\(Bundle.main.bundleURL.path)/BunnyPatches.bundle"
|
||||
|
||||
class FileManagerLoadHook: ClassHook<FileManager> {
|
||||
func containerURLForSecurityApplicationGroupIdentifier(_ groupIdentifier: NSString?) -> URL? {
|
||||
os_log("containerURLForSecurityApplicationGroupIdentifier called! %{public}@ groupIdentifier", log: bunnyLog, 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: bunnyLog, type: .debug)
|
||||
|
||||
let loaderConfig = getLoaderConfig()
|
||||
|
||||
let bunnyPatchesBundle = Bundle(path: bunnyPatchesBundlePath)!
|
||||
|
||||
if let patchPath = bunnyPatchesBundle.url(forResource: "payload-base", withExtension: "js") {
|
||||
let patchData = try! Data(contentsOf: patchPath)
|
||||
os_log("Executing payload base", log: bunnyLog, 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: bunnyLog, type: .info,
|
||||
loaderConfig.customLoadUrl.url.absoluteString)
|
||||
bundleUrl = loaderConfig.customLoadUrl.url
|
||||
} else {
|
||||
bundleUrl = URL(
|
||||
string: "https://raw.githubusercontent.com/pyoncord/bunny-builds/main/bunny.js")!
|
||||
}
|
||||
|
||||
os_log("Fetching JS bundle", log: bunnyLog, 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: bunnyLog, 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: bunnyLog, 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: bunnyLog, type: .error)
|
||||
}
|
||||
}
|
||||
|
||||
if bundle != nil {
|
||||
os_log("Executing JS bundle", log: bunnyLog, type: .info)
|
||||
orig.executeApplicationScript(bundle!, url: source, async: async)
|
||||
} else {
|
||||
os_log("Unable to fetch JS bundle", log: bunnyLog, type: .error)
|
||||
}
|
||||
|
||||
os_log("Executing original script", log: bunnyLog, type: .info)
|
||||
orig.executeApplicationScript(script, url: url, async: async)
|
||||
}
|
||||
}
|
||||
|
||||
struct BunnyTweak: 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/BunnyTweak/Utils.x.swift
Normal file
16
Sources/BunnyTweak/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/BunnyTweakC/Tweak.m
Normal file
11
Sources/BunnyTweakC/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
|
||||
|
||||
- (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async;
|
||||
@end
|
||||
|
||||
@end
|
||||
3
Sources/BunnyTweakC/include/Tweak.h
Normal file
3
Sources/BunnyTweakC/include/Tweak.h
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#import "RCTCxxBridge.h"
|
||||
|
||||
const char * const _Nonnull get_install_prefix(void);
|
||||
4
Sources/BunnyTweakC/include/module.modulemap
Normal file
4
Sources/BunnyTweakC/include/module.modulemap
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
module BunnyTweakC {
|
||||
umbrella "."
|
||||
export *
|
||||
}
|
||||
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"
|
||||
|
|
@ -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,46 +1,58 @@
|
|||
{
|
||||
"name": "Revenge App Repo",
|
||||
"identifier": "io.github.revenge-mod.repo",
|
||||
"iconURL": "https://avatars.githubusercontent.com/u/159218871?s=200&v=4",
|
||||
"name": "Pyoncord App Repo",
|
||||
"identifier": "io.github.pyoncord.repo",
|
||||
"iconURL": "https://pyoncord.github.io/assets/icon-altstore.png",
|
||||
"apps": [
|
||||
{
|
||||
"name": "Revenge",
|
||||
"name": "Bunny",
|
||||
"bundleIdentifier": "com.hammerandchisel.discord",
|
||||
"developerName": "Cristiandis",
|
||||
"iconURL": "https://avatars.githubusercontent.com/u/159218871?s=200&v=4",
|
||||
"developerName": "Pylix",
|
||||
"iconURL": "https://pyoncord.github.io/assets/icon-altstore.png",
|
||||
"localizedDescription": "A client mod for Discord",
|
||||
"subtitle": "A client mod for Discord",
|
||||
"tintColor": "7289da",
|
||||
"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": "237.0",
|
||||
"date": "2024-07-13",
|
||||
"size": 114323285,
|
||||
"downloadURL": "https://github.com/pyoncord/BunnyTweak/releases/download/v237.0/Bunny.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": "235.0",
|
||||
"date": "2024-06-27",
|
||||
"size": 112173273,
|
||||
"downloadURL": "https://github.com/pyoncord/BunnyTweak/releases/download/v235.0/Bunny.ipa"
|
||||
},
|
||||
{
|
||||
"version": "250.0",
|
||||
"date": "2024-10-17",
|
||||
"size": 101218840,
|
||||
"downloadURL": "https://github.com/revenge-mod/revenge-tweak/releases/download/v250.0/Revenge.ipa"
|
||||
"version": "234.0",
|
||||
"date": "2024-06-24",
|
||||
"size": 111671815,
|
||||
"downloadURL": "https://github.com/pyoncord/BunnyTweak/releases/download/v234.0/Bunny.ipa"
|
||||
},
|
||||
{
|
||||
"version": "246.0",
|
||||
"date": "2024-09-15",
|
||||
"size": 102811465,
|
||||
"downloadURL": "https://github.com/revenge-mod/RevengeTweak/releases/download/v246.0/Revenge.ipa"
|
||||
"version": "230.0",
|
||||
"date": "2024-05-18",
|
||||
"size": 102457139,
|
||||
"downloadURL": "https://github.com/pyoncord/BunnyTweak/releases/download/v230.0/Bunny.ipa"
|
||||
},
|
||||
{
|
||||
"version": "246.0",
|
||||
"date": "2024-09-14",
|
||||
"size": 103194076,
|
||||
"downloadURL": "https://github.com/revenge-mod/RevengeTweak/releases/download/v246.0/Revenge.ipa"
|
||||
"version": "229.0",
|
||||
"date": "2024-05-16",
|
||||
"size": 103776118,
|
||||
"downloadURL": "https://github.com/pyoncord/BunnyTweak/releases/download/v229.0/Bunny.ipa"
|
||||
},
|
||||
{
|
||||
"version": "226.1",
|
||||
"date": "2024-04-30",
|
||||
"size": 102705408,
|
||||
"downloadURL": "https://github.com/pyoncord/BunnyTweak/releases/download/v226.1/Pyoncord.ipa"
|
||||
},
|
||||
{
|
||||
"version": "223.0",
|
||||
"date": "2024-04-08",
|
||||
"size": 106082919,
|
||||
"downloadURL": "https://github.com/pyoncord/BunnyTweak/releases/download/v223.0/Pyoncord.ipa"
|
||||
}
|
||||
],
|
||||
"appPermissions": {}
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
<svg width="320" height="320" viewBox="0 0 320 320" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.26979 99H294.723L312.234 215.273C312.723 222.597 310.021 229.993 304.839 235.184L265.514 274.935C260.741 279.717 254.27 283.273 247.514 283.273H182.785C168.723 283.273 152.981 270.126 152.981 256.073V255.61C152.981 250.056 156.895 244.851 160.088 240.605L160.088 240.605L160.288 240.339L75.9944 236.224C73.9944 236.135 69.0077 236.055 62.9899 235.984C43.5233 235.762 27.1144 221.37 24.4033 202.09L8.74994 103.006C8.84706 101.648 9.02178 100.311 9.26979 99ZM84.5458 137.592L139.983 152.446C139.994 152.788 140 153.132 140 153.477C140 170.045 126.568 183.477 110 183.477C93.4314 183.477 79.9999 170.045 79.9999 153.477C79.9999 147.643 81.665 142.198 84.5458 137.592ZM180.019 152.4L235.429 137.553C238.325 142.168 240 147.627 240 153.477C240 170.045 226.568 183.477 210 183.477C193.431 183.477 180 170.045 180 153.477C180 153.116 180.006 152.757 180.019 152.4Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M288.328 82.0258C288.569 83.4188 289.198 84.7073 290.018 85.8593C292.71 89.6425 294.372 94.0776 294.688 98.766L295.899 106.809C296.628 111.647 292.881 116 287.988 116H17.6382C13.7018 116 10.3504 113.136 9.73617 109.248L8.89299 103.911C8.79798 103.31 8.76815 102.697 8.82742 102.091C10.4271 85.7347 23.3039 72.6711 39.7278 70.9082L87.1856 63.328C89.9772 62.8821 92.7967 63.9426 94.6025 66.1175L105.915 79.7431C107.719 81.9152 110.533 82.9759 113.322 82.5343L203.656 68.2267C205.851 67.8792 207.802 66.6347 209.042 64.7913L222.878 44.2296C224.116 42.3892 226.063 41.1457 228.253 40.7958L247.639 37.6994C265.683 35.7705 282.181 48.0371 285.523 65.8683L288.328 82.0258Z" fill="white"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,4 +0,0 @@
|
|||
<svg width="320" height="320" viewBox="0 0 320 320" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.26979 99H294.723L312.234 215.273C312.723 222.597 310.021 229.993 304.839 235.184L265.514 274.935C260.741 279.717 254.27 283.273 247.514 283.273H182.785C168.723 283.273 152.981 270.126 152.981 256.073V255.61C152.981 250.056 156.895 244.851 160.088 240.605L160.088 240.605L160.288 240.339L75.9944 236.224C73.9944 236.135 69.0077 236.055 62.9899 235.984C43.5233 235.762 27.1144 221.37 24.4033 202.09L8.74994 103.006C8.84706 101.648 9.02178 100.311 9.26979 99ZM84.5458 137.592L139.983 152.446C139.994 152.788 140 153.132 140 153.477C140 170.045 126.568 183.477 110 183.477C93.4314 183.477 79.9999 170.045 79.9999 153.477C79.9999 147.643 81.665 142.198 84.5458 137.592ZM180.019 152.4L235.429 137.553C238.325 142.168 240 147.627 240 153.477C240 170.045 226.568 183.477 210 183.477C193.431 183.477 180 170.045 180 153.477C180 153.116 180.006 152.757 180.019 152.4Z" fill="black"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M288.328 82.0258C288.569 83.4188 289.198 84.7073 290.018 85.8593C292.71 89.6425 294.372 94.0776 294.688 98.766L295.899 106.809C296.628 111.647 292.881 116 287.988 116H17.6382C13.7018 116 10.3504 113.136 9.73617 109.248L8.89299 103.911C8.79798 103.31 8.76815 102.697 8.82742 102.091C10.4271 85.7347 23.3039 72.6711 39.7278 70.9082L87.1856 63.328C89.9772 62.8821 92.7967 63.9426 94.6025 66.1175L105.915 79.7431C107.719 81.9152 110.533 82.9759 113.322 82.5343L203.656 68.2267C205.851 67.8792 207.802 66.6347 209.042 64.7913L222.878 44.2296C224.116 42.3892 226.063 41.1457 228.253 40.7958L247.639 37.6994C265.683 35.7705 282.181 48.0371 285.523 65.8683L288.328 82.0258Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 19 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 19 KiB |
16
control
16
control
|
|
@ -1,11 +1,11 @@
|
|||
Package: io.github.revenge-mod.app
|
||||
Name: Revenge
|
||||
Version: 2.0.1
|
||||
Package: io.github.pyoncord.app
|
||||
Name: Bunny
|
||||
Version: 0.3.1
|
||||
Architecture: iphoneos-arm
|
||||
Description: A mod for Discord's mobile apps.
|
||||
Maintainer: Cristiandis
|
||||
Author: Pylix, Adrian Castro, FieryFlames, Cristiandis
|
||||
Icon: https://avatars.githubusercontent.com/u/159218871
|
||||
Maintainer: Pylix
|
||||
Author: Pylix, FieryFlames
|
||||
Icon: https://avatars.githubusercontent.com/u/132727175
|
||||
Section: Tweaks
|
||||
Depends: mobilesubstrate (>= 0.9.5000)
|
||||
Conflicts: app.enmity, mod.vendetta, app.bunny
|
||||
Depends: ${ORION}, firmware (>= 13.0)
|
||||
Conflicts: app.enmity, mod.vendetta
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue