├── .clang-format ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ ├── ci.yml │ ├── remote-deploy.yml │ └── update-submodules.yml ├── .gitignore ├── .gitmodules ├── .vscode ├── AirDrop.shortcut ├── extensions.json ├── logos-format.py ├── settings.json └── tasks.json ├── CODEOWNERS ├── LICENSE ├── Makefile ├── README.md ├── Unbound.plist ├── app-repo.json ├── build-local.sh ├── control ├── flake.lock ├── flake.nix ├── headers ├── DeviceModels.h ├── Discord │ └── RCT.h ├── FileSystem.h ├── Fonts.h ├── Logger.h ├── Misc.h ├── NativeBridge.h ├── Plugins.h ├── Recovery.h ├── Settings.h ├── Themes.h ├── Unbound.h ├── Updater.h └── Utilities.h └── sources ├── FileSystem.m ├── Fonts.x ├── Logger.m ├── Misc.x ├── NativeBridge.x ├── Plugins.m ├── Recovery.x ├── Settings.m ├── Themes.x ├── Unbound.x ├── Updater.m ├── Utilities.m └── preload.js /.clang-format: -------------------------------------------------------------------------------- 1 | # Base Style 2 | BasedOnStyle: LLVM 3 | Language: ObjC 4 | 5 | # Indentation 6 | IndentWidth: 4 7 | ObjCBlockIndentWidth: 4 8 | UseTab: Never 9 | ColumnLimit: 100 10 | AccessModifierOffset: -4 11 | IndentCaseLabels: true 12 | NamespaceIndentation: Inner 13 | 14 | # Alignment 15 | AlignConsecutiveAssignments: Consecutive 16 | AlignConsecutiveDeclarations: Consecutive 17 | AlignConsecutiveMacros: Consecutive 18 | AlignTrailingComments: true 19 | AlignOperands: true 20 | AlignAfterOpenBracket: Align 21 | 22 | # Objective-C specific 23 | ObjCSpaceAfterProperty: true 24 | ObjCSpaceBeforeProtocolList: true 25 | ObjCBreakBeforeNestedBlockParam: true 26 | ObjCBinPackProtocolList: Never 27 | 28 | # Breaking and wrapping 29 | AllowShortBlocksOnASingleLine: Empty 30 | AllowShortFunctionsOnASingleLine: Empty 31 | AllowShortCaseLabelsOnASingleLine: false 32 | AllowShortEnumsOnASingleLine: false 33 | AllowShortIfStatementsOnASingleLine: Never 34 | AllowShortLambdasOnASingleLine: Empty 35 | AllowShortLoopsOnASingleLine: false 36 | BreakBeforeBraces: Custom 37 | BraceWrapping: 38 | AfterObjCDeclaration: true 39 | AfterClass: true 40 | AfterControlStatement: true 41 | AfterEnum: true 42 | AfterFunction: true 43 | BeforeCatch: true 44 | BeforeElse: true 45 | IndentBraces: false 46 | 47 | # Spaces 48 | SpaceAfterCStyleCast: true 49 | SpaceBeforeParens: ControlStatements 50 | SpaceInEmptyParentheses: false 51 | SpacesInCStyleCastParentheses: false 52 | SpacesInContainerLiterals: true 53 | SpacesInParentheses: false 54 | SpacesInSquareBrackets: false 55 | 56 | # Import ordering 57 | SortIncludes: true 58 | IncludeBlocks: Regroup 59 | IncludeCategories: 60 | # System headers with brackets 61 | - Regex: "^<" 62 | Priority: 1 63 | # Main header (matching filename) 64 | - Regex: '^"[^/]*\.h"$' 65 | Priority: 2 66 | # Local/Project headers 67 | - Regex: '^"' 68 | Priority: 3 69 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Unbound 2 | run-name: ${{ inputs.release == true && 'Release' || 'Build' }} for ${{ inputs.ipa_url }} 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | ipa_url: 8 | default: "" 9 | description: "Direct link to the decrypted ipa" 10 | required: true 11 | type: string 12 | release: 13 | default: true 14 | description: "Create a GitHub release" 15 | type: boolean 16 | is_testflight: 17 | default: false 18 | description: "This is a TestFlight build" 19 | type: boolean 20 | add_extensions: 21 | default: true 22 | description: "Include extensions (OpenInDiscord & ShareToDiscord)" 23 | type: boolean 24 | workflow_call: 25 | inputs: 26 | ipa_url: 27 | default: "" 28 | type: string 29 | release: 30 | default: true 31 | type: boolean 32 | is_testflight: 33 | default: false 34 | type: boolean 35 | add_extensions: 36 | default: true 37 | type: boolean 38 | caller_workflow: 39 | type: string 40 | outputs: 41 | deb_url: 42 | description: "Download URL for the deb package artifact" 43 | value: ${{ jobs.build-tweak.outputs.deb_url }} 44 | ipa_url: 45 | description: "Download URL for the ipa file artifact" 46 | value: ${{ jobs.build-ipa.outputs.ipa_url }} 47 | deb_filename: 48 | description: "Filename of the deb package" 49 | value: ${{ jobs.build-tweak.outputs.deb_filename }} 50 | ipa_filename: 51 | description: "Filename of the ipa file" 52 | value: ${{ jobs.build-ipa.outputs.ipa_filename }} 53 | 54 | permissions: 55 | contents: write 56 | 57 | env: 58 | GH_TOKEN: ${{ github.token }} 59 | 60 | jobs: 61 | build-tweak: 62 | runs-on: macos-15 63 | outputs: 64 | deb_url: ${{ steps.upload-deb.outputs.artifact-url }} 65 | deb_filename: ${{ steps.set-deb-filename.outputs.filename }} 66 | 67 | env: 68 | DEB_DOWNLOADED: false 69 | 70 | steps: 71 | - name: Checkout code 72 | uses: actions/checkout@v4 73 | with: 74 | submodules: recursive 75 | 76 | - name: Download Tweak 77 | if: inputs.caller_workflow != 'ci' 78 | uses: nick-fields/retry@v3 79 | with: 80 | timeout_minutes: 5 81 | max_attempts: 3 82 | retry_wait_seconds: 10 83 | command: | 84 | set +e 85 | 86 | release_info=$(gh api --header 'Accept: application/vnd.github+json' repos/${{ github.repository }}/releases/latest) 87 | status_code=$(echo $release_info | jq -r ".status") 88 | 89 | if [ "$status_code" != "null" ]; then 90 | echo "No releases found or request failed, status code: $status_code" 91 | echo "DEB_DOWNLOADED=false" >> $GITHUB_ENV 92 | exit 0 93 | fi 94 | 95 | set -e 96 | 97 | release_version=$(echo "$release_info" | jq -r '.assets[] | select(.name | contains("iphoneos-arm64.deb")) | .name' | grep -o '_[0-9.]\+_' | tr -d '_') 98 | control_version=$(grep '^Version:' control | cut -d ' ' -f 2) 99 | 100 | if [ "$release_version" = "$control_version" ]; then 101 | echo "Versions match. Downloading DEB files..." 102 | mkdir -p packages 103 | cd packages 104 | echo "$release_info" | jq -r '.assets[] | select(.name | endswith("arm64.deb")) | .browser_download_url' | xargs -I {} curl -L -O {} 105 | echo "DEB_DOWNLOADED=true" >> $GITHUB_ENV 106 | else 107 | echo "Versions do not match. No files will be downloaded." 108 | echo "DEB_DOWNLOADED=false" >> $GITHUB_ENV 109 | exit 0 110 | fi 111 | 112 | - name: Check cache 113 | if: env.DEB_DOWNLOADED == 'false' 114 | 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 115 | 116 | - name: Use cache 117 | if: env.DEB_DOWNLOADED == 'false' 118 | id: cache 119 | uses: actions/cache@v4 120 | with: 121 | path: ${{ github.workspace }}/theos 122 | key: ${{ runner.os }}-${{ env.upstream_heads }} 123 | 124 | - name: Prepare Theos 125 | if: env.DEB_DOWNLOADED == 'false' 126 | uses: Randomblock1/theos-action@v1 127 | 128 | - name: Build package 129 | if: env.DEB_DOWNLOADED == 'false' 130 | run: gmake package 131 | 132 | - name: Upload rootless package 133 | id: upload-deb 134 | uses: actions/upload-artifact@v4 135 | with: 136 | name: rootless package 137 | path: packages/*.deb 138 | 139 | - name: Set deb filename 140 | id: set-deb-filename 141 | run: | 142 | DEB_FILE=$(ls packages/*.deb) 143 | DEB_FILENAME=$(basename "$DEB_FILE") 144 | echo "filename=$DEB_FILENAME" >> $GITHUB_OUTPUT 145 | 146 | build-ipa: 147 | runs-on: macos-15 148 | needs: build-tweak 149 | outputs: 150 | ipa_url: ${{ steps.upload-ipa.outputs.artifact-url }} 151 | ipa_filename: ${{ steps.set-ipa-filename.outputs.filename }} 152 | 153 | steps: 154 | - name: Checkout code 155 | uses: actions/checkout@v4 156 | with: 157 | submodules: recursive 158 | 159 | - name: Download build artifacts 160 | uses: actions/download-artifact@v4 161 | with: 162 | merge-multiple: true 163 | 164 | - name: Download Discord ipa 165 | uses: nick-fields/retry@v3 166 | with: 167 | timeout_minutes: 10 168 | max_attempts: 3 169 | retry_wait_seconds: 15 170 | command: curl -L -o discord.ipa ${{ inputs.ipa_url }} 171 | 172 | - name: Setup Go 173 | uses: actions/setup-go@v5 174 | with: 175 | cache: false 176 | go-version: 'stable' 177 | 178 | - name: Clone patcher 179 | uses: actions/checkout@v4 180 | with: 181 | repository: unbound-app/patcher-ios 182 | path: patcher-ios 183 | 184 | - name: Build and run patcher 185 | run: | 186 | cd patcher-ios 187 | go build -o patcher 188 | ./patcher -i ../discord.ipa -o ../patched.ipa 189 | 190 | - name: Update ShareToDiscord Info.plist 191 | if: inputs.add_extensions == true 192 | run: | 193 | INFO_PLIST="extensions/ShareToDiscord/Share/Info.plist" 194 | /usr/libexec/PlistBuddy -c "Set :URLScheme unbound" "$INFO_PLIST" 2>/dev/null || \ 195 | /usr/libexec/PlistBuddy -c "Add :URLScheme string unbound" "$INFO_PLIST" 196 | echo "Updated ShareToDiscord Info.plist - Changed URLScheme to 'unbound'" 197 | 198 | - name: Build Extensions 199 | if: inputs.add_extensions == true 200 | run: | 201 | if [ ${{ inputs.is_testflight }} == true ]; then 202 | SAFARI_EXT_BUNDLE_ID="com.hammerandchisel.discord.testflight.OpenInDiscord" 203 | SHARE_EXT_BUNDLE_ID="com.hammerandchisel.discord.testflight.Share" 204 | else 205 | SAFARI_EXT_BUNDLE_ID="com.hammerandchisel.discord.OpenInDiscord" 206 | SHARE_EXT_BUNDLE_ID="com.hammerandchisel.discord.Share" 207 | fi 208 | 209 | cd extensions/OpenInDiscord 210 | xcodebuild build \ 211 | -target "OpenInDiscord Extension" \ 212 | -configuration Release \ 213 | -sdk iphoneos \ 214 | CONFIGURATION_BUILD_DIR="build" \ 215 | PRODUCT_NAME="OpenInDiscord" \ 216 | PRODUCT_BUNDLE_IDENTIFIER="$SAFARI_EXT_BUNDLE_ID" \ 217 | PRODUCT_MODULE_NAME="OpenInDiscordExt" \ 218 | SKIP_INSTALL=NO \ 219 | DEVELOPMENT_TEAM="" \ 220 | CODE_SIGN_IDENTITY="" \ 221 | CODE_SIGNING_REQUIRED=NO \ 222 | CODE_SIGNING_ALLOWED=NO \ 223 | ONLY_ACTIVE_ARCH=NO | xcbeautify 224 | 225 | cd ../ShareToDiscord 226 | xcodebuild build \ 227 | -target "Share" \ 228 | -configuration Release \ 229 | -sdk iphoneos \ 230 | CONFIGURATION_BUILD_DIR="build" \ 231 | PRODUCT_NAME="Share" \ 232 | PRODUCT_BUNDLE_IDENTIFIER="$SHARE_EXT_BUNDLE_ID" \ 233 | PRODUCT_MODULE_NAME="Share" \ 234 | SKIP_INSTALL=NO \ 235 | DEVELOPMENT_TEAM="" \ 236 | CODE_SIGN_IDENTITY="" \ 237 | CODE_SIGNING_REQUIRED=NO \ 238 | CODE_SIGNING_ALLOWED=NO \ 239 | ONLY_ACTIVE_ARCH=NO | xcbeautify 240 | cd ../../ 241 | 242 | - name: Extract app name 243 | run: | 244 | NAME=$(grep '^Name:' control | cut -d ' ' -f 2) 245 | echo "APP_NAME=$NAME" >> $GITHUB_ENV 246 | 247 | - name: Install cyan 248 | run: pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip Pillow 249 | 250 | - name: Inject tweak (and extensions) 251 | run: | 252 | if [ ${{ inputs.add_extensions }} == true ]; then 253 | if [ ${{ inputs.is_testflight }} == true ]; then 254 | cyan -duwsgq -b "com.hammerandchisel.discord.testflight" -i discord.ipa -o ${{ env.APP_NAME }}.ipa -f *.deb extensions/OpenInDiscord/build/OpenInDiscord.appex extensions/ShareToDiscord/build/Share.appex 255 | else 256 | cyan -duwsgq -i patched.ipa -o ${{ env.APP_NAME }}.ipa -f *.deb extensions/OpenInDiscord/build/OpenInDiscord.appex extensions/ShareToDiscord/build/Share.appex 257 | fi 258 | else 259 | if [ ${{ inputs.is_testflight }} == true ]; then 260 | cyan -duwsgq -b "com.hammerandchisel.discord.testflight" -i discord.ipa -o ${{ env.APP_NAME }}.ipa -f *.deb 261 | else 262 | cyan -duwsgq -i patched.ipa -o ${{ env.APP_NAME }}.ipa -f *.deb 263 | fi 264 | fi 265 | 266 | - name: Upload ipa as artifact 267 | id: upload-ipa 268 | uses: actions/upload-artifact@v4 269 | with: 270 | name: ipa 271 | path: ${{ env.APP_NAME }}.ipa 272 | 273 | - name: Set ipa filename 274 | id: set-ipa-filename 275 | run: echo "filename=${{ env.APP_NAME }}.ipa" >> $GITHUB_OUTPUT 276 | 277 | release-app: 278 | if: inputs.caller_workflow != 'ci' && inputs.release == true 279 | runs-on: macos-15 280 | needs: build-ipa 281 | 282 | steps: 283 | - name: Checkout code 284 | uses: actions/checkout@v4 285 | 286 | - name: Download build artifacts 287 | uses: actions/download-artifact@v4 288 | with: 289 | merge-multiple: true 290 | 291 | - name: Extract Discord Version 292 | run: | 293 | unzip -q *.ipa 294 | VERSION=$(plutil -p Payload/Discord.app/Info.plist | grep CFBundleShortVersionString | cut -d '"' -f 4) 295 | 296 | if [[ ${{ inputs.is_testflight }} == true ]]; then 297 | BUILD=$(plutil -p Payload/Discord.app/Info.plist | grep CFBundleVersion | cut -d '"' -f 4) 298 | VERSION="${VERSION}_${BUILD}" 299 | fi 300 | 301 | echo "DISCORD_VERSION=$VERSION" >> $GITHUB_ENV 302 | 303 | - name: Create GitHub Release 304 | id: create_release 305 | uses: softprops/action-gh-release@v2 306 | with: 307 | tag_name: v${{ env.DISCORD_VERSION }} 308 | files: | 309 | *.deb 310 | *.ipa 311 | generate_release_notes: true 312 | prerelease: ${{ inputs.is_testflight }} 313 | fail_on_unmatched_files: true 314 | token: ${{ env.GITHUB_TOKEN }} 315 | env: 316 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 317 | 318 | app-repo: 319 | if: inputs.caller_workflow != 'ci' && inputs.release == true 320 | continue-on-error: true 321 | runs-on: macos-15 322 | needs: release-app 323 | steps: 324 | - name: Checkout code 325 | uses: actions/checkout@v4 326 | 327 | - name: Download ipa artifact 328 | uses: actions/download-artifact@v4 329 | with: 330 | name: ipa 331 | 332 | - name: Update app-repo.json 333 | run: | 334 | APP_FILE=$(ls *.ipa) 335 | unzip -q "$APP_FILE" 336 | VERSION=$(plutil -p Payload/Discord.app/Info.plist | grep CFBundleShortVersionString | cut -d '"' -f 4) 337 | 338 | if [[ ${{ inputs.is_testflight }} == true ]]; then 339 | BUILD=$(plutil -p Payload/Discord.app/Info.plist | grep CFBundleVersion | cut -d '"' -f 4) 340 | VERSION="${VERSION}_${BUILD}" 341 | APP_INDEX=1 342 | else 343 | APP_INDEX=0 344 | fi 345 | 346 | NAME=$(grep '^Name:' control | cut -d ' ' -f 2) 347 | DATE=$(date -u +"%Y-%m-%d") 348 | IPA_SIZE=$(stat -f %z "$APP_FILE") 349 | DOWNLOAD_URL=https://github.com/${{ github.repository }}/releases/download/v$VERSION/$NAME.ipa 350 | NEW_ENTRY=$(jq -n --arg version "$VERSION" --arg date "$DATE" --arg size "$IPA_SIZE" --arg downloadURL "$DOWNLOAD_URL" '{version: $version, date: $date, size: ($size | tonumber), downloadURL: $downloadURL, localizedDescription: "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience."}') 351 | 352 | VERSION_EXISTS=$(jq --arg version "$VERSION" --argjson index "$APP_INDEX" '.apps[$index].versions | map(select(.version == $version)) | length' app-repo.json) 353 | if [ "$VERSION_EXISTS" -gt 0 ]; then 354 | jq --argjson newEntry "$NEW_ENTRY" --argjson index "$APP_INDEX" --arg version "$VERSION" '.apps[$index].versions |= map(if .version == $version then $newEntry else . end)' app-repo.json > temp.json 355 | else 356 | jq --argjson newEntry "$NEW_ENTRY" --argjson index "$APP_INDEX" '.apps[$index].versions |= [$newEntry] + .' app-repo.json > temp.json 357 | fi 358 | mv temp.json app-repo.json 359 | 360 | - uses: EndBug/add-and-commit@v9 361 | with: 362 | default_author: github_actions 363 | message: "chore: update app-repo.json" 364 | add: app-repo.json 365 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request Build 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened, ready_for_review] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | fetch-latest-ipa: 13 | name: Get latest stable ipa 14 | runs-on: ubuntu-latest 15 | outputs: 16 | ipa_url: ${{ steps.get-latest-ipa.outputs.ipa_url }} 17 | steps: 18 | - name: Get latest Discord ipa 19 | id: get-latest-ipa 20 | run: | 21 | RESPONSE=$(curl -s -H "Accept: application/json" https://ipa.aspy.dev/discord/stable/) 22 | LATEST_IPA=$(echo $RESPONSE | jq -r '.[-1].url' | sed 's/^\.\///') 23 | FULL_URL="https://ipa.aspy.dev/discord/stable/$LATEST_IPA" 24 | echo "ipa_url=$FULL_URL" >> $GITHUB_OUTPUT 25 | 26 | build: 27 | name: Build Unbound 28 | needs: fetch-latest-ipa 29 | uses: ./.github/workflows/build.yml 30 | with: 31 | ipa_url: ${{ needs.fetch-latest-ipa.outputs.ipa_url }} 32 | release: false 33 | caller_workflow: "ci" 34 | secrets: inherit 35 | 36 | comment-pr: 37 | name: Comment on PR 38 | runs-on: ubuntu-latest 39 | needs: build 40 | if: github.event_name == 'pull_request' 41 | steps: 42 | - name: Checkout code 43 | uses: actions/checkout@v4 44 | with: 45 | ref: ${{ github.event.pull_request.head.sha }} 46 | fetch-depth: 1 47 | 48 | - name: Delete existing comment 49 | uses: izhangzhihao/delete-comment@master 50 | with: 51 | github_token: ${{ secrets.GITHUB_TOKEN }} 52 | delete_user_name: github-actions[bot] 53 | issue_number: ${{ github.event.number }} 54 | 55 | - name: Get build info 56 | id: build-info 57 | run: | 58 | COMMIT_HASH=$(git rev-parse --short HEAD) 59 | BUILD_TIME=$(date "+%Y-%m-%d %H:%M:%S") 60 | 61 | echo "hash=$COMMIT_HASH" >> $GITHUB_OUTPUT 62 | echo "time=$BUILD_TIME" >> $GITHUB_OUTPUT 63 | 64 | - name: Comment on current PR 65 | uses: thollander/actions-comment-pull-request@v3 66 | with: 67 | message: | 68 | **Commit**: [#${{ steps.build-info.outputs.hash }}](https://github.com/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha }}) 69 | **Build Time**: `${{ steps.build-info.outputs.time }}` 70 | 71 | #### Artifacts: 72 | - [**${{ needs.build.outputs.deb_filename }}**](${{ needs.build.outputs.deb_url }}) 73 | - [**${{ needs.build.outputs.ipa_filename }}**](${{ needs.build.outputs.ipa_url }}) 74 | 75 | This comment was automatically generated. [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) 76 | comment-tag: build-result 77 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/remote-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Remote ipa update 2 | 3 | on: 4 | repository_dispatch: 5 | types: [ipa-update] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | trigger-deploy: 12 | uses: ./.github/workflows/build.yml 13 | with: 14 | ipa_url: ${{ github.event.client_payload.ipa_url }} 15 | is_testflight: ${{ github.event.client_payload.is_testflight }} 16 | secrets: inherit 17 | -------------------------------------------------------------------------------- /.github/workflows/update-submodules.yml: -------------------------------------------------------------------------------- 1 | name: Update Submodules 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | update-submodules: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | with: 19 | submodules: recursive 20 | 21 | - name: Configure Git 22 | run: | 23 | git config --global user.name 'github-actions[bot]' 24 | git config --global user.email 'github-actions[bot]@users.noreply.github.com' 25 | 26 | - name: Update submodules 27 | run: | 28 | git submodule update --init --recursive --remote --force 29 | git add . 30 | git commit -m "chore: updated submodules" || echo "No changes to commit" 31 | git push origin || echo "No changes to push" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.ipa 2 | .theos 3 | .DS_Store 4 | packages 5 | binaries 6 | venv 7 | patcher-ios 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "resources"] 2 | path = resources 3 | url = https://github.com/unbound-app/bootstrap 4 | [submodule "extensions/OpenInDiscord"] 5 | path = extensions/OpenInDiscord 6 | url = https://github.com/castdrian/OpenInDiscord 7 | [submodule "extensions/ShareToDiscord"] 8 | path = extensions/ShareToDiscord 9 | url = https://github.com/castdrian/ShareToDiscord 10 | -------------------------------------------------------------------------------- /.vscode/AirDrop.shortcut: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unbound-app/loader-ios/ddf352518ee3394e80733cfdb1832358ab6162fb/.vscode/AirDrop.shortcut -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "tale.logos-vscode", 4 | "spencerwmiles.vscode-task-buttons", 5 | "redhat.vscode-yaml", 6 | "davidanson.vscode-markdownlint", 7 | "SteefH.external-formatters" 8 | ] 9 | } -------------------------------------------------------------------------------- /.vscode/logos-format.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import re 3 | from subprocess import Popen, PIPE, STDOUT 4 | 5 | # block level 6 | # hook -> replace with @logosformathook with ; at the end 7 | # end -> replace with @logosformatend with ; at the end 8 | # property -> replace with @logosformatproperty with NO ; at the end. Special case for block level 9 | # new -> replce with @logosformatnew with ; at the end 10 | # group -> replace with @logosformatgroup with ; at the end 11 | # subclass -> replace with @logosformatsubclass with ; at the end 12 | # top level 13 | # config -> replace with @logosformatconfig 14 | # hookf -> replace with @logosformathookf 15 | # ctor -> replace with @logosformatctor 16 | # dtor -> replace with @logosformatdtor 17 | 18 | # function level 19 | # init -> replace with @logosformatinit 20 | # c -> replace with @logosformatc 21 | # orig -> replace with @logosformatorig 22 | # log -> replace with @logosformatlog 23 | 24 | specialFilterList = ["%hook", "%end", "%new", "%group", "%subclass"] 25 | filterList = [ 26 | "%property", 27 | "%config", 28 | "%hookf", 29 | "%ctor", 30 | "%dtor", 31 | "%init", 32 | "%c", 33 | "%orig", 34 | "%log", 35 | ] 36 | 37 | 38 | fileContentsList = sys.stdin.read().splitlines() 39 | newList = [] 40 | 41 | for line in fileContentsList: 42 | for token in filterList: 43 | if token in line: 44 | line = re.sub(rf"%({token[1:]})\b", r"@logosformat\1", line) 45 | for token in specialFilterList: 46 | if token in line: 47 | line = re.sub(rf"%({token[1:]})\b", r"@logosformat\1", line) + ";" 48 | newList.append(line) 49 | 50 | command = ["clang-format"] + sys.argv[1:] 51 | process = Popen(command, stdout=PIPE, stderr=None, stdin=PIPE) 52 | stdoutData = process.communicate(input="\n".join(newList).encode())[0] 53 | refinedList = stdoutData.decode().splitlines() 54 | 55 | 56 | for line in refinedList: 57 | if "@logosformat" in line: 58 | fix = line.replace("@logosformat", "%") 59 | if any(token in fix for token in specialFilterList): 60 | print(fix.replace(";","")) 61 | else: 62 | print(fix) 63 | else: 64 | print(line) 65 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdownlint.config": { 3 | "MD029": false, 4 | "MD033": false, 5 | "MD045": false, 6 | }, 7 | "VsCodeTaskButtons.tasks": [ 8 | { 9 | "label": "$(tools) Build Tweak", 10 | "task": "Build Tweak", 11 | "tooltip": "Build Tweak" 12 | }, 13 | { 14 | "label": "$(cloud-upload) AirDrop Tweak", 15 | "task": "AirDrop Tweak", 16 | "tooltip": "AirDrop Tweak" 17 | }, 18 | { 19 | "label": "$(package) Build IPA", 20 | "task": "Build IPA", 21 | "tooltip": "Build IPA locally" 22 | } 23 | ], 24 | "externalFormatters.languages": { 25 | "logos": { 26 | "command": "python3", 27 | "arguments": [ 28 | "../.vscode/logos-format.py", 29 | "--assume-filename", 30 | "objc" 31 | ] 32 | }, 33 | "objective-c": { 34 | "command": "clang-format", 35 | "arguments": [ 36 | "-style=file" 37 | ] 38 | } 39 | }, 40 | "yaml.schemas": { 41 | "https://json.schemastore.org/github-workflow.json": [ 42 | "*.github/workflows/*.yml", 43 | ] 44 | }, 45 | "editor.formatOnSave": true, 46 | "markdown.validate.enabled": true 47 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "shell", 6 | "label": "Build Tweak", 7 | "command": "zsh", 8 | "args": [ 9 | "-c", 10 | "rm -rf packages && gmake clean package" 11 | ], 12 | "problemMatcher": { 13 | "owner": "cpp", 14 | "pattern": { 15 | "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$", 16 | "file": 1, 17 | "line": 2, 18 | "column": 3, 19 | "severity": 4, 20 | "message": 5 21 | } 22 | }, 23 | "group": { 24 | "kind": "build", 25 | "isDefault": true 26 | }, 27 | "presentation": { 28 | "panel": "shared", 29 | "showReuseMessage": false, 30 | "clear": true, 31 | "close": true 32 | } 33 | }, 34 | { 35 | "type": "shell", 36 | "label": "AirDrop Tweak", 37 | "command": "zsh", 38 | "args": [ 39 | "-c", 40 | "shortcuts run 'AirDrop' -i ./packages/*.deb" 41 | ], 42 | "presentation": { 43 | "panel": "shared", 44 | "showReuseMessage": false, 45 | "clear": true, 46 | "close": true 47 | }, 48 | "dependsOn": ["Build Tweak"] 49 | }, 50 | { 51 | "type": "shell", 52 | "label": "Build IPA", 53 | "command": "chmod +x build-local.sh && ./build-local.sh", 54 | "presentation": { 55 | "panel": "shared", 56 | "showReuseMessage": false, 57 | "clear": true, 58 | "reveal": "always" 59 | } 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @acquitelol @marioparaschiv @castdrian 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | THEOS_PACKAGE_SCHEME=rootless 2 | FINALPACKAGE=1 3 | INSTALL_TARGET_PROCESSES = Discord 4 | 5 | ARCHS := arm64 arm64e 6 | TARGET := iphone:clang:latest:14.0 7 | 8 | include $(THEOS)/makefiles/common.mk 9 | 10 | TWEAK_NAME = Unbound 11 | $(TWEAK_NAME)_FILES = $(shell find sources -name "*.x*" -o -name "*.m*") 12 | $(TWEAK_NAME)_CFLAGS = -fobjc-arc -DPACKAGE_VERSION='@"$(THEOS_PACKAGE_BASE_VERSION)"' -I$(THEOS_PROJECT_DIR)/headers 13 | $(TWEAK_NAME)_FRAMEWORKS = UIKit Foundation UniformTypeIdentifiers 14 | 15 | BUNDLE_NAME = UnboundResources 16 | $(BUNDLE_NAME)_INSTALL_PATH = "/Library/Application\ Support/" 17 | $(BUNDLE_NAME)_RESOURCE_DIRS = "resources" 18 | 19 | include $(THEOS_MAKE_PATH)/tweak.mk 20 | include $(THEOS_MAKE_PATH)/bundle.mk 21 | 22 | before-all:: 23 | @if [ ! -d "resources" ] || [ -z "$$(ls -A resources 2>/dev/null)" ]; then \ 24 | echo "Resources folder empty or missing, initializing submodule..."; \ 25 | git submodule update --init --recursive || exit 1; \ 26 | fi 27 | 28 | $(ECHO_NOTHING)VERSION_NUM=$$(echo "$(THEOS_PACKAGE_BASE_VERSION)" | cut -d'.' -f1,2) && \ 29 | sed "s/VERSION_PLACEHOLDER/$$VERSION_NUM/" sources/preload.js > resources/preload.js$(ECHO_END) 30 | 31 | after-stage:: 32 | $(ECHO_NOTHING)find $(THEOS_STAGING_DIR) -name ".DS_Store" -delete$(ECHO_END) 33 | 34 | after-package:: 35 | $(ECHO_NOTHING)rm resources/preload.js$(ECHO_END) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @unbound-app/loader-ios 2 | 3 | Tweak to inject [Unbound](https://github.com/unbound-app/client) into Discord and perform various utility tasks. 4 | 5 | ## Installation 6 | 7 | Builds can be found in the [Releases](https://github.com/unbound-app/loader-ios/releases/latest) tab. 8 | 9 | ### Jailbroken 10 | 11 | - Add the apt repo to your package manager: 12 | - Install Unbound by downloading the appropriate Debian package (or by building your own, see [Building](#building)) and adding it to your package manager. 13 | 14 | ### Jailed 15 | 16 | 17 | 18 | 19 | 20 | > [!WARNING] 21 | > Trying to use non-substrate tweak runtimes (such as TrollFools or LiveContainer's TweakLoader) will likely break functionality. Please always use the pre-patched ipa when sideloading. 22 | 23 | - Download and install [Unbound.ipa](https://github.com/unbound-app/loader-ios/releases/latest/download/Unbound.ipa) using your preferred sideloading method. 24 | 25 | ## Building 26 | 27 | > [!NOTE] 28 | > Unless you plan on modifying source code you should fork this repository and use the provided workflow. 29 | 30 |
31 | Instructions 32 | 33 | > These steps assume you use macOS. 34 | 35 | 1. Install Xcode from the App Store. If you've previously installed the `Command Line Utilities` package, you will need to run `sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer` to make sure you're using the Xcode tools instead. 36 | 37 | > If you want to revert the `xcode-select` change, run `sudo xcode-select -switch /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk` 38 | 39 | 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/). 40 | 41 | 3. Setup your gnu make path: 42 | 43 | ```bash 44 | export PATH="$(brew --prefix make)/libexec/gnubin:$PATH" 45 | ``` 46 | 47 | 4. Setup [theos](https://theos.dev/docs/installation-macos) by running the script provided by theos. 48 | 49 | ```bash 50 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/theos/theos/master/bin/install-theos)" 51 | ``` 52 | 53 | If you've already installed theos, you can run `$THEOS/bin/update-theos` to make sure it's up to date. 54 | 55 | 5. Clone this repository via `git clone git@github.com:unbound-app/loader-ios.git` and `cd` into it. 56 | 57 | 6. To build, you can run `make package`. 58 | 59 | The resulting `.deb` file will be in the `packages` folder. 60 | 61 |
62 | 63 | ## Contributors 64 | 65 | [![Contributors](https://contrib.rocks/image?repo=unbound-app/loader-ios)](https://github.com/unbound-app/loader-ios/graphs/contributors) 66 | -------------------------------------------------------------------------------- /Unbound.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "com.hammerandchisel.discord" ); }; } 2 | -------------------------------------------------------------------------------- /app-repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unbound App Repo", 3 | "identifier": "app.unbound.ios.repo", 4 | "iconURL": "https://repo.unbound.rip/icons/Unbound.png", 5 | "apps": [ 6 | { 7 | "name": "Unbound", 8 | "bundleIdentifier": "com.hammerandchisel.discord", 9 | "developerName": "Unbound Team", 10 | "iconURL": "https://assets.unbound.rip/logo/logo.png", 11 | "localizedDescription": "A client mod for Discord", 12 | "subtitle": "A client mod for Discord", 13 | "tintColor": "aa454d", 14 | "category": "social", 15 | "versions": [ 16 | { 17 | "version": "280.0", 18 | "date": "2025-05-21", 19 | "size": 90460070, 20 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v280.0/Unbound.ipa", 21 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 22 | }, 23 | { 24 | "version": "279.0", 25 | "date": "2025-05-14", 26 | "size": 90247340, 27 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v279.0/Unbound.ipa", 28 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 29 | }, 30 | { 31 | "version": "278.1", 32 | "date": "2025-05-14", 33 | "size": 90518423, 34 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v278.1/Unbound.ipa", 35 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 36 | }, 37 | { 38 | "version": "275.0", 39 | "date": "2025-04-17", 40 | "size": 89648900, 41 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v275.0/Unbound.ipa", 42 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 43 | }, 44 | { 45 | "version": "273.0", 46 | "date": "2025-04-17", 47 | "size": 88155972, 48 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v273.0/Unbound.ipa", 49 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 50 | }, 51 | { 52 | "version": "272.0", 53 | "date": "2025-03-26", 54 | "size": 88138834, 55 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v272.0/Unbound.ipa", 56 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 57 | }, 58 | { 59 | "version": "271.0", 60 | "date": "2025-03-18", 61 | "size": 88098309, 62 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v271.0/Unbound.ipa", 63 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 64 | } 65 | ] 66 | }, 67 | { 68 | "name": "Unbound (TestFlight)", 69 | "bundleIdentifier": "com.hammerandchisel.discord.testflight", 70 | "developerName": "Unbound Team", 71 | "beta": true, 72 | "iconURL": "https://assets.unbound.rip/logo/logo.png", 73 | "localizedDescription": "A client mod for Discord (TestFlight builds)", 74 | "subtitle": "A client mod for Discord (TestFlight builds)", 75 | "tintColor": "aa454d", 76 | "category": "social", 77 | "versions": [ 78 | { 79 | "version": "282.0_78067", 80 | "date": "2025-05-27", 81 | "size": 89960843, 82 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v282.0_78067/Unbound.ipa", 83 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 84 | }, 85 | { 86 | "version": "281.0_78025", 87 | "date": "2025-05-25", 88 | "size": 89904786, 89 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v281.0_78025/Unbound.ipa", 90 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 91 | }, 92 | { 93 | "version": "274.0_74656", 94 | "date": "2025-03-30", 95 | "size": 89602414, 96 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v274.0_74656/Unbound.ipa", 97 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 98 | }, 99 | { 100 | "version": "273.0_74481", 101 | "date": "2025-03-26", 102 | "size": 88134498, 103 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v273.0_74481/Unbound.ipa", 104 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 105 | }, 106 | { 107 | "version": "273.0_74387", 108 | "date": "2025-03-26", 109 | "size": 88155886, 110 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v273.0_74387/Unbound.ipa", 111 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 112 | }, 113 | { 114 | "version": "273.0_74288", 115 | "date": "2025-03-24", 116 | "size": 88156846, 117 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v273.0_74288/Unbound.ipa", 118 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 119 | }, 120 | { 121 | "version": "272.0_74225", 122 | "date": "2025-03-22", 123 | "size": 88138964, 124 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v272.0_74225/Unbound.ipa", 125 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 126 | }, 127 | { 128 | "version": "272.0_74108", 129 | "date": "2025-03-21", 130 | "size": 88145825, 131 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v272.0_74108/Unbound.ipa", 132 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 133 | }, 134 | { 135 | "version": "272.0_73751", 136 | "date": "2025-03-19", 137 | "size": 88126076, 138 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v272.0_73751/Unbound.ipa", 139 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 140 | }, 141 | { 142 | "version": "271.0_73724", 143 | "date": "2025-03-15", 144 | "size": 88117558, 145 | "downloadURL": "https://github.com/unbound-app/loader-ios/releases/download/v271.0_73724/Unbound.ipa", 146 | "localizedDescription": "Further improvements to overall system stability and other minor adjustments have been made to enhance the user experience." 147 | } 148 | ] 149 | } 150 | ] 151 | } 152 | -------------------------------------------------------------------------------- /build-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | RED='\033[0;31m' 4 | GREEN='\033[0;32m' 5 | BLUE='\033[0;34m' 6 | NC='\033[0m' 7 | 8 | print_status() { 9 | echo -e "${BLUE}[*]${NC} $1" 10 | } 11 | 12 | print_success() { 13 | echo -e "${GREEN}[+]${NC} $1" 14 | } 15 | 16 | print_error() { 17 | echo -e "${RED}[-]${NC} $1" 18 | } 19 | 20 | if [ ! -f "control" ]; then 21 | print_error "Control file not found. Cannot continue." 22 | exit 1 23 | fi 24 | 25 | NAME=$(grep '^Name:' control | cut -d ' ' -f 2) 26 | if [ -z "$NAME" ]; then 27 | print_error "Package name not found in control file. Cannot continue." 28 | exit 1 29 | fi 30 | 31 | print_status "Building package: $NAME" 32 | 33 | print_status "Initializing submodules..." 34 | git submodule update --init --recursive 35 | if [ $? -ne 0 ]; then 36 | print_error "Failed to initialize submodules" 37 | exit 1 38 | fi 39 | print_success "Initialized submodules" 40 | 41 | IPA_FILE=$(find . -maxdepth 1 -name "*.ipa" -print -quit) 42 | UNAME=$(uname) 43 | 44 | print_status "Build debug version? (y/n):" 45 | read -t 3 DEBUG_INPUT 46 | if [ $? -gt 128 ]; then 47 | echo "n" 48 | DEBUG_ARG="" 49 | print_status "Building release version... (default)" 50 | elif [[ $DEBUG_INPUT =~ ^[Yy]$ ]]; then 51 | DEBUG_ARG="DEBUG=1" 52 | print_status "Building debug version..." 53 | else 54 | DEBUG_ARG="" 55 | print_status "Building release version..." 56 | fi 57 | 58 | USE_EXTENSION=0 59 | if [ "$UNAME" = "Darwin" ]; then 60 | print_status "Include extensions? (y/n):" 61 | read -t 3 EXTENSIONS_INPUT 62 | if [ $? -gt 128 ]; then 63 | echo "y" 64 | USE_EXTENSION=1 65 | print_status "Including extensions... (default)" 66 | elif [[ $EXTENSIONS_INPUT =~ ^[Nn]$ ]]; then 67 | USE_EXTENSION=0 68 | print_status "Skipping extensions..." 69 | else 70 | USE_EXTENSION=1 71 | print_status "Including extensions..." 72 | fi 73 | fi 74 | 75 | if [ -z "$IPA_FILE" ]; then 76 | print_status "No ipa found. Please enter Discord ipa URL or file path:" 77 | read DISCORD_INPUT 78 | 79 | if [ -z "$DISCORD_INPUT" ]; then 80 | print_error "No input provided" 81 | exit 1 82 | fi 83 | 84 | if [[ "$DISCORD_INPUT" =~ ^https?:// ]]; then 85 | print_status "Downloading Discord ipa..." 86 | curl -L -o discord.ipa "$DISCORD_INPUT" 87 | if [ $? -ne 0 ]; then 88 | print_error "Failed to download Discord ipa" 89 | exit 1 90 | fi 91 | print_success "Downloaded Discord ipa" 92 | else 93 | if [ ! -f "$DISCORD_INPUT" ]; then 94 | print_error "File not found: $DISCORD_INPUT" 95 | exit 1 96 | fi 97 | print_status "Copying Discord ipa..." 98 | cp "$DISCORD_INPUT" discord.ipa 99 | if [ $? -ne 0 ]; then 100 | print_error "Failed to copy Discord ipa" 101 | exit 1 102 | fi 103 | print_success "Copied Discord ipa" 104 | fi 105 | IPA_FILE="discord.ipa" 106 | fi 107 | 108 | print_status "Building tweak..." 109 | 110 | if [ "$UNAME" = "Darwin" ]; then 111 | gmake package $DEBUG_ARG 112 | else 113 | make package $DEBUG_ARG 114 | fi 115 | if [ $? -ne 0 ]; then 116 | print_error "Failed to build tweak" 117 | exit 1 118 | fi 119 | print_success "Built tweak" 120 | 121 | if [ ! -f "patcher-ios/patcher" ]; then 122 | print_status "Building patcher..." 123 | rm -rf patcher-ios 124 | git clone https://github.com/unbound-app/patcher-ios 125 | cd patcher-ios 126 | go build -o patcher 127 | cd .. 128 | 129 | if [ $? -ne 0 ]; then 130 | print_error "Failed to build patcher" 131 | exit 1 132 | fi 133 | print_success "Built patcher" 134 | else 135 | print_status "Using existing patcher..." 136 | fi 137 | 138 | OUTPUT_IPA="${NAME}.ipa" 139 | TEMP_PATCHED_IPA="patched.ipa" 140 | 141 | print_status "Patching ipa..." 142 | ./patcher-ios/patcher -i "$IPA_FILE" -o "$TEMP_PATCHED_IPA" 143 | 144 | if [ $? -ne 0 ]; then 145 | print_error "Failed to patch ipa" 146 | exit 1 147 | fi 148 | print_success "Patched ipa" 149 | 150 | EXTENSIONS="" 151 | if [ "$USE_EXTENSION" = "1" ] && [ "$UNAME" = "Darwin" ]; then 152 | # OpenInDiscord Extension 153 | SAFARI_EXT="extensions/OpenInDiscord/build/OpenInDiscord.appex" 154 | 155 | if [ ! -f "$SAFARI_EXT" ]; then 156 | print_status "Building Safari extension..." 157 | mkdir -p extensions/OpenInDiscord/build 158 | cd extensions/OpenInDiscord 159 | xcodebuild build \ 160 | -target "OpenInDiscord Extension" \ 161 | -configuration Release \ 162 | -sdk iphoneos \ 163 | CONFIGURATION_BUILD_DIR="build" \ 164 | PRODUCT_NAME="OpenInDiscord" \ 165 | PRODUCT_BUNDLE_IDENTIFIER="com.hammerandchisel.discord.OpenInDiscord" \ 166 | PRODUCT_MODULE_NAME="OpenInDiscordExt" \ 167 | SKIP_INSTALL=NO \ 168 | DEVELOPMENT_TEAM="" \ 169 | CODE_SIGN_IDENTITY="" \ 170 | CODE_SIGNING_REQUIRED=NO \ 171 | CODE_SIGNING_ALLOWED=NO \ 172 | ONLY_ACTIVE_ARCH=NO | xcbeautify 173 | cd ../.. 174 | 175 | if [ $? -ne 0 ]; then 176 | print_error "Failed to build Safari extension" 177 | exit 1 178 | fi 179 | print_success "Built Safari extension" 180 | else 181 | print_status "Using existing Safari extension..." 182 | fi 183 | 184 | EXTENSIONS="$SAFARI_EXT" 185 | 186 | # ShareToDiscord Extension 187 | SHARE_EXT="extensions/ShareToDiscord/build/Share.appex" 188 | 189 | # Update the Info.plist to change URLScheme from 'discord' to 'unbound' 190 | print_status "Updating ShareToDiscord Info.plist..." 191 | INFO_PLIST_PATH="extensions/ShareToDiscord/Share/Info.plist" 192 | if [ -f "$INFO_PLIST_PATH" ]; then 193 | /usr/libexec/PlistBuddy -c "Set :URLScheme unbound" "$INFO_PLIST_PATH" 2>/dev/null || \ 194 | /usr/libexec/PlistBuddy -c "Add :URLScheme string unbound" "$INFO_PLIST_PATH" 195 | print_success "Updated ShareToDiscord Info.plist" 196 | else 197 | print_error "ShareToDiscord Info.plist not found at $INFO_PLIST_PATH" 198 | fi 199 | 200 | if [ ! -f "$SHARE_EXT" ]; then 201 | print_status "Building Share extension..." 202 | mkdir -p extensions/ShareToDiscord/build 203 | cd extensions/ShareToDiscord 204 | xcodebuild build \ 205 | -target "Share" \ 206 | -configuration Release \ 207 | -sdk iphoneos \ 208 | CONFIGURATION_BUILD_DIR="build" \ 209 | PRODUCT_NAME="Share" \ 210 | PRODUCT_BUNDLE_IDENTIFIER="com.hammerandchisel.discord.Share" \ 211 | PRODUCT_MODULE_NAME="Share" \ 212 | SKIP_INSTALL=NO \ 213 | DEVELOPMENT_TEAM="" \ 214 | CODE_SIGN_IDENTITY="" \ 215 | CODE_SIGNING_REQUIRED=NO \ 216 | CODE_SIGNING_ALLOWED=NO \ 217 | ONLY_ACTIVE_ARCH=NO | xcbeautify 218 | cd ../.. 219 | 220 | if [ $? -ne 0 ]; then 221 | print_error "Failed to build Share extension" 222 | exit 1 223 | fi 224 | print_success "Built Share extension" 225 | else 226 | print_status "Using existing Share extension..." 227 | fi 228 | 229 | EXTENSIONS="$EXTENSIONS $SHARE_EXT" 230 | fi 231 | 232 | if [ ! -d "venv" ] || [ ! -f "venv/bin/cyan" ]; then 233 | print_status "Setting up Python environment..." 234 | [ -d "venv" ] && rm -rf venv 235 | python3 -m venv venv 236 | source venv/bin/activate 237 | pip install --force-reinstall https://github.com/asdfzxcvbn/pyzule-rw/archive/main.zip Pillow 238 | 239 | if [ $? -ne 0 ]; then 240 | print_error "Failed to install cyan" 241 | exit 1 242 | fi 243 | print_success "Installed cyan" 244 | else 245 | print_status "Using existing Python environment..." 246 | source venv/bin/activate 247 | fi 248 | 249 | DEB_FILE=$(find packages -maxdepth 1 -name "*.deb" -print -quit) 250 | 251 | print_status "Injecting tweak..." 252 | if [ "$USE_EXTENSION" = "1" ] && [ -n "$EXTENSIONS" ]; then 253 | cyan -duwsgq -i "$TEMP_PATCHED_IPA" -o "$OUTPUT_IPA" -f "$DEB_FILE" $EXTENSIONS 254 | else 255 | cyan -duwsgq -i "$TEMP_PATCHED_IPA" -o "$OUTPUT_IPA" -f "$DEB_FILE" 256 | fi 257 | 258 | if [ $? -ne 0 ]; then 259 | print_error "Failed to inject tweak" 260 | exit 1 261 | fi 262 | 263 | deactivate 264 | 265 | print_status "Cleaning up..." 266 | rm -rf packages "$TEMP_PATCHED_IPA" 267 | 268 | print_success "Successfully created $OUTPUT_IPA" 269 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Name: Unbound 2 | Description: Discord mobile client aimed at providing the user control, stability and customizability. 3 | Version: 1.4.0 4 | Package: app.unbound.ios 5 | Architecture: iphoneos-arm 6 | Maintainer: eternal, acquitelol, Adrian Castro 7 | Author: Unbound Team 8 | Section: Tweaks 9 | Depends: mobilesubstrate (>= 0.9.5000) 10 | SileoDepiction: https://repo.unbound.rip/depictions/unbound.json 11 | Icon: https://assets.unbound.rip/logo/logo.png 12 | Conflicts: app.enmity, mod.vendetta, app.pyoncord 13 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1739446958, 6 | "narHash": "sha256-+/bYK3DbPxMIvSL4zArkMX0LQvS7rzBKXnDXLfKyRVc=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "2ff53fe64443980e139eaa286017f53f88336dd0", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Theos development environment"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; 6 | }; 7 | 8 | outputs = { 9 | self, 10 | nixpkgs, 11 | }: let 12 | allSystems = [ 13 | "x86_64-linux" # 64bit AMD/Intel x86 14 | ]; 15 | 16 | forAllSystems = fn: 17 | nixpkgs.lib.genAttrs allSystems 18 | (system: fn {pkgs = import nixpkgs {inherit system;};}); 19 | in { 20 | devShells = forAllSystems ({pkgs}: { 21 | default = let 22 | rev = "82af6911de5f8d8bafe98945ed1ae3d4f4537218"; 23 | 24 | llvm-swift = builtins.fetchurl { 25 | url = "https://github.com/CRKatri/llvm-project/releases/download/swift-5.3.2-RELEASE/swift-5.3.2-RELEASE-ubuntu18.04.tar.zst"; 26 | sha256 = "1mhr91n6p0sahqdmlpz4fr539g9rwrwq0k9mx9ikg6gcl4ddjzfk"; 27 | }; 28 | 29 | theos-src = pkgs.fetchgit { 30 | url = "https://github.com/theos/theos"; 31 | rev = rev; 32 | sha256 = "sha256-T6k8XFnSGFrdy1S2JNXZHZGHJ/NpG09uW20G7bdPIv0="; 33 | leaveDotGit = true; 34 | fetchSubmodules = true; 35 | }; 36 | 37 | theos-sdks = pkgs.fetchFromGitHub { 38 | owner = "theos"; 39 | repo = "sdks"; 40 | rev = "bb425abf3acae8eac328b828628b82df544d2774"; 41 | sha256 = "sha256-cZfCEWI+Nuon/cbZLBNpqwGNIbiPg184a0NjblrkaQ4="; 42 | }; 43 | 44 | rpath = pkgs.lib.makeLibraryPath [ 45 | pkgs.gcc-unwrapped.lib 46 | pkgs.glibc 47 | pkgs.libedit 48 | pkgs.ncurses5 49 | pkgs.swift 50 | pkgs.util-linux 51 | pkgs.zlib 52 | ]; 53 | 54 | theos = pkgs.stdenv.mkDerivation { 55 | name = "theos"; 56 | version = rev; 57 | 58 | srcs = [llvm-swift theos-src theos-sdks]; 59 | 60 | nativeBuildInputs = [pkgs.autoPatchelfHook]; 61 | buildInputs = [pkgs.patchelf pkgs.zstd]; 62 | 63 | phases = ["installPhase"]; 64 | 65 | installPhase = '' 66 | mkdir -p $out/share 67 | THEOS=$out/share/theos 68 | # install theos 69 | cp -r --no-preserve=mode,ownership ${theos-src} $THEOS 70 | # install llvm-swift 71 | tar -xf ${llvm-swift} -C $TMPDIR 72 | mkdir -p $THEOS/toolchain/linux/iphone $THEOS/toolchain/swift 73 | mv $TMPDIR/swift-5.3.2-RELEASE-ubuntu18.04/* $THEOS/toolchain/linux/iphone/ 74 | ln -s $THEOS/toolchain/linux/iphone $THEOS/toolchain/swift 75 | # install 14.5 sdk 76 | cp -r --no-preserve=mode,ownership ${theos-sdks}/iPhoneOS14.5.sdk $THEOS/sdks 77 | # mutate perl scripts so they use `/usr/bin/env perl` as a shebang 78 | find $THEOS -type f -name '*.pl' -exec sed -i 's|#!/usr/bin/perl|#!/usr/bin/env perl|g' {} \; 79 | chmod +x $(find $THEOS -type f -name '*.pl') 80 | # mutate bin/bash scripts so they use `/usr/bin/env bash` as a shebang 81 | find $THEOS/bin -type f -exec sed -i 's|#!/bin/bash|#!/usr/bin/env bash|g' {} \; 82 | chmod +x $THEOS/bin/* 83 | # install nic.pl 84 | mkdir -p $out/bin 85 | cat <$out/bin/theos-nic 86 | #!/usr/bin/env bash 87 | perl $THEOS/bin/nic.pl 88 | EOF 89 | chmod +x $out/bin/theos-nic 90 | # patchELF all executables 91 | find $THEOS -type f -executable -exec patchelf --replace-needed libedit.so.2 libedit.so.0 {} \; 92 | find $THEOS -type f -executable -exec patchelf --set-rpath ${rpath} --set-interpreter $(cat $NIX_CC/nix-support/dynamic-linker) {} \; 93 | # manual fix for an annoying bug 94 | cat <$THEOS/toolchain/linux/iphone/bin/ld 95 | #!/bin/sh 96 | LD_LIBRARY_PATH="\$(dirname "\$0")"/../lib:${rpath} "\$(dirname "\$0")"/ld64 "\$@" 97 | EOF 98 | ''; 99 | }; 100 | 101 | # modeled after: https://archlinux.org/groups/x86_64/base-devel/ 102 | base-devel = with pkgs; [ 103 | autoconf 104 | automake 105 | binutils 106 | bison 107 | fakeroot 108 | file 109 | findutils 110 | flex 111 | gawk 112 | gcc 113 | gettext 114 | gnumake 115 | groff 116 | gzip 117 | libtool 118 | m4 119 | patch 120 | pkgconf 121 | texinfo 122 | which 123 | zstd 124 | ]; 125 | in 126 | pkgs.mkShell { 127 | buildInputs = with pkgs; base-devel ++ [git perl unzip ncurses5 theos go]; 128 | shellHook = '' 129 | export THEOS=${theos}/share/theos 130 | ''; 131 | }; 132 | }); 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /headers/DeviceModels.h: -------------------------------------------------------------------------------- 1 | #define DEVICE_MODELS \ 2 | @{ \ 3 | @"i386" : @"iPhone Simulator", \ 4 | @"x86_64" : @"iPhone Simulator", \ 5 | @"arm64" : @"iPhone Simulator", \ 6 | @"iPhone1,1" : @"iPhone", \ 7 | @"iPhone1,2" : @"iPhone 3G", \ 8 | @"iPhone2,1" : @"iPhone 3GS", \ 9 | @"iPhone3,1" : @"iPhone 4", \ 10 | @"iPhone3,2" : @"iPhone 4 GSM Rev A", \ 11 | @"iPhone3,3" : @"iPhone 4 CDMA", \ 12 | @"iPhone4,1" : @"iPhone 4S", \ 13 | @"iPhone5,1" : @"iPhone 5 (GSM)", \ 14 | @"iPhone5,2" : @"iPhone 5 (GSM+CDMA)", \ 15 | @"iPhone5,3" : @"iPhone 5C (GSM)", \ 16 | @"iPhone5,4" : @"iPhone 5C (Global)", \ 17 | @"iPhone6,1" : @"iPhone 5S (GSM)", \ 18 | @"iPhone6,2" : @"iPhone 5S (Global)", \ 19 | @"iPhone7,1" : @"iPhone 6 Plus", \ 20 | @"iPhone7,2" : @"iPhone 6", \ 21 | @"iPhone8,1" : @"iPhone 6s", \ 22 | @"iPhone8,2" : @"iPhone 6s Plus", \ 23 | @"iPhone8,4" : @"iPhone SE (GSM)", \ 24 | @"iPhone9,1" : @"iPhone 7", \ 25 | @"iPhone9,2" : @"iPhone 7 Plus", \ 26 | @"iPhone9,3" : @"iPhone 7", \ 27 | @"iPhone9,4" : @"iPhone 7 Plus", \ 28 | @"iPhone10,1" : @"iPhone 8", \ 29 | @"iPhone10,2" : @"iPhone 8 Plus", \ 30 | @"iPhone10,3" : @"iPhone X Global", \ 31 | @"iPhone10,4" : @"iPhone 8", \ 32 | @"iPhone10,5" : @"iPhone 8 Plus", \ 33 | @"iPhone10,6" : @"iPhone X GSM", \ 34 | @"iPhone11,2" : @"iPhone XS", \ 35 | @"iPhone11,4" : @"iPhone XS Max", \ 36 | @"iPhone11,6" : @"iPhone XS Max Global", \ 37 | @"iPhone11,8" : @"iPhone XR", \ 38 | @"iPhone12,1" : @"iPhone 11", \ 39 | @"iPhone12,3" : @"iPhone 11 Pro", \ 40 | @"iPhone12,5" : @"iPhone 11 Pro Max", \ 41 | @"iPhone12,8" : @"iPhone SE 2nd Gen", \ 42 | @"iPhone13,1" : @"iPhone 12 Mini", \ 43 | @"iPhone13,2" : @"iPhone 12", \ 44 | @"iPhone13,3" : @"iPhone 12 Pro", \ 45 | @"iPhone13,4" : @"iPhone 12 Pro Max", \ 46 | @"iPhone14,2" : @"iPhone 13 Pro", \ 47 | @"iPhone14,3" : @"iPhone 13 Pro Max", \ 48 | @"iPhone14,4" : @"iPhone 13 Mini", \ 49 | @"iPhone14,5" : @"iPhone 13", \ 50 | @"iPhone14,6" : @"iPhone SE 3rd Gen", \ 51 | @"iPhone14,7" : @"iPhone 14", \ 52 | @"iPhone14,8" : @"iPhone 14 Plus", \ 53 | @"iPhone15,2" : @"iPhone 14 Pro", \ 54 | @"iPhone15,3" : @"iPhone 14 Pro Max", \ 55 | @"iPhone15,4" : @"iPhone 15", \ 56 | @"iPhone15,5" : @"iPhone 15 Plus", \ 57 | @"iPhone16,1" : @"iPhone 15 Pro", \ 58 | @"iPhone16,2" : @"iPhone 15 Pro Max", \ 59 | @"iPhone17,1" : @"iPhone 16 Pro", \ 60 | @"iPhone17,2" : @"iPhone 16 Pro Max", \ 61 | @"iPhone17,3" : @"iPhone 16", \ 62 | @"iPhone17,4" : @"iPhone 16 Plus", \ 63 | @"iPod1,1" : @"1st Gen iPod", \ 64 | @"iPod2,1" : @"2nd Gen iPod", \ 65 | @"iPod3,1" : @"3rd Gen iPod", \ 66 | @"iPod4,1" : @"4th Gen iPod", \ 67 | @"iPod5,1" : @"5th Gen iPod", \ 68 | @"iPod7,1" : @"6th Gen iPod", \ 69 | @"iPod9,1" : @"7th Gen iPod", \ 70 | @"iPad1,1" : @"iPad", \ 71 | @"iPad1,2" : @"iPad 3G", \ 72 | @"iPad2,1" : @"2nd Gen iPad", \ 73 | @"iPad2,2" : @"2nd Gen iPad GSM", \ 74 | @"iPad2,3" : @"2nd Gen iPad CDMA", \ 75 | @"iPad2,4" : @"2nd Gen iPad New Revision", \ 76 | @"iPad3,1" : @"3rd Gen iPad", \ 77 | @"iPad3,2" : @"3rd Gen iPad CDMA", \ 78 | @"iPad3,3" : @"3rd Gen iPad GSM", \ 79 | @"iPad2,5" : @"iPad mini", \ 80 | @"iPad2,6" : @"iPad mini GSM+LTE", \ 81 | @"iPad2,7" : @"iPad mini CDMA+LTE", \ 82 | @"iPad3,4" : @"4th Gen iPad", \ 83 | @"iPad3,5" : @"4th Gen iPad GSM+LTE", \ 84 | @"iPad3,6" : @"4th Gen iPad CDMA+LTE", \ 85 | @"iPad4,1" : @"iPad Air (WiFi)", \ 86 | @"iPad4,2" : @"iPad Air (GSM+CDMA)", \ 87 | @"iPad4,3" : @"1st Gen iPad Air (China)", \ 88 | @"iPad4,4" : @"iPad mini Retina (WiFi)", \ 89 | @"iPad4,5" : @"iPad mini Retina (GSM+CDMA)", \ 90 | @"iPad4,6" : @"iPad mini Retina (China)", \ 91 | @"iPad4,7" : @"iPad mini 3 (WiFi)", \ 92 | @"iPad4,8" : @"iPad mini 3 (GSM+CDMA)", \ 93 | @"iPad4,9" : @"iPad Mini 3 (China)", \ 94 | @"iPad5,1" : @"iPad mini 4 (WiFi)", \ 95 | @"iPad5,2" : @"4th Gen iPad mini (WiFi+Cellular)", \ 96 | @"iPad5,3" : @"iPad Air 2 (WiFi)", \ 97 | @"iPad5,4" : @"iPad Air 2 (Cellular)", \ 98 | @"iPad6,3" : @"iPad Pro (9.7 inch, WiFi)", \ 99 | @"iPad6,4" : @"iPad Pro (9.7 inch, WiFi+LTE)", \ 100 | @"iPad6,7" : @"iPad Pro (12.9 inch, WiFi)", \ 101 | @"iPad6,8" : @"iPad Pro (12.9 inch, WiFi+LTE)", \ 102 | @"iPad6,11" : @"iPad (2017)", \ 103 | @"iPad6,12" : @"iPad (2017)", \ 104 | @"iPad7,1" : @"iPad Pro 2nd Gen (WiFi)", \ 105 | @"iPad7,2" : @"iPad Pro 2nd Gen (WiFi+Cellular)", \ 106 | @"iPad7,3" : @"iPad Pro 10.5-inch 2nd Gen", \ 107 | @"iPad7,4" : @"iPad Pro 10.5-inch 2nd Gen", \ 108 | @"iPad7,5" : @"iPad 6th Gen (WiFi)", \ 109 | @"iPad7,6" : @"iPad 6th Gen (WiFi+Cellular)", \ 110 | @"iPad7,11" : @"iPad 7th Gen 10.2-inch (WiFi)", \ 111 | @"iPad7,12" : @"iPad 7th Gen 10.2-inch (WiFi+Cellular)", \ 112 | @"iPad8,1" : @"iPad Pro 11 inch 3rd Gen (WiFi)", \ 113 | @"iPad8,2" : @"iPad Pro 11 inch 3rd Gen (1TB, WiFi)", \ 114 | @"iPad8,3" : @"iPad Pro 11 inch 3rd Gen (WiFi+Cellular)", \ 115 | @"iPad8,4" : @"iPad Pro 11 inch 3rd Gen (1TB, WiFi+Cellular)", \ 116 | @"iPad8,5" : @"iPad Pro 12.9 inch 3rd Gen (WiFi)", \ 117 | @"iPad8,6" : @"iPad Pro 12.9 inch 3rd Gen (1TB, WiFi)", \ 118 | @"iPad8,7" : @"iPad Pro 12.9 inch 3rd Gen (WiFi+Cellular)", \ 119 | @"iPad8,8" : @"iPad Pro 12.9 inch 3rd Gen (1TB, WiFi+Cellular)", \ 120 | @"iPad8,9" : @"iPad Pro 11 inch 4th Gen (WiFi)", \ 121 | @"iPad8,10" : @"iPad Pro 11 inch 4th Gen (WiFi+Cellular)", \ 122 | @"iPad8,11" : @"iPad Pro 12.9 inch 4th Gen (WiFi)", \ 123 | @"iPad8,12" : @"iPad Pro 12.9 inch 4th Gen (WiFi+Cellular)", \ 124 | @"iPad11,1" : @"iPad mini 5th Gen (WiFi)", \ 125 | @"iPad11,2" : @"iPad mini 5th Gen", \ 126 | @"iPad11,3" : @"iPad Air 3rd Gen (WiFi)", \ 127 | @"iPad11,4" : @"iPad Air 3rd Gen", \ 128 | @"iPad11,6" : @"iPad 8th Gen (WiFi)", \ 129 | @"iPad11,7" : @"iPad 8th Gen (WiFi+Cellular)", \ 130 | @"iPad12,1" : @"iPad 9th Gen (WiFi)", \ 131 | @"iPad12,2" : @"iPad 9th Gen (WiFi+Cellular)", \ 132 | @"iPad14,1" : @"iPad mini 6th Gen (WiFi)", \ 133 | @"iPad14,2" : @"iPad mini 6th Gen (WiFi+Cellular)", \ 134 | @"iPad13,1" : @"iPad Air 4th Gen (WiFi)", \ 135 | @"iPad13,2" : @"iPad Air 4th Gen (WiFi+Cellular)", \ 136 | @"iPad13,4" : @"iPad Pro 11 inch 5th Gen", \ 137 | @"iPad13,5" : @"iPad Pro 11 inch 5th Gen", \ 138 | @"iPad13,6" : @"iPad Pro 11 inch 5th Gen", \ 139 | @"iPad13,7" : @"iPad Pro 11 inch 5th Gen", \ 140 | @"iPad13,8" : @"iPad Pro 12.9 inch 5th Gen", \ 141 | @"iPad13,9" : @"iPad Pro 12.9 inch 5th Gen", \ 142 | @"iPad13,10" : @"iPad Pro 12.9 inch 5th Gen", \ 143 | @"iPad13,11" : @"iPad Pro 12.9 inch 5th Gen", \ 144 | @"iPad13,16" : @"iPad Air 5th Gen (WiFi)", \ 145 | @"iPad13,17" : @"iPad Air 5th Gen (WiFi+Cellular)", \ 146 | @"iPad13,18" : @"iPad 10th Gen", \ 147 | @"iPad13,19" : @"iPad 10th Gen", \ 148 | @"iPad14,3" : @"iPad Pro 11 inch 4th Gen", \ 149 | @"iPad14,4" : @"iPad Pro 11 inch 4th Gen", \ 150 | @"iPad14,5" : @"iPad Pro 12.9 inch 6th Gen", \ 151 | @"iPad14,6" : @"iPad Pro 12.9 inch 6th Gen", \ 152 | @"iPad14,8" : @"iPad Air 6th Gen", \ 153 | @"iPad14,9" : @"iPad Air 6th Gen", \ 154 | @"iPad14,10" : @"iPad Air 7th Gen", \ 155 | @"iPad14,11" : @"iPad Air 7th Gen", \ 156 | @"iPad16,3" : @"iPad Pro 11 inch 5th Gen", \ 157 | @"iPad16,4" : @"iPad Pro 11 inch 5th Gen", \ 158 | @"iPad16,5" : @"iPad Pro 12.9 inch 7th Gen", \ 159 | @"iPad16,6" : @"iPad Pro 12.9 inch 7th Gen" \ 160 | } -------------------------------------------------------------------------------- /headers/Discord/RCT.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface RCTCxxBridge : NSObject 4 | { 5 | } 6 | 7 | - (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async; 8 | 9 | @end -------------------------------------------------------------------------------- /headers/FileSystem.h: -------------------------------------------------------------------------------- 1 | #import "Unbound.h" 2 | 3 | @interface FileSystem : NSObject 4 | { 5 | NSFileManager *manager; 6 | NSString *documents; 7 | } 8 | 9 | + (BOOL)createDirectory:(NSString *)path; 10 | + (void)writeFile:(NSString *)path contents:(NSData *)contents; 11 | 12 | + (id)delete:(NSString *)path; 13 | 14 | + (NSHTTPURLResponse *)download:(NSURL *)url 15 | path:(NSString *)path 16 | withHeaders:(NSDictionary *)headers; 17 | + (NSHTTPURLResponse *)download:(NSURL *)url path:(NSString *)path; 18 | 19 | + (void)monitor:(NSString *)filePath onChange:(void (^)())onChange autoRestart:(BOOL)autoRestart; 20 | 21 | + (NSArray *)readDirectory:(NSString *)path; 22 | + (NSData *)readFile:(NSString *)path; 23 | 24 | + (BOOL)isDirectory:(NSString *)path; 25 | + (BOOL)exists:(NSString *)path; 26 | 27 | + (void)init; 28 | 29 | + (NSString *)documents; 30 | 31 | @end -------------------------------------------------------------------------------- /headers/Fonts.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | 6 | #import "Unbound.h" 7 | 8 | @interface Fonts : NSObject 9 | { 10 | NSMutableDictionary *overrides; 11 | NSMutableArray *> *fonts; 12 | } 13 | 14 | + (void)apply; 15 | + (void)init; 16 | + (void)loadFont:(NSString *)path; 17 | + (NSString *)getFontName:(NSString *)path; 18 | + (NSString *)getFontNameByRef:(CGFontRef)ref; 19 | + (NSArray *)getAvailableFonts; 20 | 21 | + (NSString *)makeAvailableJSON; 22 | + (NSString *)makeJSON; 23 | 24 | + (NSMutableDictionary *)overrides; 25 | 26 | @end -------------------------------------------------------------------------------- /headers/Logger.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | NS_ASSUME_NONNULL_BEGIN 5 | 6 | typedef NS_ENUM(NSInteger, LogLevel) { 7 | LogLevelDebug, 8 | LogLevelInfo, 9 | LogLevelNotice, 10 | LogLevelError, 11 | LogLevelFault 12 | }; 13 | 14 | @interface Logger : NSObject 15 | 16 | // Ensure logger is properly initialized 17 | + (void)initialize; 18 | 19 | // Main logging methods 20 | + (void)log:(LogLevel)level category:(const char *)category format:(NSString *)format, ...; 21 | 22 | // Convenience methods 23 | + (void)debug:(const char *)category format:(NSString *)format, ...; 24 | + (void)info:(const char *)category format:(NSString *)format, ...; 25 | + (void)notice:(const char *)category format:(NSString *)format, ...; 26 | + (void)error:(const char *)category format:(NSString *)format, ...; 27 | + (void)fault:(const char *)category format:(NSString *)format, ...; 28 | 29 | @end 30 | 31 | // Log category constants for different components 32 | #define LOG_CATEGORY_DEFAULT "default" 33 | #define LOG_CATEGORY_PLUGINS "plugins" 34 | #define LOG_CATEGORY_THEMES "themes" 35 | #define LOG_CATEGORY_SETTINGS "settings" 36 | #define LOG_CATEGORY_FILESYSTEM "filesystem" 37 | #define LOG_CATEGORY_UPDATER "updater" 38 | #define LOG_CATEGORY_UTILITIES "utilities" 39 | #define LOG_CATEGORY_RECOVERY "recovery" 40 | #define LOG_CATEGORY_FONTS "fonts" 41 | #define LOG_CATEGORY_NATIVEBRIDGE "nativebridge" 42 | 43 | NS_ASSUME_NONNULL_END 44 | -------------------------------------------------------------------------------- /headers/Misc.h: -------------------------------------------------------------------------------- 1 | #import "Unbound.h" 2 | #import 3 | -------------------------------------------------------------------------------- /headers/NativeBridge.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | #import "Unbound.h" 6 | 7 | typedef void (^RCTPromiseResolveBlock)(id result); 8 | typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError *error); 9 | 10 | @interface NativeBridge : NSObject 11 | 12 | + (id)callNativeMethod:(NSString *)moduleName 13 | method:(NSString *)methodName 14 | arguments:(NSArray *)arguments; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /headers/Plugins.h: -------------------------------------------------------------------------------- 1 | #import "Unbound.h" 2 | 3 | @interface Plugins : NSObject 4 | { 5 | NSMutableArray *plugins; 6 | } 7 | 8 | + (NSString *)makeJSON; 9 | + (void)init; 10 | 11 | @end -------------------------------------------------------------------------------- /headers/Recovery.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | #import 6 | #import 7 | 8 | #import "DeviceModels.h" 9 | #import "FileSystem.h" 10 | #import "Settings.h" 11 | #import "Updater.h" 12 | #import "Utilities.h" 13 | 14 | BOOL isRecoveryModeEnabled(void); 15 | NSString *getDeviceIdentifier(void); 16 | void showMenuSheet(void); 17 | void reloadApp(UIViewController *viewController); 18 | 19 | extern id gBridge; 20 | 21 | @interface UnboundMenuViewController : UIViewController 22 | 23 | @property (nonatomic, strong) UITableView *tableView; 24 | 25 | - (void)dismiss; 26 | 27 | @end 28 | 29 | @interface UnboundMenuViewController () 30 | @property (nonatomic, strong) NSArray *menuSections; 31 | @end -------------------------------------------------------------------------------- /headers/Settings.h: -------------------------------------------------------------------------------- 1 | #import "Unbound.h" 2 | 3 | @interface Settings : NSObject 4 | { 5 | NSDictionary *data; 6 | NSString *path; 7 | } 8 | 9 | + (NSDictionary *)getDictionary:(NSString *)store key:(NSString *)key def:(NSDictionary *)def; 10 | + (NSString *)getString:(NSString *)store key:(NSString *)key def:(NSString *)def; 11 | + (BOOL)getBoolean:(NSString *)store key:(NSString *)key def:(BOOL)def; 12 | + (void)set:(NSString *)store key:(NSString *)key value:(id)value; 13 | + (NSString *)getSettings; 14 | + (void)loadSettings; 15 | + (void)reset; 16 | + (void)init; 17 | + (void)save; 18 | 19 | @end -------------------------------------------------------------------------------- /headers/Themes.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "Unbound.h" 5 | 6 | @interface DCDTheme : NSObject 7 | + (NSInteger)themeIndex; 8 | @end 9 | 10 | @interface Themes : NSObject 11 | { 12 | NSMutableArray *themes; 13 | NSMutableDictionary *originalRawImplementations; 14 | NSString *currentThemeId; 15 | } 16 | 17 | + (NSDictionary *)getThemeById:(NSString *)manifestId; 18 | + (BOOL)isValidCustomTheme:(NSString *)manifestId; 19 | + (void)swizzleRawColors:(NSDictionary *)payload; 20 | + (UIColor *)parseColor:(NSString *)color; 21 | + (void)restoreOriginalRawColors; 22 | + (void)swizzleSemanticColors; 23 | + (NSString *)makeJSON; 24 | + (void)init; 25 | 26 | @end -------------------------------------------------------------------------------- /headers/Unbound.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import "FileSystem.h" 5 | #import "Fonts.h" 6 | #import "Logger.h" 7 | #import "Plugins.h" 8 | #import "Settings.h" 9 | #import "Themes.h" 10 | #import "Updater.h" 11 | #import "Utilities.h" 12 | 13 | #import "Discord/RCT.h" -------------------------------------------------------------------------------- /headers/Updater.h: -------------------------------------------------------------------------------- 1 | #import "Unbound.h" 2 | 3 | @interface Updater : NSObject 4 | { 5 | NSString *etag; 6 | } 7 | 8 | + (void)downloadBundle:(NSString *)path; 9 | + (NSURL *)getDownloadURL; 10 | 11 | @end -------------------------------------------------------------------------------- /headers/Utilities.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | 6 | #import "DeviceModels.h" 7 | #import "FileSystem.h" 8 | #import "Unbound.h" 9 | 10 | @interface Utilities : NSObject 11 | { 12 | NSString *bundle; 13 | } 14 | 15 | + (NSString *)getBundlePath; 16 | 17 | + (NSData *)getResource:(NSString *)file data:(BOOL)data ext:(NSString *)ext; 18 | + (NSString *)getResource:(NSString *)file ext:(NSString *)ext; 19 | 20 | + (NSData *)getResource:(NSString *)file data:(BOOL)data; 21 | + (NSString *)getResource:(NSString *)file; 22 | 23 | + (void)alert:(NSString *)message 24 | title:(NSString *)title 25 | buttons:(NSArray *)buttons; 26 | + (void)alert:(NSString *)message title:(NSString *)title; 27 | + (void)alert:(NSString *)message; 28 | 29 | + (id)parseJSON:(NSData *)data; 30 | 31 | + (dispatch_source_t)createDebounceTimer:(double)delay 32 | queue:(dispatch_queue_t)queue 33 | block:(dispatch_block_t)block; 34 | 35 | + (uint32_t)getHermesBytecodeVersion; 36 | + (BOOL)isHermesBytecode:(NSData *)data; 37 | + (void *)getHermesSymbol:(const char *)symbol error:(NSString **)error; 38 | + (BOOL)isAppStoreApp; 39 | + (BOOL)isJailbroken; 40 | 41 | + (NSString *)getDeviceModelIdentifier; 42 | + (BOOL)deviceHasDynamicIsland; 43 | + (void)initializeDynamicIslandOverlay; 44 | + (void)showDynamicIslandOverlay; 45 | + (void)hideDynamicIslandOverlay; 46 | 47 | @end -------------------------------------------------------------------------------- /sources/FileSystem.m: -------------------------------------------------------------------------------- 1 | #import "FileSystem.h" 2 | 3 | @implementation FileSystem 4 | static NSMutableDictionary *monitors = nil; 5 | static NSFileManager *manager = nil; 6 | static NSString *documents = nil; 7 | 8 | + (BOOL)exists:(NSString *)path 9 | { 10 | return [manager fileExistsAtPath:path]; 11 | } 12 | 13 | + (BOOL)isDirectory:(NSString *)path 14 | { 15 | BOOL isDirectory = NO; 16 | 17 | [manager fileExistsAtPath:path isDirectory:&isDirectory]; 18 | 19 | return isDirectory; 20 | } 21 | 22 | + (void)writeFile:(NSString *)path contents:(NSData *)contents 23 | { 24 | [manager createFileAtPath:path contents:contents attributes:nil]; 25 | } 26 | 27 | + (id)delete:(NSString *)path 28 | { 29 | if (![manager fileExistsAtPath:path]) 30 | { 31 | NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain 32 | code:NSFileNoSuchFileError 33 | userInfo:@{NSFilePathErrorKey : path}]; 34 | 35 | return error; 36 | } 37 | 38 | NSError *error; 39 | [manager removeItemAtPath:path error:&error]; 40 | 41 | return error ? error : path; 42 | } 43 | 44 | + (NSData *)readFile:(NSString *)path 45 | { 46 | if (![manager fileExistsAtPath:path]) 47 | { 48 | @throw [[NSException alloc] 49 | initWithName:@"FileNotFound" 50 | reason:[NSString stringWithFormat:@"File at path %@ was not found.", path] 51 | userInfo:nil]; 52 | } 53 | 54 | NSError *error = nil; 55 | NSData *data = [NSData dataWithContentsOfFile:path options:0 error:&error]; 56 | 57 | if (error) 58 | { 59 | @throw [[NSException alloc] initWithName:error.domain 60 | reason:error.localizedDescription 61 | userInfo:nil]; 62 | } 63 | 64 | return data; 65 | } 66 | 67 | + (BOOL)createDirectory:(NSString *)path 68 | { 69 | if ([manager fileExistsAtPath:path]) 70 | { 71 | return true; 72 | } 73 | 74 | NSError *err; 75 | [manager createDirectoryAtPath:path 76 | withIntermediateDirectories:false 77 | attributes:nil 78 | error:&err]; 79 | 80 | return err ? false : true; 81 | } 82 | 83 | + (NSArray *)readDirectory:(NSString *)path 84 | { 85 | NSError *err; 86 | NSArray *files = [manager contentsOfDirectoryAtPath:path error:&err]; 87 | 88 | return err ? @[] : files; 89 | } 90 | 91 | + (void)monitor:(NSString *)filePath onChange:(void (^)())onChange autoRestart:(BOOL)autoRestart 92 | { 93 | // If file is already being monitored, ignore this extra request. 94 | if ([monitors objectForKey:filePath]) 95 | { 96 | return; 97 | } 98 | 99 | const char *path = [filePath fileSystemRepresentation]; 100 | 101 | int fdescriptor = open(path, O_EVTONLY); 102 | 103 | // Get a reference to the default queue so our file notifications can go out on it 104 | dispatch_queue_t defaultQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 105 | // Create a dispatch source 106 | dispatch_source_t source = dispatch_source_create( 107 | DISPATCH_SOURCE_TYPE_VNODE, fdescriptor, 108 | DISPATCH_VNODE_ATTRIB | DISPATCH_VNODE_DELETE | DISPATCH_VNODE_EXTEND | 109 | DISPATCH_VNODE_LINK | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_REVOKE | 110 | DISPATCH_VNODE_WRITE, 111 | defaultQueue); 112 | 113 | // Add cancel handler 114 | 115 | NSMutableDictionary *monitor = [[NSMutableDictionary alloc] init]; 116 | 117 | monitor[@"cancel"] = ^{ 118 | close(fdescriptor); 119 | dispatch_source_cancel(source); 120 | 121 | [monitors removeObjectForKey:filePath]; 122 | [Logger debug:LOG_CATEGORY_FILESYSTEM format:@"monitor for %@ was destroyed", filePath]; 123 | }; 124 | 125 | monitor[@"debounce_timer"] = nil; 126 | 127 | [monitors setValue:monitor forKey:filePath]; 128 | 129 | dispatch_source_set_event_handler(source, ^{ 130 | if (monitor[@"debounce_timer"] != nil) 131 | { 132 | dispatch_source_cancel(monitor[@"debounce_timer"]); 133 | monitor[@"debounce_timer"] = nil; 134 | } 135 | 136 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 137 | double secondsToThrottle = 0.250f; 138 | monitor[@"debounce_timer"] = [Utilities createDebounceTimer:secondsToThrottle 139 | queue:queue 140 | block:^{ onChange(); }]; 141 | }); 142 | 143 | dispatch_source_set_cancel_handler(source, ^(void) { 144 | [Logger debug:LOG_CATEGORY_FILESYSTEM 145 | format:@"event listener got cancelled for %@", filePath]; 146 | close(fdescriptor); 147 | 148 | // If this dispatch source was canceled because of a rename or delete notification, recreate 149 | // it 150 | if (autoRestart) 151 | { 152 | [Logger debug:LOG_CATEGORY_FILESYSTEM format:@"Restarting file watcher."]; 153 | [FileSystem monitor:filePath onChange:onChange autoRestart:autoRestart]; 154 | } 155 | }); 156 | 157 | // Start monitoring the target file 158 | dispatch_resume(source); 159 | } 160 | 161 | + (void)stopMonitoring:(NSString *)path 162 | { 163 | if (!monitors) 164 | { 165 | return; 166 | } 167 | 168 | NSMutableDictionary *monitor = [monitors valueForKey:path]; 169 | void (^block)(void) = monitor[@"cancel"]; 170 | if (block) 171 | { 172 | block(); 173 | } 174 | } 175 | 176 | + (NSHTTPURLResponse *)download:(NSURL *)url path:(NSString *)path 177 | { 178 | return [FileSystem download:url path:path withHeaders:@{}]; 179 | } 180 | 181 | + (NSHTTPURLResponse *)download:(NSURL *)url 182 | path:(NSString *)path 183 | withHeaders:(NSDictionary *)headers 184 | { 185 | dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); 186 | 187 | NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; 188 | 189 | __block NSHTTPURLResponse *response; 190 | __block NSException *exception; 191 | 192 | request.timeoutInterval = 1.0; 193 | request.cachePolicy = NSURLRequestReloadIgnoringCacheData; 194 | 195 | for (NSString *header in headers) 196 | { 197 | NSString *value = headers[header]; 198 | [request setValue:value forHTTPHeaderField:header]; 199 | } 200 | 201 | dispatch_async(dispatch_get_main_queue(), ^{ 202 | @try 203 | { 204 | NSURLSession *session = [NSURLSession sharedSession]; 205 | NSURLSessionTask *task = [session 206 | dataTaskWithRequest:request 207 | completionHandler:^(NSData *data, NSURLResponse *res, NSError *error) { 208 | response = (NSHTTPURLResponse *) res; 209 | 210 | if (error != nil || 211 | ([response statusCode] != 200 && [response statusCode] != 304)) 212 | { 213 | exception = [[NSException alloc] initWithName:@"DownloadFailed" 214 | reason:error.localizedDescription 215 | userInfo:nil]; 216 | } 217 | else if ([response statusCode] != 304) 218 | { 219 | [Logger info:LOG_CATEGORY_FILESYSTEM 220 | format:@"Saving file from %@ to %@", url, path]; 221 | [data writeToFile:path atomically:YES]; 222 | } 223 | 224 | dispatch_semaphore_signal(semaphore); 225 | }]; 226 | 227 | [task resume]; 228 | } 229 | @catch (NSException *e) 230 | { 231 | exception = e; 232 | 233 | dispatch_semaphore_signal(semaphore); 234 | } 235 | }); 236 | 237 | // Freeze the main thread until the file is downloaded 238 | dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); 239 | 240 | if (exception) 241 | { 242 | @throw exception; 243 | } 244 | 245 | return response; 246 | } 247 | 248 | + (void)init 249 | { 250 | if (!manager) 251 | { 252 | manager = [NSFileManager defaultManager]; 253 | } 254 | 255 | if (!documents) 256 | { 257 | documents = [NSString pathWithComponents:@[ NSHomeDirectory(), @"Documents", @"Unbound" ]]; 258 | } 259 | 260 | if (![FileSystem exists:documents]) 261 | { 262 | [FileSystem createDirectory:documents]; 263 | } 264 | } 265 | 266 | // Properties 267 | + (NSString *)documents 268 | { 269 | return documents; 270 | } 271 | @end -------------------------------------------------------------------------------- /sources/Fonts.x: -------------------------------------------------------------------------------- 1 | #import "Fonts.h" 2 | 3 | @implementation Fonts 4 | static NSMutableDictionary *overrides = nil; 5 | static NSMutableArray *> *fonts = nil; 6 | 7 | + (NSString *)makeJSON 8 | { 9 | NSError *error; 10 | NSData *data = [NSJSONSerialization dataWithJSONObject:fonts options:0 error:&error]; 11 | 12 | if (error != nil) 13 | { 14 | return @"[]"; 15 | } 16 | else 17 | { 18 | return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 19 | } 20 | }; 21 | 22 | + (NSString *)makeAvailableJSON 23 | { 24 | NSError *error; 25 | 26 | NSArray *availabeFonts = [Fonts getAvailableFonts]; 27 | 28 | NSData *data = [NSJSONSerialization dataWithJSONObject:availabeFonts options:0 error:&error]; 29 | 30 | if (error != nil) 31 | { 32 | return @"[]"; 33 | } 34 | else 35 | { 36 | return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 37 | } 38 | }; 39 | 40 | + (NSArray *)getAvailableFonts 41 | { 42 | CFArrayRef available = CTFontManagerCopyAvailableFontFamilyNames(); 43 | NSArray *fonts = (__bridge NSArray *) available; 44 | 45 | return fonts ? fonts : @[]; 46 | } 47 | 48 | + (void)init 49 | { 50 | if (!fonts) 51 | { 52 | fonts = [[NSMutableArray alloc] init]; 53 | } 54 | 55 | if (!overrides) 56 | { 57 | overrides = [[NSMutableDictionary alloc] init]; 58 | } 59 | 60 | NSString *path = [NSString pathWithComponents:@[ FileSystem.documents, @"Fonts" ]]; 61 | [FileSystem createDirectory:path]; 62 | 63 | NSArray *contents = [FileSystem readDirectory:path]; 64 | 65 | for (NSString *file in contents) 66 | { 67 | [Logger info:LOG_CATEGORY_FONTS format:@"Attempting to load %@...", file]; 68 | 69 | @try 70 | { 71 | NSString *font = [NSString pathWithComponents:@[ path, file ]]; 72 | NSString *name = [Fonts getFontName:font]; 73 | 74 | [Logger info:LOG_CATEGORY_FONTS format:@"Registering font: %@", font]; 75 | 76 | [fonts addObject:@{@"name" : name, @"file" : file, @"path" : font}]; 77 | } 78 | @catch (NSException *e) 79 | { 80 | [Logger error:LOG_CATEGORY_FONTS format:@"Failed to load %@ (%@)", file, e.reason]; 81 | } 82 | } 83 | 84 | @try 85 | { 86 | [Logger info:LOG_CATEGORY_FONTS format:@"Loading..."]; 87 | [Fonts apply]; 88 | [Logger info:LOG_CATEGORY_FONTS format:@"Successfully loaded."]; 89 | } 90 | @catch (NSException *e) 91 | { 92 | [Logger error:LOG_CATEGORY_FONTS format:@"Failed to load. (%@)", e.reason]; 93 | } 94 | }; 95 | 96 | + (void)apply 97 | { 98 | NSDictionary *states = [Settings getDictionary:@"unbound" key:@"font-states" def:@{}]; 99 | 100 | for (NSString *state in states) 101 | { 102 | NSString *override = states[state]; 103 | if (!override) 104 | continue; 105 | 106 | NSPredicate *customPredicate = [NSPredicate predicateWithFormat:@"name == %@", override]; 107 | NSArray *custom = [fonts filteredArrayUsingPredicate:customPredicate]; 108 | 109 | NSArray *loadedSystemFonts = [Fonts getAvailableFonts]; 110 | NSPredicate *systemPredicate = [NSPredicate predicateWithFormat:@"SELF == %@", override]; 111 | NSArray *systemFonts = [loadedSystemFonts filteredArrayUsingPredicate:systemPredicate]; 112 | 113 | if ([custom count] == 0 && [systemFonts count] == 0) 114 | { 115 | [Logger error:LOG_CATEGORY_FONTS 116 | format:@"Failed to replace \"%@\" with \"%@\". The requested font isn't loaded.", 117 | state, override]; 118 | continue; 119 | } 120 | 121 | @try 122 | { 123 | NSDictionary *font = [custom count] != 0 ? custom[0] : systemFonts[0]; 124 | if (!font) 125 | continue; 126 | 127 | BOOL isString = [font isKindOfClass:[NSString class]]; 128 | 129 | if (!isString && font[@"path"] != nil) 130 | { 131 | NSString *path = font[@"path"]; 132 | if (!path) 133 | continue; 134 | [Fonts loadFont:path]; 135 | } 136 | 137 | overrides[state] = isString ? font : font[@"name"]; 138 | } 139 | @catch (NSException *e) 140 | { 141 | [Logger error:LOG_CATEGORY_FONTS 142 | format:@"Failed to load font \"%@\". (%@)", override, e.reason]; 143 | } 144 | } 145 | } 146 | 147 | + (void)loadFont:(NSString *)path 148 | { 149 | NSURL *url = [NSURL fileURLWithPath:path]; 150 | 151 | CGDataProviderRef provider = CGDataProviderCreateWithURL((__bridge CFURLRef) url); 152 | CGFontRef ref = CGFontCreateWithDataProvider(provider); 153 | 154 | CGDataProviderRelease(provider); 155 | CTFontManagerRegisterGraphicsFont(ref, nil); 156 | CGFontRelease(ref); 157 | 158 | [Logger info:LOG_CATEGORY_FONTS format:@"Loaded font: %@.", [Fonts getFontNameByRef:ref]]; 159 | } 160 | 161 | + (NSString *)getFontName:(NSString *)path 162 | { 163 | NSURL *url = [NSURL fileURLWithPath:path]; 164 | 165 | CGDataProviderRef provider = CGDataProviderCreateWithURL((__bridge CFURLRef) url); 166 | CGFontRef ref = CGFontCreateWithDataProvider(provider); 167 | CGDataProviderRelease(provider); 168 | 169 | return [Fonts getFontNameByRef:ref]; 170 | } 171 | 172 | + (NSString *)getFontNameByRef:(CGFontRef)ref 173 | { 174 | return CFBridgingRelease(CGFontCopyFullName(ref)); 175 | } 176 | 177 | // Properties 178 | + (NSMutableDictionary *)overrides 179 | { 180 | return overrides; 181 | } 182 | @end 183 | 184 | %hook UIFont 185 | + (UIFont *)fontWithName:(NSString *)name size:(CGFloat)size 186 | { 187 | NSDictionary *overrides = [Fonts overrides]; 188 | NSObject *all = overrides[@"*"]; 189 | 190 | if (overrides[name] || all) 191 | { 192 | return %orig(all ? all : overrides[name], size); 193 | } 194 | 195 | return %orig; 196 | } 197 | %end 198 | -------------------------------------------------------------------------------- /sources/Logger.m: -------------------------------------------------------------------------------- 1 | #import "Logger.h" 2 | 3 | @implementation Logger 4 | 5 | static dispatch_queue_t _logQueue; 6 | 7 | + (void)initialize 8 | { 9 | static dispatch_once_t onceToken; 10 | dispatch_once(&onceToken, ^{ 11 | _logQueue = dispatch_queue_create("app.unbound", DISPATCH_QUEUE_SERIAL); 12 | }); 13 | } 14 | 15 | static os_log_t getLoggerForCategory(const char *category) 16 | { 17 | static NSMutableDictionary *loggers = nil; 18 | static dispatch_once_t onceToken; 19 | 20 | dispatch_once(&onceToken, ^{ loggers = [NSMutableDictionary dictionary]; }); 21 | 22 | NSString *categoryKey = [NSString stringWithUTF8String:category]; 23 | os_log_t logger = nil; 24 | 25 | @synchronized(loggers) 26 | { 27 | logger = [loggers objectForKey:categoryKey]; 28 | 29 | if (!logger) 30 | { 31 | logger = os_log_create("app.unbound", category); 32 | if (logger) 33 | { 34 | [loggers setObject:logger forKey:categoryKey]; 35 | } 36 | } 37 | } 38 | 39 | return logger ?: OS_LOG_DEFAULT; 40 | } 41 | 42 | + (void)log:(LogLevel)level category:(const char *)category format:(NSString *)format, ... 43 | { 44 | // Ensure logger is initialized 45 | [self initialize]; 46 | 47 | // Create a copy of the arguments to use in the async block 48 | va_list args; 49 | va_start(args, format); 50 | NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; 51 | va_end(args); 52 | 53 | // Get category info 54 | NSString *categoryStr = [NSString stringWithUTF8String:category]; 55 | BOOL isDefaultCategory = [categoryStr isEqualToString:@"default"]; 56 | 57 | // Format the final message 58 | if (isDefaultCategory) 59 | { 60 | message = [NSString stringWithFormat:@"[Unbound] %@", message]; 61 | } 62 | else 63 | { 64 | if (categoryStr.length > 0) 65 | { 66 | NSString *firstChar = [[categoryStr substringToIndex:1] uppercaseString]; 67 | NSString *restOfStr = categoryStr.length > 1 ? [categoryStr substringFromIndex:1] : @""; 68 | categoryStr = [firstChar stringByAppendingString:restOfStr]; 69 | } 70 | 71 | message = [NSString stringWithFormat:@"[Unbound] [%@] %@", categoryStr, message]; 72 | } 73 | 74 | // Get the logger for this category 75 | os_log_t logger = getLoggerForCategory(category); 76 | 77 | // Log on the background thread to avoid UI freezes 78 | dispatch_async(_logQueue, ^{ 79 | switch (level) 80 | { 81 | case LogLevelDebug: 82 | os_log_debug(logger, "%{public}@", message); 83 | break; 84 | case LogLevelInfo: 85 | os_log_info(logger, "%{public}@", message); 86 | break; 87 | case LogLevelNotice: 88 | os_log(logger, "%{public}@", message); 89 | break; 90 | case LogLevelError: 91 | os_log_error(logger, "%{public}@", message); 92 | #ifndef DEBUG 93 | NSLog(@"ERROR: %@", message); // Fallback in release mode 94 | #endif 95 | break; 96 | case LogLevelFault: 97 | os_log_fault(logger, "%{public}@", message); 98 | #ifndef DEBUG 99 | NSLog(@"FAULT: %@", message); // Fallback in release mode 100 | #endif 101 | break; 102 | } 103 | }); 104 | } 105 | 106 | + (void)debug:(const char *)category format:(NSString *)format, ... 107 | { 108 | va_list args; 109 | va_start(args, format); 110 | NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; 111 | va_end(args); 112 | 113 | [self log:LogLevelDebug category:category format:@"%@", message]; 114 | } 115 | 116 | + (void)info:(const char *)category format:(NSString *)format, ... 117 | { 118 | va_list args; 119 | va_start(args, format); 120 | NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; 121 | va_end(args); 122 | 123 | [self log:LogLevelInfo category:category format:@"%@", message]; 124 | } 125 | 126 | + (void)notice:(const char *)category format:(NSString *)format, ... 127 | { 128 | va_list args; 129 | va_start(args, format); 130 | NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; 131 | va_end(args); 132 | 133 | [self log:LogLevelNotice category:category format:@"%@", message]; 134 | } 135 | 136 | + (void)error:(const char *)category format:(NSString *)format, ... 137 | { 138 | va_list args; 139 | va_start(args, format); 140 | NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; 141 | va_end(args); 142 | 143 | [self log:LogLevelError category:category format:@"%@", message]; 144 | } 145 | 146 | + (void)fault:(const char *)category format:(NSString *)format, ... 147 | { 148 | va_list args; 149 | va_start(args, format); 150 | NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; 151 | va_end(args); 152 | 153 | [self log:LogLevelFault category:category format:@"%@", message]; 154 | } 155 | 156 | @end 157 | -------------------------------------------------------------------------------- /sources/Misc.x: -------------------------------------------------------------------------------- 1 | #import "Misc.h" 2 | 3 | %hook SentrySDK 4 | + (void)startWithOptions:(id)options 5 | { 6 | [Logger info:LOG_CATEGORY_DEFAULT format:@"Blocked SentrySDK."]; 7 | return; 8 | } 9 | 10 | + (void)startWithConfigureOptions:(id)callback 11 | { 12 | [Logger info:LOG_CATEGORY_DEFAULT format:@"Blocked SentrySDK."]; 13 | return; 14 | } 15 | 16 | + (BOOL)isEnabled 17 | { 18 | return NO; 19 | } 20 | %end 21 | 22 | %hook FIRInstallations 23 | + (void)load 24 | { 25 | [Logger info:LOG_CATEGORY_DEFAULT format:@"Blocked Firebase Installations."]; 26 | return; 27 | } 28 | %end 29 | 30 | // Fix issues with sideloading 31 | %group Sideloading 32 | %hook NSFileManager 33 | - (NSURL *)containerURLForSecurityApplicationGroupIdentifier:(NSString *)identifier 34 | { 35 | if (identifier != nil) 36 | { 37 | NSError *error; 38 | 39 | NSFileManager *manager = [NSFileManager defaultManager]; 40 | NSURL *url = [manager URLForDirectory:NSDocumentDirectory 41 | inDomain:NSUserDomainMask 42 | appropriateForURL:nil 43 | create:YES 44 | error:&error]; 45 | 46 | if (error) 47 | { 48 | [Logger error:LOG_CATEGORY_DEFAULT 49 | format:@"Failed getting documents directory: %@", error]; 50 | return %orig(identifier); 51 | } 52 | 53 | return url; 54 | } 55 | 56 | return %orig(identifier); 57 | } 58 | %end 59 | 60 | // fix file access by using asCopy, adapted from 61 | // https://github.com/khanhduytran0/LiveContainer/blob/main/TweakLoader/DocumentPicker.m 62 | %hook UIDocumentPickerViewController 63 | 64 | - (instancetype)initForOpeningContentTypes:(NSArray *)contentTypes asCopy:(BOOL)asCopy 65 | { 66 | BOOL shouldMultiselect = NO; 67 | if ([contentTypes count] == 1 && contentTypes[0] == UTTypeFolder) 68 | { 69 | shouldMultiselect = YES; 70 | } 71 | 72 | NSArray *contentTypesNew = @[ UTTypeItem, UTTypeFolder ]; 73 | 74 | UIDocumentPickerViewController *ans = %orig(contentTypesNew, YES); 75 | if (shouldMultiselect) 76 | { 77 | [ans setAllowsMultipleSelection:YES]; 78 | } 79 | return ans; 80 | } 81 | 82 | - (instancetype)initWithDocumentTypes:(NSArray *)contentTypes inMode:(NSUInteger)mode 83 | { 84 | return [self initForOpeningContentTypes:contentTypes asCopy:(mode == 1 ? NO : YES)]; 85 | } 86 | 87 | - (void)setAllowsMultipleSelection:(BOOL)allowsMultipleSelection 88 | { 89 | if ([self allowsMultipleSelection]) 90 | { 91 | return; 92 | } 93 | %orig(YES); 94 | } 95 | 96 | %end 97 | 98 | %hook UIDocumentBrowserViewController 99 | 100 | - (instancetype)initForOpeningContentTypes:(NSArray *)contentTypes 101 | { 102 | NSArray *contentTypesNew = @[ UTTypeItem, UTTypeFolder ]; 103 | return %orig(contentTypesNew); 104 | } 105 | 106 | %end 107 | 108 | %hook NSURL 109 | 110 | - (BOOL)startAccessingSecurityScopedResource 111 | { 112 | %orig; 113 | return YES; 114 | } 115 | 116 | %end 117 | 118 | // show icon change error alert 119 | %hook UIApplication 120 | - (void)setAlternateIconName:(NSString *)iconName completionHandler:(void (^)(NSError *))completion 121 | { 122 | void (^wrappedCompletion)(NSError *) = ^(NSError *error) { 123 | if (error) 124 | { 125 | [Utilities alert:@"For this to work change the Bundle ID so that it matches your " 126 | @"provisioning profile's App ID (excluding the Team ID prefix)." 127 | title:@"Cannot Change Icon"]; 128 | } 129 | 130 | if (completion) 131 | { 132 | completion(error); 133 | } 134 | }; 135 | 136 | %orig(iconName, wrappedCompletion); 137 | } 138 | %end 139 | 140 | // show passkey error alert 141 | %hook ASAuthorizationController 142 | 143 | - (void)performRequests 144 | { 145 | [Utilities alert:@"Passkeys are not supported when sideloading Discord. Please use a different " 146 | @"login method." 147 | title:@"Cannot Use Passkey"]; 148 | } 149 | 150 | %end 151 | %end 152 | 153 | #ifdef DEBUG 154 | %group Debug 155 | 156 | static BOOL shouldIgnoreError(NSString *domain, NSInteger code, NSDictionary *info) 157 | { 158 | // Firebase Dynamic Links errors 159 | if ([domain isEqualToString:@"com.firebase.dynamicLinks"] && code == 1) 160 | { 161 | return YES; 162 | } 163 | 164 | // AppSSO errors 165 | if ([domain isEqualToString:@"com.apple.AppSSO.AuthorizationError"] && (code == -1000)) 166 | { 167 | return YES; 168 | } 169 | 170 | // RCT JavaScript loader errors 171 | if ([domain isEqualToString:@"RCTJavaScriptLoaderErrorDomain"] && code == 1000) 172 | { 173 | return YES; 174 | } 175 | 176 | // File not found errors 177 | if ([domain isEqualToString:@"NSPOSIXErrorDomain"]) 178 | { 179 | // Error code 2: No such file 180 | if (code == 2) 181 | return YES; 182 | 183 | // Error code 17: File exists (harmless error when creating directories/files) 184 | if (code == 17) 185 | return YES; 186 | } 187 | 188 | // BS Action errors 189 | if ([domain isEqualToString:@"BSActionErrorDomain"] && code == 1) 190 | { 191 | return YES; 192 | } 193 | 194 | // Filter NSOSStatusErrorDomain errors related to FSNode 195 | if ([domain isEqualToString:@"NSOSStatusErrorDomain"]) 196 | { 197 | // -10813: Common error related to FSNode getHFSType 198 | if (code == -10813) 199 | return YES; 200 | } 201 | 202 | // Related Cocoa errors 203 | if ([domain isEqualToString:@"NSCocoaErrorDomain"]) 204 | { 205 | // File not found related 206 | if (code == 260) 207 | return YES; 208 | 209 | // File exists related error 210 | if (code == 516) 211 | return YES; 212 | 213 | // NSKeyedUnarchiver null data 214 | if (code == 4864) 215 | return YES; 216 | 217 | // Saved application state errors 218 | if (code == 4) 219 | return YES; 220 | } 221 | 222 | return NO; 223 | } 224 | 225 | %hook NSError 226 | - (id)initWithDomain:(id)domain code:(int)code userInfo:(id)userInfo 227 | { 228 | if (!shouldIgnoreError(domain, code, userInfo)) 229 | { 230 | [Logger error:LOG_CATEGORY_DEFAULT 231 | format:@"Initialized with info: %@ %@ %d", userInfo, domain, code]; 232 | } 233 | return %orig; 234 | } 235 | 236 | + (id)errorWithDomain:(id)domain code:(int)code userInfo:(id)userInfo 237 | { 238 | if (!shouldIgnoreError(domain, code, userInfo)) 239 | { 240 | [Logger error:LOG_CATEGORY_DEFAULT 241 | format:@"Initialized with info: %@ %@ %d", userInfo, domain, code]; 242 | } 243 | return %orig; 244 | } 245 | %end 246 | 247 | %hook NSException 248 | - (id)initWithName:(id)name reason:(id)reason userInfo:(id)userInfo 249 | { 250 | [Logger error:LOG_CATEGORY_DEFAULT 251 | format:@"Initialized with info: %@ %@ %@", userInfo, name, reason]; 252 | return %orig; 253 | } 254 | 255 | + (id)exceptionWithName:(id)name reason:(id)reason userInfo:(id)userInfo 256 | { 257 | [Logger error:LOG_CATEGORY_DEFAULT 258 | format:@"Initialized with info: %@ %@ %@", userInfo, name, reason]; 259 | return %orig; 260 | } 261 | %end 262 | %end 263 | #endif 264 | 265 | %ctor 266 | { 267 | %init(); 268 | 269 | #ifdef DEBUG 270 | %init(Debug); 271 | #endif 272 | 273 | if (![Utilities isAppStoreApp]) 274 | { 275 | %init(Sideloading); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /sources/NativeBridge.x: -------------------------------------------------------------------------------- 1 | #import "NativeBridge.h" 2 | 3 | @implementation NativeBridge 4 | 5 | + (id)callNativeMethod:(NSString *)moduleName 6 | method:(NSString *)methodName 7 | arguments:(NSArray *)arguments 8 | { 9 | Class moduleClass = NSClassFromString(moduleName); 10 | if (!moduleClass) 11 | { 12 | [Logger error:LOG_CATEGORY_NATIVEBRIDGE format:@"Module %@ not found", moduleName]; 13 | @throw [NSException 14 | exceptionWithName:@"ModuleNotFound" 15 | reason:[NSString stringWithFormat:@"Module %@ not found", moduleName] 16 | userInfo:nil]; 17 | } 18 | 19 | // Check if we need to append a colon based on argument count 20 | NSString *selectorName = methodName; 21 | if (arguments.count > 0 && ![selectorName hasSuffix:@":"]) 22 | { 23 | selectorName = [selectorName stringByAppendingString:@":"]; 24 | } 25 | 26 | SEL selector = NSSelectorFromString(selectorName); 27 | if (![moduleClass respondsToSelector:selector]) 28 | { 29 | [Logger error:LOG_CATEGORY_NATIVEBRIDGE 30 | format:@"Method %@ not found on %@", selectorName, moduleName]; 31 | 32 | if (arguments.count == 1) 33 | { 34 | selector = NSSelectorFromString([methodName stringByAppendingString:@":"]); 35 | } 36 | else if (arguments.count == 2) 37 | { 38 | selector = NSSelectorFromString([NSString stringWithFormat:@"%@::", methodName]); 39 | } 40 | 41 | if (![moduleClass respondsToSelector:selector]) 42 | { 43 | @throw [NSException 44 | exceptionWithName:@"MethodNotFound" 45 | reason:[NSString stringWithFormat:@"Method %@ not found on %@", 46 | methodName, moduleName] 47 | userInfo:nil]; 48 | } 49 | } 50 | 51 | NSMethodSignature *signature = [moduleClass methodSignatureForSelector:selector]; 52 | NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; 53 | [invocation setSelector:selector]; 54 | [invocation setTarget:moduleClass]; 55 | 56 | // Set arguments 57 | NSUInteger numberOfArguments = signature.numberOfArguments; 58 | for (NSUInteger i = 0; i < arguments.count && (i + 2) < numberOfArguments; i++) 59 | { 60 | id arg = arguments[i]; 61 | [invocation setArgument:&arg atIndex:i + 2]; // start at index 2 (self, _cmd) 62 | } 63 | 64 | [invocation invoke]; 65 | 66 | // Get return value if it exists 67 | if (signature.methodReturnLength > 0) 68 | { 69 | __unsafe_unretained id result = nil; 70 | [invocation getReturnValue:&result]; 71 | return result; 72 | } 73 | 74 | return nil; 75 | } 76 | 77 | @end 78 | 79 | %hook DCDStrongboxManager 80 | - (void)getItem:(NSDictionary *)bridgeCommand 81 | resolve:(RCTPromiseResolveBlock)resolve 82 | reject:(RCTPromiseRejectBlock)reject 83 | { 84 | if (bridgeCommand && [bridgeCommand[@"$$unbound$$"] boolValue]) 85 | { 86 | NSString *moduleName = bridgeCommand[@"module"]; 87 | NSString *methodName = bridgeCommand[@"method"]; 88 | NSArray *args = bridgeCommand[@"args"]; 89 | 90 | if (!moduleName || !methodName) 91 | { 92 | [Logger error:LOG_CATEGORY_NATIVEBRIDGE format:@"Missing module or method name"]; 93 | 94 | if (reject) 95 | reject(@"INVALID_PARAMS", @"Missing module or method name", nil); 96 | return; 97 | } 98 | 99 | [Logger debug:LOG_CATEGORY_NATIVEBRIDGE 100 | format:@"Executing [%@ %@]", moduleName, methodName]; 101 | 102 | @try 103 | { 104 | // Execute the native method 105 | id result = [NativeBridge callNativeMethod:moduleName 106 | method:methodName 107 | arguments:(args ?: @[])]; 108 | 109 | // Return the result 110 | if (resolve) 111 | resolve(result ?: [NSNull null]); 112 | } 113 | @catch (NSException *exception) 114 | { 115 | [Logger error:LOG_CATEGORY_NATIVEBRIDGE 116 | format:@"Error executing [%@ %@]: %@", moduleName, methodName, 117 | exception.reason ?: @"Unknown error"]; 118 | 119 | if (reject) 120 | reject(exception.name ?: @"ERROR", exception.reason ?: @"Unknown error", nil); 121 | } 122 | return; 123 | } 124 | %orig; 125 | } 126 | %end 127 | -------------------------------------------------------------------------------- /sources/Plugins.m: -------------------------------------------------------------------------------- 1 | #import "Plugins.h" 2 | 3 | @implementation Plugins 4 | static NSMutableArray *plugins = nil; 5 | 6 | + (NSString *)makeJSON 7 | { 8 | NSError *error; 9 | NSData *data = [NSJSONSerialization dataWithJSONObject:plugins options:0 error:&error]; 10 | 11 | if (error != nil) 12 | { 13 | return @"[]"; 14 | } 15 | else 16 | { 17 | return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 18 | } 19 | }; 20 | 21 | + (void)init 22 | { 23 | plugins = [[NSMutableArray alloc] init]; 24 | 25 | NSString *path = [NSString pathWithComponents:@[ FileSystem.documents, @"Plugins" ]]; 26 | [FileSystem createDirectory:path]; 27 | 28 | NSArray *contents = [FileSystem readDirectory:path]; 29 | 30 | for (NSString *folder in contents) 31 | { 32 | [Logger info:LOG_CATEGORY_PLUGINS format:@"Attempting to load %@...", folder]; 33 | 34 | @try 35 | { 36 | NSString *dir = [NSString pathWithComponents:@[ path, folder ]]; 37 | 38 | if (![FileSystem isDirectory:dir]) 39 | { 40 | [Logger info:LOG_CATEGORY_PLUGINS 41 | format:@"Skipping %@ as it is not a directory.", folder]; 42 | continue; 43 | } 44 | 45 | NSString *data = [NSString pathWithComponents:@[ dir, @"manifest.json" ]]; 46 | if (![FileSystem exists:data]) 47 | { 48 | [Logger info:LOG_CATEGORY_PLUGINS 49 | format:@"Skipping %@ as it is missing a manifest.", folder]; 50 | continue; 51 | } 52 | 53 | __block NSMutableDictionary *manifest = nil; 54 | 55 | @try 56 | { 57 | id json = [Utilities parseJSON:[FileSystem readFile:data]]; 58 | 59 | if ([json isKindOfClass:[NSDictionary class]]) 60 | { 61 | manifest = [json mutableCopy]; 62 | } 63 | else 64 | { 65 | [Logger info:LOG_CATEGORY_PLUGINS 66 | format:@"Skipping %@ as its manifest is invalid.", folder]; 67 | continue; 68 | } 69 | } 70 | @catch (NSException *e) 71 | { 72 | [Logger error:LOG_CATEGORY_PLUGINS 73 | format:@"Skipping %@ as its manifest failed to be parsed. (%@)", folder, 74 | e.reason]; 75 | continue; 76 | } 77 | 78 | NSString *entry = [NSString pathWithComponents:@[ dir, @"bundle.js" ]]; 79 | if (![FileSystem exists:entry]) 80 | { 81 | [Logger info:LOG_CATEGORY_PLUGINS 82 | format:@"Skipping %@ as it is missing a bundle.", folder]; 83 | continue; 84 | } 85 | 86 | NSData *bundle = [FileSystem readFile:entry]; 87 | 88 | manifest[@"folder"] = folder; 89 | manifest[@"path"] = dir; 90 | 91 | [plugins addObject:@{ 92 | @"manifest" : manifest, 93 | @"bundle" : [[NSString alloc] initWithData:bundle encoding:NSUTF8StringEncoding] 94 | }]; 95 | } 96 | @catch (NSException *e) 97 | { 98 | [Logger error:LOG_CATEGORY_PLUGINS format:@"Failed to load %@ (%@)", folder, e.reason]; 99 | } 100 | } 101 | }; 102 | 103 | @end -------------------------------------------------------------------------------- /sources/Settings.m: -------------------------------------------------------------------------------- 1 | #import "Settings.h" 2 | 3 | @implementation Settings 4 | static NSMutableDictionary *data = nil; 5 | static NSString *path = nil; 6 | 7 | + (void)init 8 | { 9 | path = [NSString pathWithComponents:@[ FileSystem.documents, @"settings.json" ]]; 10 | 11 | [Settings loadSettings]; 12 | 13 | [FileSystem monitor:path onChange:^{ [Settings loadSettings]; } autoRestart:YES]; 14 | } 15 | 16 | + (void)loadSettings 17 | { 18 | if (![FileSystem exists:path]) 19 | { 20 | [Settings reset]; 21 | } 22 | 23 | NSData *settings = [FileSystem readFile:path]; 24 | 25 | NSError *error; 26 | data = [NSJSONSerialization JSONObjectWithData:settings 27 | options:NSJSONReadingMutableContainers 28 | error:&error]; 29 | } 30 | 31 | + (NSString *)getString:(NSString *)store key:(NSString *)key def:(NSString *)def 32 | { 33 | id payload = data[store]; 34 | if (!payload) 35 | return def; 36 | 37 | id value = [payload valueForKeyPath:key]; 38 | 39 | return value != nil ? value : def; 40 | } 41 | 42 | + (NSDictionary *)getDictionary:(NSString *)store key:(NSString *)key def:(NSDictionary *)def 43 | { 44 | id payload = data[store]; 45 | if (!payload) 46 | return def; 47 | 48 | id value = [payload valueForKeyPath:key]; 49 | 50 | return value != nil ? value : def; 51 | } 52 | 53 | + (BOOL)getBoolean:(NSString *)store key:(NSString *)key def:(BOOL)def 54 | { 55 | id payload = data[store]; 56 | if (!payload) 57 | return def; 58 | 59 | id value = [payload valueForKeyPath:key]; 60 | 61 | if (value != nil && [value respondsToSelector:@selector(boolValue)]) 62 | { 63 | return [value boolValue]; 64 | } 65 | 66 | return def; 67 | } 68 | 69 | + (void)set:(NSString *)store key:(NSString *)key value:(id)value 70 | { 71 | @try 72 | { 73 | if (!data) 74 | { 75 | data = [NSMutableDictionary dictionary]; 76 | } 77 | 78 | __block NSMutableDictionary *payload = data[store]; 79 | 80 | if (!payload) 81 | { 82 | payload = [NSMutableDictionary dictionary]; 83 | data[store] = payload; 84 | } 85 | 86 | // Ensure all keys exist before the last one 87 | NSArray *keys = [key componentsSeparatedByString:@"."]; 88 | __block NSMutableDictionary *res = payload; 89 | 90 | for (id key in keys) 91 | { 92 | if ([keys count] == ([keys indexOfObject:key] + 1)) 93 | { 94 | [res setValue:value forKey:key]; 95 | break; 96 | } 97 | 98 | if (res[key] == nil) 99 | { 100 | [res setValue:[NSMutableDictionary dictionary] forKey:key]; 101 | } 102 | 103 | res = res[key]; 104 | } 105 | 106 | [Settings save]; 107 | } 108 | @catch (NSException *e) 109 | { 110 | [Logger error:LOG_CATEGORY_SETTINGS format:@"Settings set failed. %@", e]; 111 | } 112 | } 113 | 114 | + (void)reset 115 | { 116 | NSString *payload = @"{}"; 117 | 118 | [FileSystem writeFile:path contents:[payload dataUsingEncoding:NSUTF8StringEncoding]]; 119 | } 120 | 121 | + (void)save 122 | { 123 | NSString *payload = [Settings getSettings]; 124 | 125 | NSData *contents = [payload dataUsingEncoding:NSUTF8StringEncoding]; 126 | 127 | [FileSystem writeFile:path contents:contents]; 128 | } 129 | 130 | + (NSString *)getSettings 131 | { 132 | NSError *error; 133 | NSData *json = [NSJSONSerialization dataWithJSONObject:data 134 | options:NSJSONWritingPrettyPrinted 135 | error:&error]; 136 | 137 | if (error != nil) 138 | { 139 | return @"{}"; 140 | } 141 | else 142 | { 143 | return [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding]; 144 | } 145 | } 146 | @end -------------------------------------------------------------------------------- /sources/Themes.x: -------------------------------------------------------------------------------- 1 | #import "Themes.h" 2 | 3 | @implementation Themes 4 | static NSMutableDictionary *originalRawImplementations; 5 | static NSMutableArray *themes = nil; 6 | static NSString *currentThemeId = nil; 7 | 8 | + (NSString *)makeJSON 9 | { 10 | NSError *error; 11 | NSData *data = [NSJSONSerialization dataWithJSONObject:themes options:0 error:&error]; 12 | 13 | if (error != nil) 14 | { 15 | return @"[]"; 16 | } 17 | else 18 | { 19 | return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 20 | } 21 | }; 22 | 23 | + (NSDictionary *)getThemeById:(NSString *)manifestId 24 | { 25 | NSPredicate *predicate = [NSPredicate predicateWithFormat:@"manifest.id == %@", manifestId]; 26 | NSArray *array = [themes filteredArrayUsingPredicate:predicate]; 27 | 28 | if ([array count] != 0) 29 | { 30 | return array[0]; 31 | } 32 | 33 | return nil; 34 | } 35 | 36 | + (BOOL)isValidCustomTheme:(NSString *)manifestId 37 | { 38 | NSDictionary *theme = [Themes getThemeById:manifestId]; 39 | 40 | if (theme != nil) 41 | { 42 | return YES; 43 | } 44 | 45 | return NO; 46 | } 47 | 48 | + (void)init 49 | { 50 | if (!themes) 51 | { 52 | themes = [[NSMutableArray alloc] init]; 53 | } 54 | 55 | if (!originalRawImplementations) 56 | { 57 | originalRawImplementations = [[NSMutableDictionary alloc] init]; 58 | } 59 | 60 | NSString *path = [NSString pathWithComponents:@[ FileSystem.documents, @"Themes" ]]; 61 | [FileSystem createDirectory:path]; 62 | 63 | NSArray *contents = [FileSystem readDirectory:path]; 64 | 65 | for (NSString *folder in contents) 66 | { 67 | [Logger info:LOG_CATEGORY_THEMES format:@"Attempting to load %@...", folder]; 68 | 69 | @try 70 | { 71 | NSString *dir = [NSString pathWithComponents:@[ path, folder ]]; 72 | 73 | if (![FileSystem isDirectory:dir]) 74 | { 75 | [Logger info:LOG_CATEGORY_THEMES 76 | format:@"Skipping %@ as it is not a directory.", folder]; 77 | continue; 78 | } 79 | 80 | NSString *data = [NSString pathWithComponents:@[ dir, @"manifest.json" ]]; 81 | if (![FileSystem exists:data]) 82 | { 83 | [Logger info:LOG_CATEGORY_THEMES 84 | format:@"Skipping %@ as it is missing a manifest.", folder]; 85 | continue; 86 | } 87 | 88 | __block NSMutableDictionary *manifest = nil; 89 | 90 | @try 91 | { 92 | id json = [Utilities parseJSON:[FileSystem readFile:data]]; 93 | 94 | if ([json isKindOfClass:[NSDictionary class]]) 95 | { 96 | manifest = [json mutableCopy]; 97 | } 98 | else 99 | { 100 | [Logger info:LOG_CATEGORY_THEMES 101 | format:@"Skipping %@ as its manifest is invalid.", folder]; 102 | continue; 103 | } 104 | } 105 | @catch (NSException *e) 106 | { 107 | [Logger error:LOG_CATEGORY_THEMES 108 | format:@"Skipping %@ as its manifest failed to be parsed. (%@)", folder, 109 | e.reason]; 110 | continue; 111 | } 112 | 113 | NSString *entry = [NSString pathWithComponents:@[ dir, @"bundle.json" ]]; 114 | if (![FileSystem exists:entry]) 115 | { 116 | [Logger info:LOG_CATEGORY_THEMES 117 | format:@"Skipping %@ as it is missing a bundle.", folder]; 118 | continue; 119 | } 120 | 121 | __block NSData *bundle = nil; 122 | 123 | @try 124 | { 125 | id json = [Utilities parseJSON:[FileSystem readFile:entry]]; 126 | 127 | if ([json isKindOfClass:[NSDictionary class]]) 128 | { 129 | bundle = [json mutableCopy]; 130 | } 131 | else 132 | { 133 | [Logger info:LOG_CATEGORY_THEMES 134 | format:@"Skipping %@ as its bundle is invalid JSON.", folder]; 135 | continue; 136 | } 137 | } 138 | @catch (NSException *e) 139 | { 140 | [Logger error:LOG_CATEGORY_THEMES 141 | format:@"Skipping %@ as its bundle failed to be parsed. (%@)", folder, 142 | e.reason]; 143 | continue; 144 | } 145 | 146 | manifest[@"folder"] = folder; 147 | manifest[@"path"] = dir; 148 | 149 | [themes addObject:@{@"manifest" : manifest, @"bundle" : bundle}]; 150 | } 151 | @catch (NSException *e) 152 | { 153 | [Logger error:LOG_CATEGORY_THEMES format:@"Failed to load %@ (%@)", folder, e.reason]; 154 | } 155 | } 156 | 157 | if (![Settings getBoolean:@"unbound" key:@"recovery" def:NO]) 158 | { 159 | [Themes swizzleSemanticColors]; 160 | } 161 | }; 162 | 163 | + (void)swizzleRawColors:(NSDictionary *)payload 164 | { 165 | // Get the class reference for UIColor 166 | Class instance = object_getClass(NSClassFromString(@"UIColor")); 167 | 168 | [Logger info:LOG_CATEGORY_THEMES format:@"Attempting swizzle raw colors..."]; 169 | 170 | @try 171 | { 172 | for (NSString *raw in payload) 173 | { 174 | SEL selector = NSSelectorFromString(raw); 175 | 176 | // Define a block to replace the original method implementation 177 | __block id (*original)(Class, SEL); 178 | IMP replacement = imp_implementationWithBlock(^UIColor *(id self) { 179 | @try 180 | { 181 | id color = payload[raw]; 182 | UIColor *parsed = [Themes parseColor:color]; 183 | if (parsed) 184 | return parsed; 185 | } 186 | @catch (NSException *e) 187 | { 188 | [Logger error:LOG_CATEGORY_THEMES 189 | format:@"Failed to use modified raw color %@. (%@)", raw, e.reason]; 190 | } 191 | 192 | // Call the original implementation if parsing fails 193 | return original(instance, selector); 194 | }); 195 | 196 | // Hook the original method with the replacement block 197 | MSHookMessageEx(instance, selector, replacement, (IMP *) &original); 198 | 199 | // Store the original implementation for restoration when the theme changes 200 | originalRawImplementations[raw] = [NSValue valueWithPointer:(void *) original]; 201 | } 202 | 203 | [Logger info:LOG_CATEGORY_THEMES format:@"Raw color swizzle completed."]; 204 | } 205 | @catch (NSException *e) 206 | { 207 | [Logger error:LOG_CATEGORY_THEMES format:@"Failed to swizzle raw colors. (%@)", e.reason]; 208 | } 209 | } 210 | 211 | + (void)restoreOriginalRawColors 212 | { 213 | Class instance = object_getClass(NSClassFromString(@"UIColor")); 214 | 215 | // Iterate over the stored original implementations and restore them 216 | for (NSString *selectorName in originalRawImplementations) 217 | { 218 | SEL selector = NSSelectorFromString(selectorName); 219 | IMP originalIMP = (IMP)[originalRawImplementations[selectorName] pointerValue]; 220 | 221 | // Reapply the original implementation 222 | if (originalIMP) 223 | { 224 | MSHookMessageEx(instance, selector, originalIMP, NULL); 225 | } 226 | else 227 | { 228 | [Logger error:LOG_CATEGORY_THEMES 229 | format:@"Failed to restore implementation for %@: Original IMP is NULL", 230 | selectorName]; 231 | } 232 | } 233 | 234 | // Clear the dictionary after unsetting swizzles 235 | [originalRawImplementations removeAllObjects]; 236 | } 237 | 238 | + (void)swizzleSemanticColors 239 | { 240 | [Logger info:LOG_CATEGORY_THEMES format:@"Attempting swizzle semantic colors..."]; 241 | 242 | @try 243 | { 244 | // Get the class reference for DCDThemeColor 245 | Class instance = object_getClass(NSClassFromString(@"DCDThemeColor")); 246 | 247 | // All DCDThemeColor methods return UIColor and are semantic colors. 248 | // We dynamically copy them and patch them to avoid hardcoding each color. 249 | unsigned methodCount = 0; 250 | Method *methods = class_copyMethodList(instance, &methodCount); 251 | 252 | for (unsigned int i = 0; i < methodCount; i++) 253 | { 254 | Method method = methods[i]; 255 | SEL selector = method_getName(method); 256 | NSString *name = NSStringFromSelector(selector); 257 | 258 | // Define a block to replace the original method implementation 259 | __block id (*original)(Class, SEL); 260 | IMP replacement = imp_implementationWithBlock(^UIColor *(id self) { 261 | if (currentThemeId != nil) 262 | { 263 | @try 264 | { 265 | NSDictionary *theme = [Themes getThemeById:currentThemeId]; 266 | if (!theme) 267 | return original(instance, selector); 268 | 269 | NSDictionary *values = theme[@"bundle"][@"semantic"]; 270 | if (!values) 271 | return original(instance, selector); 272 | 273 | NSDictionary *color = values[name]; 274 | if (!color || !color[@"type"] || !color[@"value"]) 275 | { 276 | return original(instance, selector); 277 | } 278 | 279 | NSString *colorType = color[@"type"]; 280 | NSString *colorValue = color[@"value"]; 281 | NSNumber *colorOpacity = color[@"opacity"]; 282 | 283 | // Theme Developers are allowed to specify a custom color. (rgb/rgba/hex) 284 | if ([colorType isEqualToString:@"color"]) 285 | { 286 | UIColor *parsed = [Themes parseColor:colorValue]; 287 | 288 | if (parsed) 289 | { 290 | if (colorOpacity) 291 | { 292 | return 293 | [parsed colorWithAlphaComponent:[colorOpacity doubleValue]]; 294 | } 295 | 296 | return parsed; 297 | } 298 | } 299 | 300 | // Theme Developers can also use Discord's raw colors. 301 | if ([colorType isEqualToString:@"raw"]) 302 | { 303 | SEL colorSelector = NSSelectorFromString(colorValue); 304 | Class instance = object_getClass(NSClassFromString(@"UIColor")); 305 | 306 | if ([instance respondsToSelector:colorSelector]) 307 | { 308 | UIColor *(*getColor)(id, SEL); 309 | getColor = 310 | (UIColor * 311 | (*) (id, SEL)) [instance methodForSelector:colorSelector]; 312 | 313 | return getColor(instance, colorSelector); 314 | } 315 | 316 | return original(instance, selector); 317 | } 318 | 319 | return original(instance, selector); 320 | } 321 | @catch (NSException *e) 322 | { 323 | [Logger error:LOG_CATEGORY_THEMES 324 | format:@"Failed to use modified color %@. (%@)", name, e.reason]; 325 | } 326 | } 327 | 328 | // Call the original implementation if parsing fails or the user does not have a 329 | // theme applied. 330 | return original(instance, selector); 331 | }); 332 | 333 | // Hook the original method with the replacement block 334 | MSHookMessageEx(instance, selector, replacement, (IMP *) &original); 335 | } 336 | 337 | free(methods); 338 | [Logger info:LOG_CATEGORY_THEMES format:@"Semantic color swizzle completed."]; 339 | } 340 | @catch (NSException *e) 341 | { 342 | [Logger error:LOG_CATEGORY_THEMES 343 | format:@"Failed to swizzle semantic colors. (%@)", e.reason]; 344 | } 345 | } 346 | 347 | + (UIColor *)parseColor:(NSString *)color 348 | { 349 | if ([color hasPrefix:@"#"]) 350 | { 351 | if (color.length == 7) 352 | { 353 | color = [color stringByAppendingString:@"FF"]; 354 | } 355 | 356 | NSScanner *scanner = [NSScanner scannerWithString:color]; 357 | unsigned res = 0; 358 | 359 | [scanner setScanLocation:1]; 360 | [scanner scanHexInt:&res]; 361 | 362 | CGFloat r = ((res & 0xFF000000) >> 24) / 255.0; 363 | CGFloat g = ((res & 0x00FF0000) >> 16) / 255.0; 364 | CGFloat b = ((res & 0x0000FF00) >> 8) / 255.0; 365 | CGFloat a = (res & 0x000000FF) / 255.0; 366 | 367 | return [UIColor colorWithRed:r green:g blue:b alpha:a]; 368 | } 369 | 370 | if ([color hasPrefix:@"rgba"]) 371 | { 372 | NSRegularExpression *regex = 373 | [NSRegularExpression regularExpressionWithPattern:@"\\((.*)\\)" 374 | options:NSRegularExpressionCaseInsensitive 375 | error:nil]; 376 | 377 | NSArray *matches = [regex matchesInString:color 378 | options:0 379 | range:NSMakeRange(0, [color length])]; 380 | NSString *value = [[NSString alloc] init]; 381 | 382 | for (NSTextCheckingResult *match in matches) 383 | { 384 | NSRange range = [match rangeAtIndex:1]; 385 | value = [color substringWithRange:range]; 386 | } 387 | 388 | NSCharacterSet *whitespaces = [NSCharacterSet whitespaceCharacterSet]; 389 | NSArray *values = [value componentsSeparatedByString:@","]; 390 | NSMutableArray *res = [[NSMutableArray alloc] init]; 391 | 392 | for (NSString *value in values) 393 | { 394 | NSString *trimmed = [value stringByTrimmingCharactersInSet:whitespaces]; 395 | NSNumber *payload = [NSNumber numberWithFloat:[trimmed floatValue]]; 396 | 397 | [res addObject:payload]; 398 | } 399 | 400 | CGFloat r = [[res objectAtIndex:0] floatValue] / 255.0f; 401 | CGFloat g = [[res objectAtIndex:1] floatValue] / 255.0f; 402 | CGFloat b = [[res objectAtIndex:2] floatValue] / 255.0f; 403 | CGFloat a = [[res objectAtIndex:3] floatValue]; 404 | 405 | return [UIColor colorWithRed:r green:g blue:b alpha:a]; 406 | } 407 | 408 | if ([color hasPrefix:@"rgb"]) 409 | { 410 | NSRegularExpression *regex = 411 | [NSRegularExpression regularExpressionWithPattern:@"\\((.*)\\)" 412 | options:NSRegularExpressionCaseInsensitive 413 | error:nil]; 414 | 415 | NSArray *matches = [regex matchesInString:color 416 | options:0 417 | range:NSMakeRange(0, [color length])]; 418 | NSString *value = [[NSString alloc] init]; 419 | 420 | for (NSTextCheckingResult *match in matches) 421 | { 422 | NSRange range = [match rangeAtIndex:1]; 423 | value = [color substringWithRange:range]; 424 | } 425 | 426 | NSCharacterSet *whitespaces = [NSCharacterSet whitespaceCharacterSet]; 427 | NSArray *values = [value componentsSeparatedByString:@","]; 428 | NSMutableArray *res = [[NSMutableArray alloc] init]; 429 | 430 | for (NSString *value in values) 431 | { 432 | NSString *trimmed = [value stringByTrimmingCharactersInSet:whitespaces]; 433 | NSNumber *payload = [NSNumber numberWithFloat:[trimmed floatValue]]; 434 | 435 | [res addObject:payload]; 436 | } 437 | 438 | CGFloat r = [[res objectAtIndex:0] floatValue] / 255.0f; 439 | CGFloat g = [[res objectAtIndex:1] floatValue] / 255.0f; 440 | CGFloat b = [[res objectAtIndex:2] floatValue] / 255.0f; 441 | 442 | return [UIColor colorWithRed:r green:g blue:b alpha:1.0f]; 443 | } 444 | 445 | return nil; 446 | } 447 | @end 448 | 449 | %hook DCDTheme 450 | - (void)updateTheme:(id)theme 451 | { 452 | if ([currentThemeId isEqualToString:theme]) 453 | { 454 | return %orig; 455 | } 456 | 457 | [Logger info:LOG_CATEGORY_THEMES format:@"Theme updated. (%@)", theme]; 458 | currentThemeId = theme; 459 | 460 | [Themes restoreOriginalRawColors]; 461 | 462 | NSDictionary *instance = [Themes getThemeById:theme]; 463 | 464 | if (instance) 465 | { 466 | NSDictionary *raw = instance[@"bundle"][@"raw"]; 467 | if (raw) 468 | [Themes swizzleRawColors:raw]; 469 | } 470 | 471 | %orig; 472 | } 473 | %end 474 | -------------------------------------------------------------------------------- /sources/Unbound.x: -------------------------------------------------------------------------------- 1 | #import "Unbound.h" 2 | 3 | id gBridge = nil; 4 | 5 | %hook RCTCxxBridge 6 | - (void)executeApplicationScript:(NSData *)script url:(NSURL *)url async:(BOOL)async 7 | { 8 | gBridge = self; 9 | 10 | [FileSystem init]; 11 | [Settings init]; 12 | 13 | // Don't load bundle and addons if not configured to do so. 14 | if (![Settings getBoolean:@"unbound" key:@"loader.enabled" def:YES]) 15 | { 16 | [Logger info:LOG_CATEGORY_DEFAULT format:@"Loader is disabled. Aborting."]; 17 | return %orig(script, url, true); 18 | } 19 | 20 | [Plugins init]; 21 | [Themes init]; 22 | [Fonts init]; 23 | 24 | NSString *BUNDLE = [NSString pathWithComponents:@[ FileSystem.documents, @"unbound.bundle" ]]; 25 | NSURL *SOURCE = [NSURL URLWithString:@"unbound"]; 26 | 27 | // Apply React DevTools patch if its enabled 28 | if ([Settings getBoolean:@"unbound" key:@"loader.devtools" def:NO]) 29 | { 30 | @try 31 | { 32 | NSData *bundle = [Utilities getResource:@"devtools" data:true ext:@"js"]; 33 | 34 | [Logger info:LOG_CATEGORY_DEFAULT format:@"Attempting to execute DevTools bundle..."]; 35 | %orig(bundle, SOURCE, true); 36 | [Logger info:LOG_CATEGORY_DEFAULT format:@"Successfully executed DevTools bundle."]; 37 | } 38 | @catch (NSException *e) 39 | { 40 | [Logger error:LOG_CATEGORY_DEFAULT 41 | format:@"React DevTools failed to initialize. %@", e]; 42 | } 43 | } 44 | 45 | // Apply modules patch 46 | @try 47 | { 48 | NSData *bundle = [Utilities getResource:@"modules" data:true ext:@"js"]; 49 | 50 | [Logger info:LOG_CATEGORY_DEFAULT format:@"Attempting to execute modules patch..."]; 51 | %orig(bundle, SOURCE, true); 52 | [Logger info:LOG_CATEGORY_DEFAULT format:@"Successfully executed modules patch."]; 53 | } 54 | @catch (NSException *e) 55 | { 56 | [Logger error:LOG_CATEGORY_DEFAULT 57 | format:@"Modules patch injection failed, expect issues. %@", e]; 58 | } 59 | 60 | // Preload Unbound's settings, plugins & themes 61 | @try 62 | { 63 | NSString *bundle = [Utilities getResource:@"preload"]; 64 | NSString *settings = [Settings getSettings]; 65 | NSString *plugins = [Plugins makeJSON]; 66 | NSString *themes = [Themes makeJSON]; 67 | 68 | NSString *availableFonts = [Fonts makeAvailableJSON]; 69 | NSString *fonts = [Fonts makeJSON]; 70 | 71 | NSString *json = 72 | [NSString stringWithFormat:bundle, settings, plugins, themes, fonts, availableFonts]; 73 | NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding]; 74 | 75 | [Logger info:LOG_CATEGORY_DEFAULT 76 | format:@"Pre-loading settings, plugins, fonts and themes..."]; 77 | %orig(data, SOURCE, true); 78 | [Logger info:LOG_CATEGORY_DEFAULT 79 | format:@"Pre-loaded settings, plugins, fonts and themes."]; 80 | } 81 | @catch (NSException *e) 82 | { 83 | [Logger error:LOG_CATEGORY_DEFAULT 84 | format:@"Failed to pre-load settings, plugins, fonts and themes. %@", e]; 85 | } 86 | 87 | %orig(script, url, true); 88 | 89 | // Check for updates & re-download bundle if necessary 90 | @try 91 | { 92 | [Updater downloadBundle:BUNDLE]; 93 | } 94 | @catch (NSException *e) 95 | { 96 | [Logger error:LOG_CATEGORY_DEFAULT format:@"Bundle download failed. (%@)", e]; 97 | 98 | if (![FileSystem exists:BUNDLE]) 99 | { 100 | return [Utilities alert:@"Bundle failed to download, please report this " 101 | @"to the developers."]; 102 | } 103 | else 104 | { 105 | [Utilities alert:@"Bundle failed to update, loading out of date bundle."]; 106 | } 107 | } 108 | 109 | // Check if Unbound was downloaded properly 110 | if (![FileSystem exists:BUNDLE]) 111 | { 112 | return [Utilities alert:@"Bundle not found, please report this to the developers."]; 113 | } 114 | 115 | // Inject Unbound script 116 | @try 117 | { 118 | NSData *bundle = [FileSystem readFile:BUNDLE]; 119 | 120 | [Logger info:LOG_CATEGORY_DEFAULT format:@"Attempting to execute bundle..."]; 121 | %orig(bundle, SOURCE, true); 122 | [Logger info:LOG_CATEGORY_DEFAULT format:@"Unbound's bundle was successfully executed."]; 123 | } 124 | @catch (NSException *e) 125 | { 126 | [Logger error:LOG_CATEGORY_DEFAULT 127 | format:@"Unbound's bundle failed execution, aborting. (%@)", e.reason]; 128 | return [Utilities alert:@"Failed to load Unbound's bundle. Please report " 129 | @"this to the developers."]; 130 | } 131 | } 132 | %end 133 | 134 | %ctor 135 | { 136 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), 137 | ^{ [Utilities initializeDynamicIslandOverlay]; }); 138 | } 139 | -------------------------------------------------------------------------------- /sources/Updater.m: -------------------------------------------------------------------------------- 1 | #import "Updater.h" 2 | 3 | @implementation Updater 4 | static NSString *etag = nil; 5 | 6 | + (void)downloadBundle:(NSString *)path 7 | { 8 | [Logger info:LOG_CATEGORY_UPDATER format:@"Ensuring bundle is up to date..."]; 9 | 10 | NSString *etag = [Settings getString:@"unbound" key:@"loader.update.etag" def:@""]; 11 | NSURL *url = [Updater getDownloadURL]; 12 | 13 | __block NSHTTPURLResponse *response; 14 | 15 | if (![FileSystem exists:path] || [Settings getBoolean:@"unbound" 16 | key:@"loader.update.force" 17 | def:NO]) 18 | { 19 | response = [FileSystem download:url path:path]; 20 | } 21 | else 22 | { 23 | response = [FileSystem download:url path:path withHeaders:@{@"If-None-Match" : etag}]; 24 | } 25 | 26 | if ([response statusCode] == 304) 27 | { 28 | [Logger info:LOG_CATEGORY_UPDATER format:@"No update found."]; 29 | } 30 | else 31 | { 32 | [Logger info:LOG_CATEGORY_UPDATER format:@"Successfully updated to the latest version."]; 33 | [Settings set:@"unbound" 34 | key:@"loader.update.etag" 35 | value:[response valueForHTTPHeaderField:@"etag"]]; 36 | } 37 | } 38 | 39 | + (NSURL *)getDownloadURL 40 | { 41 | NSString *url = [Settings getString:@"unbound" 42 | key:@"loader.update.url" 43 | def:@"https://raw.githubusercontent.com/unbound-app/builds/" 44 | @"refs/heads/main/unbound.js"]; 45 | 46 | return [NSURL URLWithString:url]; 47 | } 48 | @end 49 | -------------------------------------------------------------------------------- /sources/Utilities.m: -------------------------------------------------------------------------------- 1 | #import "Utilities.h" 2 | 3 | @implementation Utilities 4 | static NSString *bundle = nil; 5 | static UIView *islandOverlayView = nil; 6 | 7 | + (NSString *)getBundlePath 8 | { 9 | if (bundle) 10 | { 11 | [Logger info:LOG_CATEGORY_UTILITIES format:@"Using cached bundle URL."]; 12 | return bundle; 13 | } 14 | 15 | // Attempt to get the bundle from an exact path 16 | NSString *bundlePath = ROOT_PATH_NS(@"/Library/Application Support/UnboundResources.bundle"); 17 | 18 | if ([FileSystem exists:bundlePath]) 19 | { 20 | bundle = bundlePath; 21 | return bundlePath; 22 | } 23 | 24 | // Fall back to a relative path on jailed devices 25 | NSURL *url = [[NSBundle mainBundle] bundleURL]; 26 | NSString *relative = [NSString stringWithFormat:@"%@/UnboundResources.bundle", [url path]]; 27 | if ([FileSystem exists:relative]) 28 | { 29 | bundle = relative; 30 | return relative; 31 | } 32 | 33 | return nil; 34 | } 35 | 36 | + (NSString *)getResource:(NSString *)file 37 | { 38 | return [Utilities getResource:file ext:@"js"]; 39 | } 40 | 41 | + (NSData *)getResource:(NSString *)file data:(BOOL)data 42 | { 43 | NSString *resource = [Utilities getResource:file]; 44 | 45 | return [resource dataUsingEncoding:NSUTF8StringEncoding]; 46 | } 47 | 48 | + (NSData *)getResource:(NSString *)file data:(BOOL)data ext:(NSString *)ext 49 | { 50 | NSBundle *bundle = [NSBundle bundleWithPath:[Utilities getBundlePath]]; 51 | if (bundle == nil) 52 | { 53 | return nil; 54 | } 55 | 56 | NSString *path = [bundle pathForResource:file ofType:ext]; 57 | 58 | return [NSData dataWithContentsOfFile:path options:0 error:nil]; 59 | } 60 | 61 | + (NSString *)getResource:(NSString *)file ext:(NSString *)ext 62 | { 63 | NSData *data = [Utilities getResource:file data:true ext:ext]; 64 | 65 | return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 66 | } 67 | 68 | + (void)alert:(NSString *)message 69 | { 70 | return [Utilities alert:message title:@"Unbound"]; 71 | } 72 | 73 | + (void)alert:(NSString *)message title:(NSString *)title 74 | { 75 | return [Utilities 76 | alert:message 77 | title:title 78 | buttons:@[ 79 | [UIAlertAction actionWithTitle:@"Okay" style:UIAlertActionStyleDefault handler:nil], 80 | 81 | [UIAlertAction actionWithTitle:@"Join Server" 82 | style:UIAlertActionStyleDefault 83 | handler:^(UIAlertAction *action) { 84 | NSURL *URL = [NSURL 85 | URLWithString:@"https://discord.com/invite/rMdzhWUaGT"]; 86 | UIApplication *application = 87 | [UIApplication sharedApplication]; 88 | 89 | [application openURL:URL options:@{} completionHandler:nil]; 90 | }] 91 | ]]; 92 | } 93 | 94 | + (void)alert:(NSString *)message 95 | title:(NSString *)title 96 | buttons:(NSArray *)buttons 97 | { 98 | UIAlertController *alert = 99 | [UIAlertController alertControllerWithTitle:title 100 | message:message 101 | preferredStyle:UIAlertControllerStyleAlert]; 102 | 103 | for (UIAlertAction *button in buttons) 104 | { 105 | [alert addAction:button]; 106 | } 107 | 108 | dispatch_async(dispatch_get_main_queue(), ^{ 109 | UIViewController *controller = 110 | [[[[UIApplication sharedApplication] delegate] window] rootViewController]; 111 | [controller presentViewController:alert animated:YES completion:nil]; 112 | }); 113 | } 114 | 115 | + (id)parseJSON:(NSData *)data 116 | { 117 | NSError *error = nil; 118 | 119 | id object = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; 120 | 121 | if (error) 122 | { 123 | @throw error; 124 | } 125 | 126 | return object; 127 | } 128 | 129 | + (dispatch_source_t)createDebounceTimer:(double)delay 130 | queue:(dispatch_queue_t)queue 131 | block:(dispatch_block_t)block 132 | { 133 | dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); 134 | 135 | if (timer) 136 | { 137 | dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, delay * NSEC_PER_SEC), 138 | DISPATCH_TIME_FOREVER, (1ull * NSEC_PER_SEC) / 10); 139 | dispatch_source_set_event_handler(timer, block); 140 | dispatch_resume(timer); 141 | } 142 | 143 | return timer; 144 | } 145 | 146 | + (void *)getHermesSymbol:(const char *)symbol error:(NSString **)error 147 | { 148 | NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; 149 | NSString *executablePath = [bundlePath stringByAppendingPathComponent:@"Discord"]; 150 | 151 | void *handle = dlopen([executablePath UTF8String], RTLD_LAZY); 152 | if (!handle) 153 | { 154 | if (error) 155 | *error = [NSString stringWithUTF8String:dlerror()]; 156 | return NULL; 157 | } 158 | 159 | void *sym = dlsym(handle, symbol); 160 | if (!sym) 161 | { 162 | if (error) 163 | *error = [NSString stringWithUTF8String:dlerror()]; 164 | dlclose(handle); 165 | return NULL; 166 | } 167 | 168 | return sym; 169 | } 170 | 171 | + (uint32_t)getHermesBytecodeVersion 172 | { 173 | NSString *error = nil; 174 | uint32_t (*getBytecodeVersion)() = (uint32_t (*)()) 175 | [self getHermesSymbol:"_ZN8facebook6hermes13HermesRuntime18getBytecodeVersionEv" 176 | error:&error]; 177 | 178 | if (!getBytecodeVersion) 179 | { 180 | [Logger error:LOG_CATEGORY_UTILITIES format:@"Failed to get bytecode version: %@", error]; 181 | return 0; 182 | } 183 | 184 | return getBytecodeVersion(); 185 | } 186 | 187 | + (BOOL)isHermesBytecode:(NSData *)data 188 | { 189 | NSString *error = nil; 190 | BOOL (*isHermesBytecode)(const uint8_t *, size_t) = (BOOL (*)(const uint8_t *, size_t)) 191 | [self getHermesSymbol:"_ZN8facebook6hermes13HermesRuntime16isHermesBytecodeEPKhm" 192 | error:&error]; 193 | 194 | if (!isHermesBytecode) 195 | { 196 | [Logger error:LOG_CATEGORY_UTILITIES format:@"Failed to check Hermes bytecode: %@", error]; 197 | return NO; 198 | } 199 | 200 | return isHermesBytecode((const uint8_t *) [data bytes], [data length]); 201 | } 202 | 203 | + (BOOL)isAppStoreApp 204 | { 205 | return [[NSFileManager defaultManager] 206 | fileExistsAtPath:[[NSBundle mainBundle] appStoreReceiptURL].path]; 207 | } 208 | 209 | + (BOOL)isJailbroken 210 | { 211 | return [[NSFileManager defaultManager] fileExistsAtPath:@"/var/jb"]; 212 | } 213 | 214 | + (NSString *)getDeviceModelIdentifier 215 | { 216 | struct utsname systemInfo; 217 | uname(&systemInfo); 218 | return [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; 219 | } 220 | 221 | + (BOOL)deviceHasDynamicIsland 222 | { 223 | NSString *identifier = [self getDeviceModelIdentifier]; 224 | NSArray *dynamicIslandDevices = @[ 225 | @"iPhone15,2", @"iPhone15,3", @"iPhone15,4", @"iPhone15,5", @"iPhone16,1", @"iPhone16,2", 226 | @"iPhone17,1", @"iPhone17,2", @"iPhone17,3", @"iPhone17,4" 227 | ]; 228 | 229 | return [dynamicIslandDevices containsObject:identifier]; 230 | } 231 | 232 | + (UIImage *)createLogoImage 233 | { 234 | CGFloat size = 512.0; 235 | UIGraphicsBeginImageContextWithOptions(CGSizeMake(size, size), NO, 0); 236 | 237 | [[UIColor whiteColor] setFill]; 238 | 239 | UIBezierPath *rightPath = [UIBezierPath bezierPath]; 240 | [rightPath moveToPoint:CGPointMake(272.52, 177.27)]; 241 | [rightPath addLineToPoint:CGPointMake(277.81, 215.63)]; 242 | [rightPath addLineToPoint:CGPointMake(338.67, 215.74)]; 243 | [rightPath addLineToPoint:CGPointMake(338.67, 215.83)]; 244 | [rightPath addCurveToPoint:CGPointMake(373.01, 240.88) 245 | controlPoint1:CGPointMake(345.73, 216.18) 246 | controlPoint2:CGPointMake(359.97, 225.73)]; 247 | [rightPath addLineToPoint:CGPointMake(349.25, 240.88)]; 248 | [rightPath addCurveToPoint:CGPointMake(333.37, 260.06) 249 | controlPoint1:CGPointMake(345.04, 240.88) 250 | controlPoint2:CGPointMake(333.37, 249.47)]; 251 | [rightPath addCurveToPoint:CGPointMake(349.25, 279.24) 252 | controlPoint1:CGPointMake(333.37, 270.65) 253 | controlPoint2:CGPointMake(345.04, 279.24)]; 254 | [rightPath addLineToPoint:CGPointMake(376.41, 279.24)]; 255 | [rightPath addCurveToPoint:CGPointMake(338.67, 313.64) 256 | controlPoint1:CGPointMake(373.86, 288.78) 257 | controlPoint2:CGPointMake(357.41, 308.18)]; 258 | [rightPath addLineToPoint:CGPointMake(338.67, 313.75)]; 259 | [rightPath addLineToPoint:CGPointMake(297.66, 313.64)]; 260 | [rightPath addLineToPoint:CGPointMake(302.95, 351.9)]; 261 | [rightPath addLineToPoint:CGPointMake(338.67, 352.01)]; 262 | [rightPath addCurveToPoint:CGPointMake(416.94, 279.23) 263 | controlPoint1:CGPointMake(378, 352.01) 264 | controlPoint2:CGPointMake(410.64, 320.52)]; 265 | [rightPath addLineToPoint:CGPointMake(473.61, 279.14)]; 266 | [rightPath addLineToPoint:CGPointMake(489.48, 240.77)]; 267 | [rightPath addLineToPoint:CGPointMake(415.05, 240.88)]; 268 | [rightPath addCurveToPoint:CGPointMake(338.67, 177.38) 269 | controlPoint1:CGPointMake(405.63, 204.23) 270 | controlPoint2:CGPointMake(375, 177.38)]; 271 | [rightPath addLineToPoint:CGPointMake(272.52, 177.27)]; 272 | [rightPath closePath]; 273 | 274 | UIBezierPath *leftPath = [UIBezierPath bezierPath]; 275 | [leftPath moveToPoint:CGPointMake(164.04, 160.07)]; 276 | [leftPath addCurveToPoint:CGPointMake(87.66, 223.57) 277 | controlPoint1:CGPointMake(127.71, 160.07) 278 | controlPoint2:CGPointMake(97.08, 186.92)]; 279 | [leftPath addLineToPoint:CGPointMake(41.01, 223.57)]; 280 | [leftPath addLineToPoint:CGPointMake(25.14, 261.94)]; 281 | [leftPath addLineToPoint:CGPointMake(85.77, 261.94)]; 282 | [leftPath addCurveToPoint:CGPointMake(164.04, 334.7) 283 | controlPoint1:CGPointMake(92.07, 303.24) 284 | controlPoint2:CGPointMake(124.7, 334.7)]; 285 | [leftPath addLineToPoint:CGPointMake(243.41, 334.7)]; 286 | [leftPath addLineToPoint:CGPointMake(238.12, 296.34)]; 287 | [leftPath addLineToPoint:CGPointMake(164.04, 296.34)]; 288 | [leftPath addLineToPoint:CGPointMake(164.04, 296.26)]; 289 | [leftPath addCurveToPoint:CGPointMake(126.3, 261.94) 290 | controlPoint1:CGPointMake(145.3, 296.01) 291 | controlPoint2:CGPointMake(128.85, 271.48)]; 292 | [leftPath addLineToPoint:CGPointMake(153.46, 261.94)]; 293 | [leftPath addCurveToPoint:CGPointMake(169.33, 242.76) 294 | controlPoint1:CGPointMake(157.67, 261.94) 295 | controlPoint2:CGPointMake(169.33, 253.35)]; 296 | [leftPath addCurveToPoint:CGPointMake(153.46, 223.57) 297 | controlPoint1:CGPointMake(169.33, 232.17) 298 | controlPoint2:CGPointMake(157.67, 223.57)]; 299 | [leftPath addLineToPoint:CGPointMake(129.7, 223.57)]; 300 | [leftPath addCurveToPoint:CGPointMake(164.04, 198.44) 301 | controlPoint1:CGPointMake(133.15, 216.35) 302 | controlPoint2:CGPointMake(147.27, 198.89)]; 303 | [leftPath addLineToPoint:CGPointMake(164.04, 198.44)]; 304 | [leftPath addLineToPoint:CGPointMake(219.6, 198.44)]; 305 | [leftPath addLineToPoint:CGPointMake(214.3, 160.07)]; 306 | [leftPath addLineToPoint:CGPointMake(164.04, 160.07)]; 307 | [leftPath closePath]; 308 | 309 | [rightPath fill]; 310 | [leftPath fill]; 311 | 312 | UIImage *result = UIGraphicsGetImageFromCurrentImageContext(); 313 | UIGraphicsEndImageContext(); 314 | 315 | return result; 316 | } 317 | 318 | + (void)showDynamicIslandOverlay 319 | { 320 | if (!islandOverlayView) 321 | { 322 | [self createDynamicIslandOverlayView]; 323 | } 324 | 325 | if (islandOverlayView && !islandOverlayView.hidden && islandOverlayView.alpha >= 1.0) 326 | { 327 | return; 328 | } 329 | 330 | islandOverlayView.hidden = NO; 331 | 332 | [UIView animateWithDuration:0.2 animations:^{ islandOverlayView.alpha = 1.0; }]; 333 | 334 | [Logger debug:LOG_CATEGORY_UTILITIES format:@"Showing Dynamic Island overlay"]; 335 | } 336 | 337 | + (void)hideDynamicIslandOverlay 338 | { 339 | if (!islandOverlayView || islandOverlayView.hidden) 340 | { 341 | return; 342 | } 343 | 344 | islandOverlayView.hidden = YES; 345 | islandOverlayView.alpha = 0.0; 346 | 347 | [islandOverlayView.superview setNeedsLayout]; 348 | [islandOverlayView.superview layoutIfNeeded]; 349 | 350 | [Logger debug:LOG_CATEGORY_UTILITIES format:@"Hiding Dynamic Island overlay"]; 351 | } 352 | 353 | + (void)createDynamicIslandOverlayView 354 | { 355 | if (islandOverlayView) 356 | { 357 | [Logger debug:LOG_CATEGORY_UTILITIES 358 | format:@"Island overlay view already exists, skipping creation"]; 359 | return; 360 | } 361 | 362 | CGFloat width = 126.0; 363 | CGFloat height = 37.33; 364 | 365 | CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; 366 | CGFloat x = (screenWidth - width) / 2; 367 | CGFloat y = 11.0; 368 | 369 | [Logger debug:LOG_CATEGORY_UTILITIES 370 | format:@"Creating Dynamic Island overlay view at x:%f y:%f width:%f height:%f", x, y, 371 | width, height]; 372 | 373 | islandOverlayView = [[UIView alloc] initWithFrame:CGRectMake(x, y, width, height)]; 374 | islandOverlayView.backgroundColor = [UIColor blackColor]; 375 | islandOverlayView.alpha = 0.0; 376 | islandOverlayView.hidden = YES; 377 | 378 | islandOverlayView.userInteractionEnabled = NO; 379 | 380 | UIBezierPath *path = 381 | [UIBezierPath bezierPathWithRoundedRect:islandOverlayView.bounds 382 | byRoundingCorners:UIRectCornerAllCorners 383 | cornerRadii:CGSizeMake(height / 2, height / 2)]; 384 | 385 | CAShapeLayer *maskLayer = [CAShapeLayer layer]; 386 | maskLayer.path = path.CGPath; 387 | islandOverlayView.layer.mask = maskLayer; 388 | 389 | UIImage *logoImage = [self createLogoImage]; 390 | [Logger debug:LOG_CATEGORY_UTILITIES format:@"Created logo image for Dynamic Island overlay"]; 391 | 392 | UIImageView *logoView = [[UIImageView alloc] init]; 393 | logoView.image = logoImage; 394 | logoView.contentMode = UIViewContentModeScaleAspectFit; 395 | 396 | CGFloat logoHeight = height * 0.99; 397 | CGFloat aspectRatio = logoImage.size.width / logoImage.size.height; 398 | CGFloat logoWidth = logoHeight * aspectRatio; 399 | logoView.frame = 400 | CGRectMake((width - logoWidth) / 2, (height - logoHeight) / 2, logoWidth, logoHeight); 401 | 402 | [islandOverlayView addSubview:logoView]; 403 | 404 | UIWindow *keyWindow = nil; 405 | for (UIScene *scene in [UIApplication sharedApplication].connectedScenes) 406 | { 407 | if (scene.activationState == UISceneActivationStateForegroundActive) 408 | { 409 | keyWindow = ((UIWindowScene *) scene).windows.firstObject; 410 | break; 411 | } 412 | } 413 | 414 | if (keyWindow) 415 | { 416 | [keyWindow addSubview:islandOverlayView]; 417 | [keyWindow bringSubviewToFront:islandOverlayView]; 418 | [Logger info:LOG_CATEGORY_UTILITIES 419 | format:@"Successfully added Dynamic Island overlay to key window"]; 420 | } 421 | else 422 | { 423 | [Logger error:LOG_CATEGORY_UTILITIES 424 | format:@"Failed to find key window for Dynamic Island overlay"]; 425 | } 426 | } 427 | 428 | + (void)initializeDynamicIslandOverlay 429 | { 430 | [Logger info:LOG_CATEGORY_UTILITIES format:@"Checking if device has Dynamic Island..."]; 431 | 432 | if (![self deviceHasDynamicIsland]) 433 | { 434 | [Logger info:LOG_CATEGORY_UTILITIES 435 | format:@"Device does not have Dynamic Island, skipping overlay"]; 436 | return; 437 | } 438 | 439 | static BOOL isInitialized = NO; 440 | if (isInitialized) 441 | { 442 | [Logger info:LOG_CATEGORY_UTILITIES format:@"Dynamic Island overlay already initialized"]; 443 | return; 444 | } 445 | isInitialized = YES; 446 | 447 | [Logger info:LOG_CATEGORY_UTILITIES format:@"Setting up Dynamic Island overlay notifications"]; 448 | 449 | NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 450 | 451 | [center addObserverForName:UIApplicationDidBecomeActiveNotification 452 | object:nil 453 | queue:[NSOperationQueue mainQueue] 454 | usingBlock:^(NSNotification *note) { 455 | [Logger debug:LOG_CATEGORY_UTILITIES 456 | format:@"App did become active, showing overlay"]; 457 | dispatch_async(dispatch_get_main_queue(), 458 | ^{ [self showDynamicIslandOverlay]; }); 459 | }]; 460 | 461 | [center addObserverForName:UIApplicationWillResignActiveNotification 462 | object:nil 463 | queue:[NSOperationQueue mainQueue] 464 | usingBlock:^(NSNotification *note) { 465 | [Logger debug:LOG_CATEGORY_UTILITIES 466 | format:@"App will resign active, hiding overlay"]; 467 | dispatch_async(dispatch_get_main_queue(), 468 | ^{ [self hideDynamicIslandOverlay]; }); 469 | }]; 470 | 471 | dispatch_after( 472 | dispatch_time(DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 473 | [Logger info:LOG_CATEGORY_UTILITIES format:@"Creating Dynamic Island overlay..."]; 474 | [self createDynamicIslandOverlayView]; 475 | 476 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC), 477 | dispatch_get_main_queue(), ^{ [self showDynamicIslandOverlay]; }); 478 | }); 479 | } 480 | 481 | @end -------------------------------------------------------------------------------- /sources/preload.js: -------------------------------------------------------------------------------- 1 | this.UNBOUND_SETTINGS = %@; 2 | this.UNBOUND_PLUGINS = %@; 3 | this.UNBOUND_THEMES = %@; 4 | this.UNBOUND_FONTS = %@; 5 | this.UNBOUND_AVAILABLE_FONTS = %@; 6 | 7 | this.UNBOUND_LOADER = { 8 | platform: 'iOS', 9 | origin: 'Substrate', 10 | version: VERSION_PLACEHOLDER 11 | }; 12 | --------------------------------------------------------------------------------