├── .github ├── RELEASE-TEMPLATE.md └── workflows │ ├── build.yml │ ├── deploy.yml │ ├── dev.yml │ ├── draft.yml │ ├── pre-release.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── archive └── js │ ├── TestFlight.request.v2.0.beta.js │ └── TestFlight.response.v2.0.beta.js ├── arguments-builder.config.ts ├── biome.json ├── package-lock.json ├── package.json ├── rspack.config.js ├── rspack.dev.config.js ├── src ├── function │ ├── database.mjs │ ├── detectPlatform.mjs │ └── setENV.mjs ├── request.dev.js ├── request.js ├── response.dev.js ├── response.js └── types.d.ts └── template ├── boxjs.settings.json ├── loon.handlebars ├── quantumultx.handlebars ├── shadowrocket.handlebars ├── stash.handlebars └── surge.handlebars /.github/RELEASE-TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### 🆕 New Features 2 | * none 3 | 4 | ### 🛠️ Bug Fixes 5 | * none 6 | 7 | ### 🔣 Dependencies 8 | * none 9 | 10 | ### ‼️ Breaking Changes 11 | * none 12 | 13 | ### 🔄 Other Changes 14 | * none 15 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | workflow_call: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | actions: read 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@main 16 | with: 17 | submodules: recursive 18 | token: ${{ secrets.SUBMODULE_TOKEN }} 19 | - name: Set up Node.js 20 | uses: actions/setup-node@main 21 | with: 22 | node-version: 'latest' 23 | cache: 'npm' 24 | - name: Install dependencies 25 | run: npm install 26 | env: 27 | NODE_AUTH_TOKEN: ${{ secrets.PACKAGE_TOKEN }} 28 | - name: Update local package.json version from release tag 29 | if: github.ref_type == 'tag' 30 | uses: BellCubeDev/update-package-version-by-release-tag@v2 31 | with: 32 | version: ${{ github.ref_name }} 33 | keep-v: "false" # If set to "true", will not remove any 'v' prefix from the version number. 34 | ignore-semver-check: "false" # If set to "true", will not check if the version number is a valid semver version. 35 | - name: Build scripts 36 | run: npm run build 37 | - name: Generate modules 38 | run: npm run build:args 39 | - name: Upload artifact 40 | uses: actions/upload-artifact@master 41 | with: 42 | name: artifact 43 | path: | 44 | CHANGELOG.md 45 | rulesets 46 | dist 47 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/heads 5 | branches: 6 | - dev 7 | 8 | permissions: 9 | actions: read 10 | contents: read 11 | 12 | jobs: 13 | debug: 14 | uses: ./.github/workflows/dev.yml 15 | secrets: inherit 16 | deploy: 17 | needs: debug 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Download artifact 21 | uses: actions/download-artifact@master 22 | with: 23 | name: artifact 24 | - name: Deploy 25 | uses: exuanbo/actions-deploy-gist@main 26 | with: 27 | token: ${{ secrets.GIST_TOKEN }} 28 | gist_id: 45827a0c304016dadad59d48333b24aa 29 | gist_description: " iRingo: ✈ TestFlight β" 30 | file_path: dist/request.bundle.js 31 | - name: Deploy 32 | uses: exuanbo/actions-deploy-gist@main 33 | with: 34 | token: ${{ secrets.GIST_TOKEN }} 35 | gist_id: 45827a0c304016dadad59d48333b24aa 36 | gist_description: " iRingo: ✈ TestFlight β" 37 | file_path: dist/response.bundle.js 38 | -------------------------------------------------------------------------------- /.github/workflows/dev.yml: -------------------------------------------------------------------------------- 1 | name: Dev 2 | on: 3 | workflow_call: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | actions: read 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@main 16 | with: 17 | submodules: recursive 18 | token: ${{ secrets.SUBMODULE_TOKEN }} 19 | ref: dev 20 | - name: Set up Node.js 21 | uses: actions/setup-node@main 22 | with: 23 | node-version: 'latest' 24 | cache: 'npm' 25 | - name: Install dependencies 26 | run: npm install 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.PACKAGE_TOKEN }} 29 | - name: Build 30 | run: npm run build:dev 31 | - name: Upload artifact 32 | uses: actions/upload-artifact@master 33 | with: 34 | name: artifact 35 | path: | 36 | CHANGELOG.md 37 | rulesets 38 | dist 39 | -------------------------------------------------------------------------------- /.github/workflows/draft.yml: -------------------------------------------------------------------------------- 1 | name: Draft 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/heads 5 | branches: 6 | - main 7 | 8 | permissions: 9 | actions: read 10 | contents: write 11 | 12 | jobs: 13 | build: 14 | uses: ./.github/workflows/build.yml 15 | secrets: inherit 16 | draft: 17 | needs: build 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Download artifact 21 | uses: actions/download-artifact@master 22 | with: 23 | name: artifact 24 | - name: Publish Draft 25 | uses: softprops/action-gh-release@v2 26 | with: 27 | body_path: CHANGELOG.md 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | files: | 30 | rulesets/* 31 | dist/* 32 | draft: true 33 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: Pre-Release 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/tags 5 | tags: 6 | - v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+ 7 | - v[0-9]+.[0-9]+.[0-9]+-beta.[0-9]+ 8 | 9 | permissions: 10 | actions: read 11 | contents: write 12 | 13 | jobs: 14 | build: 15 | uses: ./.github/workflows/build.yml 16 | secrets: inherit 17 | pre-release: 18 | needs: build 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Download artifact 22 | uses: actions/download-artifact@master 23 | with: 24 | name: artifact 25 | - name: Publish Pre-Release 26 | uses: softprops/action-gh-release@v2 27 | with: 28 | body_path: CHANGELOG.md 29 | token: ${{ secrets.GITHUB_TOKEN }} 30 | files: | 31 | rulesets/* 32 | dist/* 33 | prerelease: true 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | # Sequence of patterns matched against refs/tags 5 | tags: 6 | - v[0-9]+.[0-9]+.[0-9]+ 7 | 8 | permissions: 9 | actions: read 10 | contents: write 11 | 12 | jobs: 13 | build: 14 | uses: ./.github/workflows/build.yml 15 | secrets: inherit 16 | release: 17 | needs: build 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Download artifact 21 | uses: actions/download-artifact@master 22 | with: 23 | name: artifact 24 | - name: Publish Release 25 | uses: softprops/action-gh-release@v2 26 | with: 27 | body_path: CHANGELOG.md 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | files: | 30 | rulesets/* 31 | dist/* 32 | make_latest: "true" 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | @nsnanocat:registry=https://npm.pkg.github.com/ 3 | //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN} 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 🆕 New Features 2 | * 新增了`总是显示安装选项`功能,可以在设置中启用 3 | * 当启用此功能时,通过 Testflight 邀请加入链接的用户将始终看到`安装`选项,而不是`此 Beta 版本的测试员已满。`的提示 4 | * 连续点击`安装`按钮不会触发风控 5 | * 新增了`合并通知开关`功能,可以在设置中启用 6 | * 当启用此功能时,`通知`-`电子邮件通知`的开关将始终控制全部平台版本的通知 7 | * 用户可以通过这个功能在 iOS 平台上同步开关 tvOS 与 macOS 版本的通知 8 | 9 | ### 🔣 Dependencies 10 | * 升级了 `@nsnanocat/util` 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #  iRingo: ✈ TestFlight 2 | -------------------------------------------------------------------------------- /archive/js/TestFlight.request.v2.0.beta.js: -------------------------------------------------------------------------------- 1 | /* 2 | README:https://github.com/VirgilClyne/iRingo 3 | */ 4 | const $ = new Env(" iRingo: ✈ TestFlight v2.0.0(1) request.beta"); 5 | const URL = new URLs(); 6 | const DataBase = { 7 | "Location":{ 8 | "Settings":{"Switch":"true","PEP":{"GCC":"US"},"Services":{"PlaceData":"CN","Directions":"AUTO","Traffic":"AUTO","RAP":"XX","Tiles":"AUTO"},"Geo_manifest":{"Dynamic":{"Config":{"Country_code":{"default":"AUTO","iOS":"CN","iPadOS":"CN","watchOS":"US","macOS":"CN"}}}},"Config":{"Announcements":{"Environment:":{"default":"AUTO","iOS":"CN","iPadOS":"CN","watchOS":"XX","macOS":"CN"}},"Defaults":{"LagunaBeach":true,"DrivingMultiWaypointRoutesEnabled":true,"GEOAddressCorrection":true,"LookupMaxParametersCount":true,"LocalitiesAndLandmarks":true,"POIBusyness":true,"PedestrianAR":true,"6694982d2b14e95815e44e970235e230":true,"OpticalHeading":true,"UseCLPedestrianMapMatchedLocations":true,"TransitPayEnabled":true,"WiFiQualityNetworkDisabled":false,"WiFiQualityTileDisabled":false}}} 9 | }, 10 | "Weather":{ 11 | "Settings":{"Switch":"true","NextHour":{"Switch":true},"AQI":{"Switch":true,"Mode":"WAQI Public","Location":"Station","Auth":null,"Scale":"EPA_NowCast.2204"},"Map":{"AQI":false}}, 12 | "Configs":{ 13 | "Availability":["currentWeather","forecastDaily","forecastHourly","history","weatherChange","forecastNextHour","severeWeather","airQuality"], 14 | "Pollutants":{"co":"CO","no":"NO","no2":"NO2","so2":"SO2","o3":"OZONE","nox":"NOX","pm25":"PM2.5","pm10":"PM10","other":"OTHER"} 15 | } 16 | }, 17 | "Siri":{ 18 | "Settings":{"Switch":"true","CountryCode":"SG","Domains":["web","itunes","app_store","movies","restaurants","maps"],"Functions":["flightutilities","lookup","mail","messages","news","safari","siri","spotlight","visualintelligence"],"Safari_Smart_History":true}, 19 | "Configs":{ 20 | "VisualIntelligence":{"enabled_domains":["pets","media","books","art","nature","landmarks"],"supported_domains":["OBJECT_2D","SCULPTURE","ART","CATS","DOGS","LANDMARK","ALBUM","SKYLINE","BIRDS","NATURE","ANIMALS","INSECTS","BOOK","MEDIA","NATURAL_LANDMARK"]} 21 | } 22 | }, 23 | "TV":{ 24 | "Settings": { 25 | "Switch": true,"Third-Party": true,"Tabs":["WatchNow","Originals","Store","Movies","TV","Sports","Kids","Library","Search"], 26 | "CountryCode":{"Configs":"AUTO","Settings":"AUTO","View":["SG","TW"],"WatchNow":"AUTO","Channels":"AUTO","Originals":"TW","Movies":"AUTO","TV":"AUTO","Sports":"US","Kids":"US","Persons":"SG","Search":"TW","Others":"AUTO"} 27 | }, 28 | "Configs":{ 29 | "Locale":[["AU","en-AU"],["CA","en-CA"],["GB","en-GB"],["KR","ko-KR"],["HK","yue-Hant"],["JP","ja-JP"],["MO","zh-Hant"],["TW","zh-Hant"],["US","en-US"],["SG","zh-Hans"]], 30 | "Tabs": [ 31 | { "title": "立即观看", "type": "WatchNow", "universalLinks": ["https://tv.apple.com/watch-now"], "destinationType": "Target", "target": { "id": "tahoma_watchnow", "type": "Root", "url": "https://tv.apple.com/watch-now" } }, 32 | { "title": "原创内容", "type": "Originals", "universalLinks": ["https://tv.apple.com/channel/tvs.sbd.4000", "https://tv.apple.com/atv"], "destinationType": "Target", "target": { "id": "tvs.sbd.4000", "type": "Brand", "url": "https://tv.apple.com/us/channel/tvs.sbd.4000" } }, 33 | { "title": "电影", "type": "Movies", "universalLinks": ["https://tv.apple.com/movies"], "destinationType": "Target", "target": { "id": "tahoma_movies", "type": "Root", "url": "https://tv.apple.com/movies" } }, 34 | { "title": "电视节目", "type": "TV", "universalLinks": ["https://tv.apple.com/tv-shows"], "destinationType": "Target", "target": { "id": "tahoma_tvshows", "type": "Root", "url": "https://tv.apple.com/tv-shows" } }, 35 | { "title": "商店", "type": "Store", "universalLinks": ["https://tv.apple.com/store"], "destinationType": "SubTabs", 36 | "subTabs": [ 37 | { "title": "电影", "type": "Movies", "universalLinks": ["https://tv.apple.com/movies"], "destinationType": "Target", "target": { "id": "tahoma_movies", "type": "Root", "url": "https://tv.apple.com/movies" } }, 38 | { "title": "电视节目", "type": "TV", "universalLinks": ["https://tv.apple.com/tv-shows"], "destinationType": "Target", "target": { "id": "tahoma_tvshows", "type": "Root", "url": "https://tv.apple.com/tv-shows" } } 39 | ] 40 | }, 41 | { "title": "体育节目", "type": "Sports", "universalLinks": ["https://tv.apple.com/sports"], "destinationType": "Target", "target": { "id": "tahoma_sports", "type": "Root", "url": "https://tv.apple.com/sports" } }, 42 | { "title": "儿童", "type": "Kids", "universalLinks": ["https://tv.apple.com/kids"], "destinationType": "Target", "target": { "id": "tahoma_kids", "type": "Root", "url": "https://tv.apple.com/kids" } }, 43 | { "title": "资料库", "type": "Library", "destinationType": "Client" }, 44 | { "title": "搜索", "type": "Search", "universalLinks": ["https://tv.apple.com/search"], "destinationType": "Target", "target": { "id": "tahoma_search", "type": "Root", "url": "https://tv.apple.com/search" } } 45 | ], 46 | "i18n": { 47 | "WatchNow": [["en", "Watch Now"], ["zh", "立即观看"], ["zh-Hans", "立即观看"], ["zh-Hant", "立即觀看"]], 48 | "Originals": [["en", "Originals"], ["zh", "原创内容"], ["zh-Hans", "原创内容"], ["zh-Hant", "原創內容"]], 49 | "Movies": [["en", "Movies"], ["zh", "电影"], ["zh-Hans", "电影"], ["zh-Hant", "電影"]], 50 | "TV": [["en", "TV"], ["zh", "电视节目"], ["zh-Hans", "电视节目"], ["zh-Hant", "電視節目"]], 51 | "Store": [["en", "Store"], ["zh", "商店"], ["zh-Hans", "商店"], ["zh-Hant", "商店"]], 52 | "Sports": [["en", "Sports"], ["zh", "体育节目"], ["zh-Hans", "体育节目"], ["zh-Hant", "體育節目"]], 53 | "Kids": [["en", "Kids"], ["zh", "儿童"], ["zh-Hans", "儿童"], ["zh-Hant", "兒童"]], 54 | "Library": [["en", "Library"], ["zh", "资料库"], ["zh-Hans", "资料库"], ["zh-Hant", "資料庫"]], 55 | "Search": [["en", "Search"], ["zh", "搜索"], ["zh-Hans", "搜索"], ["zh-Hant", "蒐索"]] 56 | } 57 | } 58 | }, 59 | "News":{ 60 | "Settings":{"Switch":"true","CountryCode":"US","newsPlusUser":"true"} 61 | }, 62 | "TestFlight":{ 63 | "Settings":{"Switch":"true","CountryCode":"US","MultiAccount":"false","Universal":"true"} 64 | }, 65 | "Default": { 66 | "Settings":{"Switch":"true"}, 67 | "Configs":{ 68 | "Storefront":[["AE","143481"],["AF","143610"],["AG","143540"],["AI","143538"],["AL","143575"],["AM","143524"],["AO","143564"],["AR","143505"],["AT","143445"],["AU","143460"],["AZ","143568"],["BA","143612"],["BB","143541"],["BD","143490"],["BE","143446"],["BF","143578"],["BG","143526"],["BH","143559"],["BJ","143576"],["BM","143542"],["BN","143560"],["BO","143556"],["BR","143503"],["BS","143539"],["BT","143577"],["BW","143525"],["BY","143565"],["BZ","143555"],["CA","143455"],["CD","143613"],["CG","143582"],["CH","143459"],["CI","143527"],["CL","143483"],["CM","143574"],["CN","143465"],["CO","143501"],["CR","143495"],["CV","143580"],["CY","143557"],["CZ","143489"],["DE","143443"],["DK","143458"],["DM","143545"],["DO","143508"],["DZ","143563"],["EC","143509"],["EE","143518"],["EG","143516"],["ES","143454"],["FI","143447"],["FJ","143583"],["FM","143591"],["FR","143442"],["GA","143614"],["GB","143444"],["GD","143546"],["GF","143615"],["GH","143573"],["GM","143584"],["GR","143448"],["GT","143504"],["GW","143585"],["GY","143553"],["HK","143463"],["HN","143510"],["HR","143494"],["HU","143482"],["ID","143476"],["IE","143449"],["IL","143491"],["IN","143467"],["IQ","143617"],["IS","143558"],["IT","143450"],["JM","143511"],["JO","143528"],["JP","143462"],["KE","143529"],["KG","143586"],["KH","143579"],["KN","143548"],["KP","143466"],["KR","143466"],["KW","143493"],["KY","143544"],["KZ","143517"],["TC","143552"],["TD","143581"],["TJ","143603"],["TH","143475"],["TM","143604"],["TN","143536"],["TO","143608"],["TR","143480"],["TT","143551"],["TW","143470"],["TZ","143572"],["LA","143587"],["LB","143497"],["LC","143549"],["LI","143522"],["LK","143486"],["LR","143588"],["LT","143520"],["LU","143451"],["LV","143519"],["LY","143567"],["MA","143620"],["MD","143523"],["ME","143619"],["MG","143531"],["MK","143530"],["ML","143532"],["MM","143570"],["MN","143592"],["MO","143515"],["MR","143590"],["MS","143547"],["MT","143521"],["MU","143533"],["MV","143488"],["MW","143589"],["MX","143468"],["MY","143473"],["MZ","143593"],["NA","143594"],["NE","143534"],["NG","143561"],["NI","143512"],["NL","143452"],["NO","143457"],["NP","143484"],["NR","143606"],["NZ","143461"],["OM","143562"],["PA","143485"],["PE","143507"],["PG","143597"],["PH","143474"],["PK","143477"],["PL","143478"],["PT","143453"],["PW","143595"],["PY","143513"],["QA","143498"],["RO","143487"],["RS","143500"],["RU","143469"],["RW","143621"],["SA","143479"],["SB","143601"],["SC","143599"],["SE","143456"],["SG","143464"],["SI","143499"],["SK","143496"],["SL","143600"],["SN","143535"],["SR","143554"],["ST","143598"],["SV","143506"],["SZ","143602"],["UA","143492"],["UG","143537"],["US","143441"],["UY","143514"],["UZ","143566"],["VC","143550"],["VE","143502"],["VG","143543"],["VN","143471"],["VU","143609"],["XK","143624"],["YE","143571"],["ZA","143472"],["ZM","143622"],["ZW","143605"]] 69 | } 70 | } 71 | }; 72 | 73 | // 构造回复数据 74 | let $response = undefined; 75 | 76 | /***************** Processing *****************/ 77 | (async () => { 78 | const { Settings, Caches, Configs } = setENV("iRingo", "TestFlight", DataBase); 79 | $.log(`⚠ ${$.name}`, `Settings.Switch: ${Settings?.Switch}`, ""); 80 | switch (Settings?.Switch) { 81 | case "true": 82 | default: 83 | let url = URL.parse($request?.url); 84 | const METHOD = $request?.method, HOST = url?.host, PATH = url?.path, PATHs = PATH.split("/"); 85 | // 解析格式 86 | const FORMAT = ($request?.headers?.["Content-Type"] ?? $request?.headers?.["content-type"])?.split(";")?.[0]; 87 | $.log(`⚠ ${$.name}`, `METHOD: ${METHOD}`, `HOST: ${HOST}`, `PATH: ${PATH}`, `PATHs: ${PATHs}`, `FORMAT: ${FORMAT}`, ""); 88 | // 创建空数据 89 | let body = {}; 90 | // 方法判断 91 | switch (METHOD) { 92 | case "POST": 93 | case "PUT": 94 | case "PATCH": 95 | case "DELETE": 96 | // 格式判断 97 | switch (FORMAT) { 98 | case undefined: // 视为无body 99 | break; 100 | case "application/x-www-form-urlencoded": 101 | case "text/plain": 102 | case "text/html": 103 | default: 104 | break; 105 | case "text/xml": 106 | case "text/plist": 107 | case "application/xml": 108 | case "application/plist": 109 | case "application/x-plist": 110 | //body = await PLIST("plist2json", $request.body); 111 | //$.log(body); 112 | //$request.body = await PLIST("json2plist", body); 113 | break; 114 | case "text/json": 115 | case "application/json": 116 | body = JSON.parse($request.body); 117 | switch (HOST) { 118 | case "testflight.apple.com": 119 | switch (PATH) { 120 | case "v1/session/authenticate": 121 | /* 122 | if (Settings.storeCookies) { // 使用Cookies 123 | $.log(`🚧 ${$.name}, storeCookies`, ""); 124 | if (Caches?.dsId && Caches?.storeCookies) { // 有 DS ID和iTunes Store Cookie 125 | $.log(`🚧 ${$.name}, 有Caches, DS ID和iTunes Store Cookie`, ""); 126 | if (body.dsId !== Caches?.dsId) { // DS ID不相等,覆盖iTunes Store Cookie 127 | $.log(`🚧 ${$.name}, DS ID不相等,覆盖DS ID和iTunes Store Cookie`, ""); 128 | body.dsId = Caches.dsId; 129 | body.deviceModel = Caches.deviceModel; 130 | body.storeCookies = Caches.storeCookies; 131 | body.deviceVendorId = Caches.deviceVendorId; 132 | body.deviceName = Caches.deviceName; 133 | } else $.setjson({ ...Caches, ...body }, "@iRingo.TestFlight.Caches"); // DS ID相等,刷新缓存 134 | } else $.setjson({ ...Caches, ...body }, "@iRingo.TestFlight.Caches"); // Caches空 135 | } 136 | */ 137 | if (Settings.CountryCode !== "AUTO") body.storeFrontIdentifier = body.storeFrontIdentifier.replace(/\d{6}/, Configs.Storefront.get(Settings.CountryCode)); 138 | break; 139 | case "v1/devices": 140 | case "v1/devices/apns": 141 | case "v1/devices/add": 142 | case "v1/devices/remove": 143 | break; 144 | default: 145 | // v2.x 146 | switch (PATHs?.[0]) { 147 | case "v1": 148 | switch (PATHs?.[1]) { 149 | case "accounts": 150 | switch (PATHs?.[2]) { 151 | case "settings": 152 | switch (PATHs?.[3]) { 153 | case "notifications": 154 | switch (PATHs?.[4]) { 155 | case "apps": 156 | $.log(`🚧 ${$.name}, ${PATHs?.[0]}/accounts/settings/notifications/apps/`, ""); 157 | break; 158 | }; 159 | break; 160 | }; 161 | break; 162 | case Caches?.data?.accountId: // UUID 163 | default: 164 | switch (PATHs?.[3]) { 165 | case "apps": 166 | $.log(`🚧 ${$.name}, ${PATHs?.[0]}/accounts/${PATHs?.[2]}/apps/`, ""); 167 | break; 168 | }; 169 | break; 170 | }; 171 | break; 172 | case "apps": 173 | switch (PATHs?.[3]) { 174 | case "install": 175 | switch (PATHs?.[4]) { 176 | case undefined: 177 | $.log(`🚧 ${$.name}, ${PATHs?.[0]}/apps/install`, ""); 178 | break; 179 | case "status": 180 | $.log(`🚧 ${$.name}, ${PATHs?.[0]}/apps/install/status`, ""); 181 | break; 182 | }; 183 | break; 184 | }; 185 | break; 186 | }; 187 | break; 188 | case "v2": 189 | switch (PATHs?.[1]) { 190 | case "accounts": 191 | switch (PATHs?.[2]) { 192 | case Caches?.data?.accountId: // UUID 193 | default: 194 | switch (PATHs?.[3]) { 195 | case "apps": 196 | $.log(`🚧 ${$.name}, ${PATHs?.[0]}/accounts/${PATHs?.[2]}/apps/`, ""); 197 | switch (PATHs?.[5]) { 198 | case "builds": 199 | $.log(`🚧 ${$.name}, ${PATHs?.[0]}/accounts/${PATHs?.[2]}/apps/${PATHs?.[4]}/builds/`, ""); 200 | switch (PATHs?.[7]) { 201 | case undefined: 202 | $.log(`🚧 ${$.name}, ${PATHs?.[0]}/accounts/${PATHs?.[2]}/apps/${PATHs?.[4]}/builds/${PATHs?.[6]}`, ""); 203 | break; 204 | case "install": 205 | $.log(`🚧 ${$.name}, ${PATHs?.[0]}/accounts/${PATHs?.[2]}/apps/${PATHs?.[4]}/builds/${PATHs?.[6]}/install`, ""); 206 | if (Settings.CountryCode !== "AUTO") body.storefrontId = body.storefrontId.replace(/\d{6}/, Configs.Storefront.get(Settings.CountryCode)); 207 | break; 208 | }; 209 | break; 210 | }; 211 | break; 212 | }; 213 | break; 214 | }; 215 | break; 216 | }; 217 | break; 218 | case "v3": 219 | switch (PATHs?.[1]) { 220 | case "accounts": 221 | switch (PATHs?.[2]) { 222 | case Caches?.data?.accountId: // UUID 223 | default: 224 | switch (PATHs?.[3]) { 225 | case undefined: 226 | $.log(`🚧 ${$.name}, ${PATHs?.[0]}/accounts/${PATHs?.[2]}`, ""); 227 | break; 228 | case "apps": 229 | switch (PATHs?.[4]) { 230 | case undefined: 231 | $.log(`🚧 ${$.name}, ${PATHs?.[0]}/accounts/${PATHs?.[2]}/apps`, ""); 232 | break; 233 | default: 234 | $.log(`🚧 ${$.name}, ${PATHs?.[0]}/accounts/${PATHs?.[2]}/apps/${PATHs?.[4]}`, ""); 235 | break; 236 | }; 237 | break; 238 | }; 239 | break; 240 | }; 241 | break; 242 | }; 243 | break; 244 | }; 245 | // v1.x 246 | /* 247 | if (/\/accounts\//i.test(url.path)) { 248 | $.log(`🚧 ${$.name}, accounts`, ""); 249 | if (/\/settings\//i.test(url.path)) { 250 | $.log(`🚧 ${$.name}, settings`, ""); 251 | if (/\/notifications\/apps\/\d+$/i.test(url.path)) { 252 | $.log(`🚧 ${$.name}, notifications/apps`, ""); 253 | } else $.log(`🚧 ${$.name}, unknown`, ""); 254 | } else if (/\/apps/i.test(url.path)) { // app info mod 255 | $.log(`🚧 ${$.name}, /apps`, ""); 256 | if (/\/apps$/i.test(url.path)) { 257 | $.log(`🚧 ${$.name}, /apps`, ""); 258 | } else if (/\/apps\/\d+\/builds\/\d+$/i.test(url.path)) { 259 | $.log(`🚧 ${$.name}, /app/bulids`, ""); 260 | } else if (/\/apps\/\d+\/platforms\/\w+\/trains$/i.test(url.path)) { 261 | $.log(`🚧 ${$.name}, /app/platforms/trains`, ""); 262 | } else if (/\/apps\/\d+\/platforms\/\w+\/trains\/[\d.]+\/builds$/i.test(url.path)) { 263 | $.log(`🚧 ${$.name}, /app/platforms/trains/builds`, ""); 264 | } else if (/\/apps\/\d+\/builds\/\d+\/install$/i.test(url.path)) { 265 | $.log(`🚧 ${$.name}, /app/bulids/install`, ""); 266 | if (Settings.CountryCode !== "AUTO") body.storefrontId = body.storefrontId.replace(/\d{6}/, Configs.Storefront.get(Settings.CountryCode)); 267 | } else if (/\/apps\/\d+\/builds\/\d+\/install\/status$/i.test(url.path)) { 268 | $.log(`🚧 ${$.name}, /app/bulids/install/status`, ""); 269 | } else $.log(`🚧 ${$.name}, unknown`, ""); 270 | }; 271 | }; 272 | */ 273 | break; 274 | }; 275 | break; 276 | }; 277 | $request.body = JSON.stringify(body); 278 | break; 279 | case "application/x-protobuf": 280 | case "application/grpc": 281 | case "application/grpc+proto": 282 | case "applecation/octet-stream": 283 | break; 284 | }; 285 | //break; // 不中断,继续处理URL 286 | case "GET": 287 | case "HEAD": 288 | case "OPTIONS": 289 | // 主机判断 290 | switch (HOST) { 291 | case "testflight.apple.com": 292 | // 路径判断 293 | switch (PATH) { 294 | case "v1/properties/testflight": 295 | //$request.headers["X-Apple-Rosetta-Available"] = Settings.Rosetta; 296 | break; 297 | case `v1/messages/${Caches?.data?.accountId}`: 298 | case `v1/messages/${Caches?.data?.accountId}/read`: 299 | $.log(`🚧 ${$.name}, "accountId"相同,更新`, ""); 300 | Caches.headers = { 301 | "X-Request-Id": $request.headers["x-request-id"], 302 | "X-Session-Id": $request.headers["x-session-id"], 303 | "X-Session-Digest": $request.headers["x-session-digest"] 304 | }; 305 | $.setjson(Caches, "@iRingo.TestFlight.Caches"); 306 | break; 307 | default: 308 | // headers auth mod 309 | switch (Settings.MultiAccount) { // MultiAccount 310 | case "true": 311 | $.log(`🚧 ${$.name}, 启用多账号支持`, ""); 312 | if (Caches?.data) { // Caches.data存在` 313 | $.log(`🚧 ${$.name}, data存在`, ""); 314 | switch (/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/.exec(url.path)?.[0]) { 315 | case Caches?.data?.accountId: // url.path有UUID且与accountId相同 316 | $.log(`🚧 ${$.name}, accountId相同,更新`, ""); 317 | Caches.headers = { 318 | "X-Request-Id": $request.headers["x-request-id"], 319 | "X-Session-Id": $request.headers["x-session-id"], 320 | "X-Session-Digest": $request.headers["x-session-digest"] 321 | }; 322 | $.setjson(Caches, "@iRingo.TestFlight.Caches"); 323 | break; 324 | case undefined: // url.path没有UUID 325 | $.log(`🚧 ${$.name}, url.path没有UUID`, ""); 326 | if ($request.headers["x-session-id"] !== Caches.headers["X-Session-Id"]) { // sessionId不同 327 | $.log(`🚧 ${$.name}, sessionId不同,替换`, ""); 328 | if ($request?.headers?.["if-none-match"]) $request.headers["if-none-match"] = `\"${$request.headers["if-none-match"].replace(/\"/g, "")}_\"` 329 | $request.headers["x-request-id"] = Caches.headers["X-Request-Id"]; 330 | $request.headers["x-session-id"] = Caches.headers["X-Session-Id"]; 331 | $request.headers["x-session-digest"] = Caches.headers["X-Session-Digest"]; 332 | } 333 | break; 334 | default: // url.path有UUID但与accountId不同 335 | $.log(`🚧 ${$.name}, accountId不同,替换`, ""); 336 | url.path = url.path.replace(/\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\//i, `/${Caches.data.accountId}/`); 337 | if ($request?.headers?.["if-none-match"]) $request.headers["if-none-match"] = `\"${$request.headers["if-none-match"].replace(/\"/g, "")}_\"` 338 | $request.headers["x-request-id"] = Caches.data["X-Request-Id"]; 339 | $request.headers["x-session-id"] = Caches.data["X-Session-Id"]; 340 | $request.headers["x-session-digest"] = Caches.data["X-Session-Digest"]; 341 | break; 342 | } 343 | } else { // Caches空 344 | $.log(`🚧 ${$.name}, Caches空,写入`, ""); 345 | Caches.headers = { 346 | "X-Request-Id": $request.headers["x-request-id"], 347 | "X-Session-Id": $request.headers["x-session-id"], 348 | "X-Session-Digest": $request.headers["x-session-digest"] 349 | }; 350 | Caches.data = { 351 | "accountId": /[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/.exec(url.path)?.[0], 352 | "sessionId": $request.headers["x-session-id"] 353 | }; 354 | $.setjson(Caches, "@iRingo.TestFlight.Caches"); 355 | }; 356 | //break; // 不中断,继续处理 357 | case "false": 358 | default: 359 | // v1.x 360 | /* 361 | if (/\/accounts\//i.test(url.path)) { 362 | $.log(`🚧 ${$.name}, accounts`, ""); 363 | if (/\/settings\//i.test(url.path)) { 364 | $.log(`🚧 ${$.name}, settings`, ""); 365 | if (/\/notifications\/apps\/\d+$/i.test(url.path)) { 366 | $.log(`🚧 ${$.name}, notifications/apps`, ""); 367 | } else $.log(`🚧 ${$.name}, unknown`, ""); 368 | } else if (/\/apps/i.test(url.path)) { // app info mod 369 | $.log(`🚧 ${$.name}, /apps`, ""); 370 | if (/\/apps$/i.test(url.path)) { 371 | $.log(`🚧 ${$.name}, /apps`, ""); 372 | } else if (/\/apps\/\d+\/builds\/\d+$/i.test(url.path)) { 373 | $.log(`🚧 ${$.name}, /app/bulids`, ""); 374 | } else if (/\/apps\/\d+\/platforms\/\w+\/trains$/i.test(url.path)) { 375 | $.log(`🚧 ${$.name}, /app/platforms/trains`, ""); 376 | } else if (/\/apps\/\d+\/platforms\/\w+\/trains\/[\d.]+\/builds$/i.test(url.path)) { 377 | $.log(`🚧 ${$.name}, /app/platforms/trains/builds`, ""); 378 | } else if (/\/apps\/\d+\/builds\/\d+\/install$/i.test(url.path)) { 379 | $.log(`🚧 ${$.name}, /app/bulids/install`, ""); 380 | //let install = JSON.parse($request.body); 381 | //if (Settings.CountryCode !== "AUTO") install.storefrontId = install.storefrontId.replace(/\d{6}/, Configs.Storefront.get(Settings.CountryCode)); 382 | //$request.body = JSON.stringify(install); 383 | } else if (/\/apps\/\d+\/builds\/\d+\/install\/status$/i.test(url.path)) { 384 | $.log(`🚧 ${$.name}, /app/bulids/install/status`, ""); 385 | } else $.log(`🚧 ${$.name}, unknown`, ""); 386 | }; 387 | } else if (/\/ru\//i.test(url.path)) { 388 | $.log(`🚧 ${$.name}, /ru/`, ""); 389 | if (/\/app$/i.test(url.path)) { 390 | $.log(`🚧 ${$.name}, /app`, ""); 391 | } else if (/\/accept$/i.test(url.path)) { 392 | $.log(`🚧 ${$.name}, /accept`, ""); 393 | } else $.log(`🚧 ${$.name}, unknown`, ""); 394 | } else if (/\/invites\//i.test(url.path)) { 395 | $.log(`🚧 ${$.name}, /invites/`, ""); 396 | if (/\/app$/i.test(url.path)) { 397 | $.log(`🚧 ${$.name}, /app`, ""); 398 | } else if (/\/accept$/i.test(url.path)) { 399 | $.log(`🚧 ${$.name}, /accept`, ""); 400 | } else $.log(`🚧 ${$.name}, unknown`, ""); 401 | } else if (/\/messages\//i.test(url.path)) { 402 | $.log(`🚧 ${$.name}, /messages/`, ""); 403 | if (/\/read$/i.test(url.path)) { 404 | $.log(`🚧 ${$.name}, /read`, ""); 405 | } else $.log(`🚧 ${$.name}, unknown`, ""); 406 | }; 407 | */ 408 | break; 409 | }; 410 | break; 411 | }; 412 | break; 413 | }; 414 | if ($request?.headers?.Host) $request.headers.Host = url.host; 415 | $request.url = URL.stringify(url); 416 | $.log(`🚧 ${$.name}, 调试信息`, `$request.url: ${$request.url}`, ""); 417 | break; 418 | case "CONNECT": 419 | case "TRACE": 420 | break; 421 | }; 422 | break; 423 | case "false": 424 | break; 425 | }; 426 | })() 427 | .catch((e) => $.logErr(e)) 428 | .finally(() => { 429 | switch ($response) { 430 | default: { // 有构造回复数据,返回构造的回复数据 431 | const FORMAT = ($response?.headers?.["Content-Type"] ?? $response?.headers?.["content-type"])?.split(";")?.[0]; 432 | $.log(`🎉 ${$.name}, finally`, `echo $response`, `FORMAT: ${FORMAT}`, ""); 433 | //$.log(`🚧 ${$.name}, finally`, `echo $response: ${JSON.stringify($response)}`, ""); 434 | if ($response?.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; 435 | if ($response?.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; 436 | if ($.isQuanX()) { 437 | $response.status = "HTTP/1.1 200 OK"; 438 | delete $response?.headers?.["Content-Length"]; 439 | delete $response?.headers?.["content-length"]; 440 | delete $response?.headers?.["Transfer-Encoding"]; 441 | switch (FORMAT) { 442 | case undefined: // 视为无body 443 | // 返回普通数据 444 | $.done({ status: $response.status, headers: $response.headers }); 445 | break; 446 | case "application/x-www-form-urlencoded": 447 | case "text/plain": 448 | case "text/html": 449 | case "text/xml": 450 | case "text/plist": 451 | case "application/xml": 452 | case "application/plist": 453 | case "application/x-plist": 454 | case "text/json": 455 | case "application/json": 456 | default: 457 | // 返回普通数据 458 | $.done({ status: $response.status, headers: $response.headers, body: $response.body }); 459 | break; 460 | case "application/x-protobuf": 461 | case "application/grpc": 462 | case "application/grpc+proto": 463 | case "applecation/octet-stream": 464 | // 返回二进制数据 465 | //$.log(`${$response.bodyBytes.byteLength}---${$response.bodyBytes.buffer.byteLength}`); 466 | $.done({ status: $response.status, headers: $response.headers, bodyBytes: $response.bodyBytes }); 467 | break; 468 | }; 469 | } else $.done({ response: $response }); 470 | break; 471 | }; 472 | case undefined: { // 无构造回复数据,发送修改的请求数据 473 | const FORMAT = ($request?.headers?.["Content-Type"] ?? $request?.headers?.["content-type"])?.split(";")?.[0]; 474 | $.log(`🎉 ${$.name}, finally`, `$request`, `FORMAT: ${FORMAT}`, ""); 475 | //$.log(`🚧 ${$.name}, finally`, `$request: ${JSON.stringify($request)}`, ""); 476 | if ($.isQuanX()) { 477 | switch (FORMAT) { 478 | case undefined: // 视为无body 479 | // 返回普通数据 480 | $.done({ url: $request.url, headers: $request.headers }) 481 | break; 482 | case "application/x-www-form-urlencoded": 483 | case "text/plain": 484 | case "text/html": 485 | case "text/xml": 486 | case "text/plist": 487 | case "application/xml": 488 | case "application/plist": 489 | case "application/x-plist": 490 | case "text/json": 491 | case "application/json": 492 | default: 493 | // 返回普通数据 494 | $.done({ url: $request.url, headers: $request.headers, body: $request.body }) 495 | break; 496 | case "application/x-protobuf": 497 | case "application/grpc": 498 | case "application/grpc+proto": 499 | case "applecation/octet-stream": 500 | // 返回二进制数据 501 | //$.log(`${$request.bodyBytes.byteLength}---${$request.bodyBytes.buffer.byteLength}`); 502 | $.done({ url: $request.url, headers: $request.headers, bodyBytes: $request.bodyBytes.buffer.slice($request.bodyBytes.byteOffset, $request.bodyBytes.byteLength + $request.bodyBytes.byteOffset) }); 503 | break; 504 | }; 505 | } else $.done($request); 506 | break; 507 | }; 508 | }; 509 | }) 510 | 511 | /***************** Function *****************/ 512 | /** 513 | * Set Environment Variables 514 | * @author VirgilClyne 515 | * @param {String} name - Persistent Store Key 516 | * @param {String} platform - Platform Name 517 | * @param {Object} database - Default DataBase 518 | * @return {Object} { Settings, Caches, Configs } 519 | */ 520 | function setENV(name, platform, database) { 521 | $.log(`⚠ ${$.name}, Set Environment Variables`, ""); 522 | let { Settings, Caches, Configs } = getENV(name, platform, database); 523 | /***************** Prase *****************/ 524 | $.log(`🎉 ${$.name}, Set Environment Variables`, `Settings: ${typeof Settings}`, `Settings内容: ${JSON.stringify(Settings)}`, ""); 525 | /***************** Caches *****************/ 526 | /***************** Configs *****************/ 527 | Configs.Storefront = new Map(Configs.Storefront); 528 | return { Settings, Caches, Configs }; 529 | }; 530 | 531 | /***************** Env *****************/ 532 | // prettier-ignore 533 | // https://github.com/chavyleung/scripts/blob/master/Env.min.js 534 | function Env(t,e){class s{constructor(t){this.env=t}send(t,e="GET"){t="string"==typeof t?{url:t}:t;let s=this.get;return"POST"===e&&(s=this.post),new Promise((e,a)=>{s.call(this,t,(t,s,r)=>{t?a(t):e(s)})})}get(t){return this.send.call(this.env,t)}post(t){return this.send.call(this.env,t,"POST")}}return new class{constructor(t,e){this.name=t,this.http=new s(this),this.data=null,this.dataFile="box.dat",this.logs=[],this.isMute=!1,this.isNeedRewrite=!1,this.logSeparator="\n",this.encoding="utf-8",this.startTime=(new Date).getTime(),Object.assign(this,e),this.log("",`\ud83d\udd14${this.name}, \u5f00\u59cb!`)}getEnv(){return"undefined"!=typeof $environment&&$environment["surge-version"]?"Surge":"undefined"!=typeof $environment&&$environment["stash-version"]?"Stash":"undefined"!=typeof module&&module.exports?"Node.js":"undefined"!=typeof $task?"Quantumult X":"undefined"!=typeof $loon?"Loon":"undefined"!=typeof $rocket?"Shadowrocket":void 0}isNode(){return"Node.js"===this.getEnv()}isQuanX(){return"Quantumult X"===this.getEnv()}isSurge(){return"Surge"===this.getEnv()}isLoon(){return"Loon"===this.getEnv()}isShadowrocket(){return"Shadowrocket"===this.getEnv()}isStash(){return"Stash"===this.getEnv()}toObj(t,e=null){try{return JSON.parse(t)}catch{return e}}toStr(t,e=null){try{return JSON.stringify(t)}catch{return e}}getjson(t,e){let s=e;const a=this.getdata(t);if(a)try{s=JSON.parse(this.getdata(t))}catch{}return s}setjson(t,e){try{return this.setdata(JSON.stringify(t),e)}catch{return!1}}getScript(t){return new Promise(e=>{this.get({url:t},(t,s,a)=>e(a))})}runScript(t,e){return new Promise(s=>{let a=this.getdata("@chavy_boxjs_userCfgs.httpapi");a=a?a.replace(/\n/g,"").trim():a;let r=this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout");r=r?1*r:20,r=e&&e.timeout?e.timeout:r;const[i,o]=a.split("@"),n={url:`http://${o}/v1/scripting/evaluate`,body:{script_text:t,mock_type:"cron",timeout:r},headers:{"X-Key":i,Accept:"*/*"},timeout:r};this.post(n,(t,e,a)=>s(a))}).catch(t=>this.logErr(t))}loaddata(){if(!this.isNode())return{};{this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),a=!s&&this.fs.existsSync(e);if(!s&&!a)return{};{const a=s?t:e;try{return JSON.parse(this.fs.readFileSync(a))}catch(t){return{}}}}}writedata(){if(this.isNode()){this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),a=!s&&this.fs.existsSync(e),r=JSON.stringify(this.data);s?this.fs.writeFileSync(t,r):a?this.fs.writeFileSync(e,r):this.fs.writeFileSync(t,r)}}lodash_get(t,e,s){const a=e.replace(/\[(\d+)\]/g,".$1").split(".");let r=t;for(const t of a)if(r=Object(r)[t],void 0===r)return s;return r}lodash_set(t,e,s){return Object(t)!==t?t:(Array.isArray(e)||(e=e.toString().match(/[^.[\]]+/g)||[]),e.slice(0,-1).reduce((t,s,a)=>Object(t[s])===t[s]?t[s]:t[s]=Math.abs(e[a+1])>>0==+e[a+1]?[]:{},t)[e[e.length-1]]=s,t)}getdata(t){let e=this.getval(t);if(/^@/.test(t)){const[,s,a]=/^@(.*?)\.(.*?)$/.exec(t),r=s?this.getval(s):"";if(r)try{const t=JSON.parse(r);e=t?this.lodash_get(t,a,""):e}catch(t){e=""}}return e}setdata(t,e){let s=!1;if(/^@/.test(e)){const[,a,r]=/^@(.*?)\.(.*?)$/.exec(e),i=this.getval(a),o=a?"null"===i?null:i||"{}":"{}";try{const e=JSON.parse(o);this.lodash_set(e,r,t),s=this.setval(JSON.stringify(e),a)}catch(e){const i={};this.lodash_set(i,r,t),s=this.setval(JSON.stringify(i),a)}}else s=this.setval(t,e);return s}getval(t){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.read(t);case"Quantumult X":return $prefs.valueForKey(t);case"Node.js":return this.data=this.loaddata(),this.data[t];default:return this.data&&this.data[t]||null}}setval(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.write(t,e);case"Quantumult X":return $prefs.setValueForKey(t,e);case"Node.js":return this.data=this.loaddata(),this.data[e]=t,this.writedata(),!0;default:return this.data&&this.data[e]||null}}initGotEnv(t){this.got=this.got?this.got:require("got"),this.cktough=this.cktough?this.cktough:require("tough-cookie"),this.ckjar=this.ckjar?this.ckjar:new this.cktough.CookieJar,t&&(t.headers=t.headers?t.headers:{},void 0===t.headers.Cookie&&void 0===t.cookieJar&&(t.cookieJar=this.ckjar))}get(t,e=(()=>{})){switch(t.headers&&(delete t.headers["Content-Type"],delete t.headers["Content-Length"],delete t.headers["content-type"],delete t.headers["content-length"]),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient.get(t,(t,s,a)=>{!t&&s&&(s.body=a,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,a)});break;case"Quantumult X":this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:a,headers:r,body:i,bodyBytes:o}=t;e(null,{status:s,statusCode:a,headers:r,body:i,bodyBytes:o},i,o)},t=>e(t&&t.error||"UndefinedError"));break;case"Node.js":let s=require("iconv-lite");this.initGotEnv(t),this.got(t).on("redirect",(t,e)=>{try{if(t.headers["set-cookie"]){const s=t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString();s&&this.ckjar.setCookieSync(s,null),e.cookieJar=this.ckjar}}catch(t){this.logErr(t)}}).then(t=>{const{statusCode:a,statusCode:r,headers:i,rawBody:o}=t,n=s.decode(o,this.encoding);e(null,{status:a,statusCode:r,headers:i,rawBody:o,body:n},n)},t=>{const{message:a,response:r}=t;e(a,r,r&&s.decode(r.rawBody,this.encoding))})}}post(t,e=(()=>{})){const s=t.method?t.method.toLocaleLowerCase():"post";switch(t.body&&t.headers&&!t.headers["Content-Type"]&&!t.headers["content-type"]&&(t.headers["content-type"]="application/x-www-form-urlencoded"),t.headers&&(delete t.headers["Content-Length"],delete t.headers["content-length"]),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient[s](t,(t,s,a)=>{!t&&s&&(s.body=a,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,a)});break;case"Quantumult X":t.method=s,this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:a,headers:r,body:i,bodyBytes:o}=t;e(null,{status:s,statusCode:a,headers:r,body:i,bodyBytes:o},i,o)},t=>e(t&&t.error||"UndefinedError"));break;case"Node.js":let a=require("iconv-lite");this.initGotEnv(t);const{url:r,...i}=t;this.got[s](r,i).then(t=>{const{statusCode:s,statusCode:r,headers:i,rawBody:o}=t,n=a.decode(o,this.encoding);e(null,{status:s,statusCode:r,headers:i,rawBody:o,body:n},n)},t=>{const{message:s,response:r}=t;e(s,r,r&&a.decode(r.rawBody,this.encoding))})}}time(t,e=null){const s=e?new Date(e):new Date;let a={"M+":s.getMonth()+1,"d+":s.getDate(),"H+":s.getHours(),"m+":s.getMinutes(),"s+":s.getSeconds(),"q+":Math.floor((s.getMonth()+3)/3),S:s.getMilliseconds()};/(y+)/.test(t)&&(t=t.replace(RegExp.$1,(s.getFullYear()+"").substr(4-RegExp.$1.length)));for(let e in a)new RegExp("("+e+")").test(t)&&(t=t.replace(RegExp.$1,1==RegExp.$1.length?a[e]:("00"+a[e]).substr((""+a[e]).length)));return t}queryStr(t){let e="";for(const s in t){let a=t[s];null!=a&&""!==a&&("object"==typeof a&&(a=JSON.stringify(a)),e+=`${s}=${a}&`)}return e=e.substring(0,e.length-1),e}msg(e=t,s="",a="",r){const i=t=>{switch(typeof t){case void 0:return t;case"string":switch(this.getEnv()){case"Surge":case"Stash":default:return{url:t};case"Loon":case"Shadowrocket":return t;case"Quantumult X":return{"open-url":t};case"Node.js":return}case"object":switch(this.getEnv()){case"Surge":case"Stash":case"Shadowrocket":default:{let e=t.url||t.openUrl||t["open-url"];return{url:e}}case"Loon":{let e=t.openUrl||t.url||t["open-url"],s=t.mediaUrl||t["media-url"];return{openUrl:e,mediaUrl:s}}case"Quantumult X":{let e=t["open-url"]||t.url||t.openUrl,s=t["media-url"]||t.mediaUrl,a=t["update-pasteboard"]||t.updatePasteboard;return{"open-url":e,"media-url":s,"update-pasteboard":a}}case"Node.js":return}default:return}};if(!this.isMute)switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:$notification.post(e,s,a,i(r));break;case"Quantumult X":$notify(e,s,a,i(r));break;case"Node.js":}if(!this.isMuteLog){let t=["","==============\ud83d\udce3\u7cfb\u7edf\u901a\u77e5\ud83d\udce3=============="];t.push(e),s&&t.push(s),a&&t.push(a),console.log(t.join("\n")),this.logs=this.logs.concat(t)}}log(...t){t.length>0&&(this.logs=[...this.logs,...t]),console.log(t.join(this.logSeparator))}logErr(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t);break;case"Node.js":this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t.stack)}}wait(t){return new Promise(e=>setTimeout(e,t))}done(t={}){const e=(new Date).getTime(),s=(e-this.startTime)/1e3;switch(this.log("",`\ud83d\udd14${this.name}, \u7ed3\u675f! \ud83d\udd5b ${s} \u79d2`),this.log(),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:$done(t);break;case"Node.js":process.exit(1)}}}(t,e)} 535 | 536 | // https://github.com/DualSubs/URL/blob/main/URLs.embedded.min.js 537 | function URLs(s){return new class{constructor(s=[]){this.name="URL v1.0.2",this.opts=s,this.json={scheme:"",host:"",path:"",params:{}}}parse(s){let t=s.match(/(?.+):\/\/(?[^/]+)\/?(?[^?]+)?\??(?.*)?/)?.groups??null;return t?.path||(t.path=""),t?.params&&(t.params=Object.fromEntries(t.params.split("&").map((s=>s.split("="))))),t}stringify(s=this.json){return s?.params?s.scheme+"://"+s.host+"/"+s.path+"?"+Object.entries(s.params).map((s=>s.join("="))).join("&"):s.scheme+"://"+s.host+"/"+s.path}}(s)} 538 | 539 | /** 540 | * Get Environment Variables 541 | * @link https://github.com/VirgilClyne/VirgilClyne/blob/main/function/getENV/getENV.min.js 542 | * @author VirgilClyne 543 | * @param {String} t - Persistent Store Key 544 | * @param {String} e - Platform Name 545 | * @param {Object} n - Default Database 546 | * @return {Object} { Settings, Caches, Configs } 547 | */ 548 | function getENV(t,e,n){let i=$.getjson(t,n),s={};if("undefined"!=typeof $argument&&Boolean($argument)){let t=Object.fromEntries($argument.split("&").map((t=>t.split("="))));for(let e in t)l(s,e,t[e])}let g={...n?.Default?.Settings,...n?.[e]?.Settings,...i?.[e]?.Settings,...s},f={...n?.Default?.Configs,...n?.[e]?.Configs,...i?.[e]?.Configs},o=i?.[e]?.Caches||{};return"string"==typeof o&&(o=JSON.parse(o)),{Settings:g,Caches:o,Configs:f};function l(t,e,n){e.split(".").reduce(((t,i,s)=>t[i]=e.split(".").length===++s?n:t[i]||{}),t)}} 549 | -------------------------------------------------------------------------------- /archive/js/TestFlight.response.v2.0.beta.js: -------------------------------------------------------------------------------- 1 | /* 2 | README:https://github.com/VirgilClyne/iRingo 3 | */ 4 | const $ = new Env(" iRingo: ✈ TestFlight v2.0.0(1) response.beta"); 5 | const URL = new URLs(); 6 | const DataBase = { 7 | "Location":{ 8 | "Settings":{"Switch":"true","PEP":{"GCC":"US"},"Services":{"PlaceData":"CN","Directions":"AUTO","Traffic":"AUTO","RAP":"XX","Tiles":"AUTO"},"Geo_manifest":{"Dynamic":{"Config":{"Country_code":{"default":"AUTO","iOS":"CN","iPadOS":"CN","watchOS":"US","macOS":"CN"}}}},"Config":{"Announcements":{"Environment:":{"default":"AUTO","iOS":"CN","iPadOS":"CN","watchOS":"XX","macOS":"CN"}},"Defaults":{"LagunaBeach":true,"DrivingMultiWaypointRoutesEnabled":true,"GEOAddressCorrection":true,"LookupMaxParametersCount":true,"LocalitiesAndLandmarks":true,"POIBusyness":true,"PedestrianAR":true,"6694982d2b14e95815e44e970235e230":true,"OpticalHeading":true,"UseCLPedestrianMapMatchedLocations":true,"TransitPayEnabled":true,"WiFiQualityNetworkDisabled":false,"WiFiQualityTileDisabled":false}}} 9 | }, 10 | "Weather":{ 11 | "Settings":{"Switch":"true","NextHour":{"Switch":true},"AQI":{"Switch":true,"Mode":"WAQI Public","Location":"Station","Auth":null,"Scale":"EPA_NowCast.2204"},"Map":{"AQI":false}}, 12 | "Configs":{ 13 | "Availability":["currentWeather","forecastDaily","forecastHourly","history","weatherChange","forecastNextHour","severeWeather","airQuality"], 14 | "Pollutants":{"co":"CO","no":"NO","no2":"NO2","so2":"SO2","o3":"OZONE","nox":"NOX","pm25":"PM2.5","pm10":"PM10","other":"OTHER"} 15 | } 16 | }, 17 | "Siri":{ 18 | "Settings":{"Switch":"true","CountryCode":"SG","Domains":["web","itunes","app_store","movies","restaurants","maps"],"Functions":["flightutilities","lookup","mail","messages","news","safari","siri","spotlight","visualintelligence"],"Safari_Smart_History":true}, 19 | "Configs":{ 20 | "VisualIntelligence":{"enabled_domains":["pets","media","books","art","nature","landmarks"],"supported_domains":["OBJECT_2D","SCULPTURE","ART","CATS","DOGS","LANDMARK","ALBUM","SKYLINE","BIRDS","NATURE","ANIMALS","INSECTS","BOOK","MEDIA","NATURAL_LANDMARK"]} 21 | } 22 | }, 23 | "TV":{ 24 | "Settings": { 25 | "Switch": true,"Third-Party": true,"Tabs":["WatchNow","Originals","Store","Movies","TV","Sports","Kids","Library","Search"], 26 | "CountryCode":{"Configs":"AUTO","Settings":"AUTO","View":["SG","TW"],"WatchNow":"AUTO","Channels":"AUTO","Originals":"TW","Movies":"AUTO","TV":"AUTO","Sports":"US","Kids":"US","Persons":"SG","Search":"TW","Others":"AUTO"} 27 | }, 28 | "Configs":{ 29 | "Locale":[["AU","en-AU"],["CA","en-CA"],["GB","en-GB"],["KR","ko-KR"],["HK","yue-Hant"],["JP","ja-JP"],["MO","zh-Hant"],["TW","zh-Hant"],["US","en-US"],["SG","zh-Hans"]], 30 | "Tabs": [ 31 | { "title": "立即观看", "type": "WatchNow", "universalLinks": ["https://tv.apple.com/watch-now"], "destinationType": "Target", "target": { "id": "tahoma_watchnow", "type": "Root", "url": "https://tv.apple.com/watch-now" } }, 32 | { "title": "原创内容", "type": "Originals", "universalLinks": ["https://tv.apple.com/channel/tvs.sbd.4000", "https://tv.apple.com/atv"], "destinationType": "Target", "target": { "id": "tvs.sbd.4000", "type": "Brand", "url": "https://tv.apple.com/us/channel/tvs.sbd.4000" } }, 33 | { "title": "电影", "type": "Movies", "universalLinks": ["https://tv.apple.com/movies"], "destinationType": "Target", "target": { "id": "tahoma_movies", "type": "Root", "url": "https://tv.apple.com/movies" } }, 34 | { "title": "电视节目", "type": "TV", "universalLinks": ["https://tv.apple.com/tv-shows"], "destinationType": "Target", "target": { "id": "tahoma_tvshows", "type": "Root", "url": "https://tv.apple.com/tv-shows" } }, 35 | { "title": "商店", "type": "Store", "universalLinks": ["https://tv.apple.com/store"], "destinationType": "SubTabs", 36 | "subTabs": [ 37 | { "title": "电影", "type": "Movies", "universalLinks": ["https://tv.apple.com/movies"], "destinationType": "Target", "target": { "id": "tahoma_movies", "type": "Root", "url": "https://tv.apple.com/movies" } }, 38 | { "title": "电视节目", "type": "TV", "universalLinks": ["https://tv.apple.com/tv-shows"], "destinationType": "Target", "target": { "id": "tahoma_tvshows", "type": "Root", "url": "https://tv.apple.com/tv-shows" } } 39 | ] 40 | }, 41 | { "title": "体育节目", "type": "Sports", "universalLinks": ["https://tv.apple.com/sports"], "destinationType": "Target", "target": { "id": "tahoma_sports", "type": "Root", "url": "https://tv.apple.com/sports" } }, 42 | { "title": "儿童", "type": "Kids", "universalLinks": ["https://tv.apple.com/kids"], "destinationType": "Target", "target": { "id": "tahoma_kids", "type": "Root", "url": "https://tv.apple.com/kids" } }, 43 | { "title": "资料库", "type": "Library", "destinationType": "Client" }, 44 | { "title": "搜索", "type": "Search", "universalLinks": ["https://tv.apple.com/search"], "destinationType": "Target", "target": { "id": "tahoma_search", "type": "Root", "url": "https://tv.apple.com/search" } } 45 | ], 46 | "i18n": { 47 | "WatchNow": [["en", "Watch Now"], ["zh", "立即观看"], ["zh-Hans", "立即观看"], ["zh-Hant", "立即觀看"]], 48 | "Originals": [["en", "Originals"], ["zh", "原创内容"], ["zh-Hans", "原创内容"], ["zh-Hant", "原創內容"]], 49 | "Movies": [["en", "Movies"], ["zh", "电影"], ["zh-Hans", "电影"], ["zh-Hant", "電影"]], 50 | "TV": [["en", "TV"], ["zh", "电视节目"], ["zh-Hans", "电视节目"], ["zh-Hant", "電視節目"]], 51 | "Store": [["en", "Store"], ["zh", "商店"], ["zh-Hans", "商店"], ["zh-Hant", "商店"]], 52 | "Sports": [["en", "Sports"], ["zh", "体育节目"], ["zh-Hans", "体育节目"], ["zh-Hant", "體育節目"]], 53 | "Kids": [["en", "Kids"], ["zh", "儿童"], ["zh-Hans", "儿童"], ["zh-Hant", "兒童"]], 54 | "Library": [["en", "Library"], ["zh", "资料库"], ["zh-Hans", "资料库"], ["zh-Hant", "資料庫"]], 55 | "Search": [["en", "Search"], ["zh", "搜索"], ["zh-Hans", "搜索"], ["zh-Hant", "蒐索"]] 56 | } 57 | } 58 | }, 59 | "News":{ 60 | "Settings":{"Switch":"true","CountryCode":"US","newsPlusUser":"true"} 61 | }, 62 | "TestFlight":{ 63 | "Settings":{"Switch":"true","CountryCode":"US","MultiAccount":"false","Universal":"true"} 64 | }, 65 | "Default": { 66 | "Settings":{"Switch":"true"}, 67 | "Configs":{ 68 | "Storefront":[["AE","143481"],["AF","143610"],["AG","143540"],["AI","143538"],["AL","143575"],["AM","143524"],["AO","143564"],["AR","143505"],["AT","143445"],["AU","143460"],["AZ","143568"],["BA","143612"],["BB","143541"],["BD","143490"],["BE","143446"],["BF","143578"],["BG","143526"],["BH","143559"],["BJ","143576"],["BM","143542"],["BN","143560"],["BO","143556"],["BR","143503"],["BS","143539"],["BT","143577"],["BW","143525"],["BY","143565"],["BZ","143555"],["CA","143455"],["CD","143613"],["CG","143582"],["CH","143459"],["CI","143527"],["CL","143483"],["CM","143574"],["CN","143465"],["CO","143501"],["CR","143495"],["CV","143580"],["CY","143557"],["CZ","143489"],["DE","143443"],["DK","143458"],["DM","143545"],["DO","143508"],["DZ","143563"],["EC","143509"],["EE","143518"],["EG","143516"],["ES","143454"],["FI","143447"],["FJ","143583"],["FM","143591"],["FR","143442"],["GA","143614"],["GB","143444"],["GD","143546"],["GF","143615"],["GH","143573"],["GM","143584"],["GR","143448"],["GT","143504"],["GW","143585"],["GY","143553"],["HK","143463"],["HN","143510"],["HR","143494"],["HU","143482"],["ID","143476"],["IE","143449"],["IL","143491"],["IN","143467"],["IQ","143617"],["IS","143558"],["IT","143450"],["JM","143511"],["JO","143528"],["JP","143462"],["KE","143529"],["KG","143586"],["KH","143579"],["KN","143548"],["KP","143466"],["KR","143466"],["KW","143493"],["KY","143544"],["KZ","143517"],["TC","143552"],["TD","143581"],["TJ","143603"],["TH","143475"],["TM","143604"],["TN","143536"],["TO","143608"],["TR","143480"],["TT","143551"],["TW","143470"],["TZ","143572"],["LA","143587"],["LB","143497"],["LC","143549"],["LI","143522"],["LK","143486"],["LR","143588"],["LT","143520"],["LU","143451"],["LV","143519"],["LY","143567"],["MA","143620"],["MD","143523"],["ME","143619"],["MG","143531"],["MK","143530"],["ML","143532"],["MM","143570"],["MN","143592"],["MO","143515"],["MR","143590"],["MS","143547"],["MT","143521"],["MU","143533"],["MV","143488"],["MW","143589"],["MX","143468"],["MY","143473"],["MZ","143593"],["NA","143594"],["NE","143534"],["NG","143561"],["NI","143512"],["NL","143452"],["NO","143457"],["NP","143484"],["NR","143606"],["NZ","143461"],["OM","143562"],["PA","143485"],["PE","143507"],["PG","143597"],["PH","143474"],["PK","143477"],["PL","143478"],["PT","143453"],["PW","143595"],["PY","143513"],["QA","143498"],["RO","143487"],["RS","143500"],["RU","143469"],["RW","143621"],["SA","143479"],["SB","143601"],["SC","143599"],["SE","143456"],["SG","143464"],["SI","143499"],["SK","143496"],["SL","143600"],["SN","143535"],["SR","143554"],["ST","143598"],["SV","143506"],["SZ","143602"],["UA","143492"],["UG","143537"],["US","143441"],["UY","143514"],["UZ","143566"],["VC","143550"],["VE","143502"],["VG","143543"],["VN","143471"],["VU","143609"],["XK","143624"],["YE","143571"],["ZA","143472"],["ZM","143622"],["ZW","143605"]] 69 | } 70 | } 71 | }; 72 | 73 | /***************** Processing *****************/ 74 | (async () => { 75 | const { Settings, Caches, Configs } = setENV("iRingo", "TestFlight", DataBase); 76 | $.log(`⚠ ${$.name}`, `Settings.Switch: ${Settings?.Switch}`, ""); 77 | switch (Settings?.Switch) { 78 | case "true": 79 | default: 80 | let url = URL.parse($request?.url); 81 | const METHOD = $request?.method, HOST = url?.host, PATH = url?.path, PATHs = PATH.split("/"); 82 | // 解析格式 83 | const FORMAT = ($response?.headers?.["Content-Type"] ?? $response?.headers?.["content-type"])?.split(";")?.[0]; 84 | $.log(`⚠ ${$.name}`, `METHOD: ${METHOD}`, `HOST: ${HOST}`, `PATH: ${PATH}`, `PATHs: ${PATHs}`, `FORMAT: ${FORMAT}`, ""); 85 | // 创建空数据 86 | let body = {}; 87 | // 格式判断 88 | switch (FORMAT) { 89 | case undefined: // 视为无body 90 | break; 91 | case "application/x-www-form-urlencoded": 92 | case "text/plain": 93 | case "text/html": 94 | default: 95 | break; 96 | case "text/xml": 97 | case "text/plist": 98 | case "application/xml": 99 | case "application/plist": 100 | case "application/x-plist": 101 | break; 102 | case "text/json": 103 | case "application/json": 104 | body = JSON.parse($response.body); 105 | // 主机判断 106 | switch (HOST) { 107 | case "testflight.apple.com": 108 | // 路径判断 109 | switch (PATH) { 110 | case "v1/session/authenticate": 111 | switch (Settings.MultiAccount) { // MultiAccount 112 | case "true": 113 | $.log(`🚧 ${$.name}, 启用多账号支持`, ""); 114 | if (Caches?.data) { //有data 115 | $.log(`🚧 ${$.name}, 有Caches.data`, ""); 116 | if (body?.data?.accountId === Caches?.data?.accountId) { // Account ID相等,刷新缓存 117 | $.log(`🚧 ${$.name}, Account ID相等,刷新缓存`, ""); 118 | Caches.headers = { 119 | "X-Request-Id": $request.headers["x-request-id"], 120 | "X-Session-Id": $request.headers["x-session-id"], 121 | "X-Session-Digest": $request.headers["x-session-digest"] 122 | }; 123 | Caches.data = body.data; 124 | $.setjson(Caches, "@iRingo.TestFlight.Caches"); 125 | } else { // Account ID不相等,Rewrite 126 | $.log(`🚧 ${$.name}, Account ID不相等,覆盖accountId和sessionId`, ""); 127 | //body.data = Caches.data; 128 | } 129 | } else { // Caches空 130 | $.log(`🚧 ${$.name}, Caches空,写入`, ""); 131 | Caches.headers = { 132 | "X-Request-Id": $request.headers["x-request-id"], 133 | "X-Session-Id": $request.headers["x-session-id"], 134 | "X-Session-Digest": $request.headers["x-session-digest"] 135 | }; 136 | Caches.data = body.data; 137 | $.setjson(Caches, "@iRingo.TestFlight.Caches"); 138 | }; 139 | //break; // 不中断,继续处理 140 | case "false": 141 | default: 142 | break; 143 | }; 144 | break; 145 | case "v1/devices": 146 | case "v1/devices/apns": 147 | case "v1/devices/add": 148 | case "v1/devices/remove": 149 | break; 150 | case `v1/messages/${Caches?.data?.accountId}`: 151 | case `v1/messages/${Caches?.data?.accountId}/read`: 152 | break; 153 | default: 154 | if (/\/accounts\//i.test(url.path)) { 155 | $.log(`🚧 ${$.name}, accounts`, ""); 156 | if (/\/settings\//i.test(url.path)) { 157 | $.log(`🚧 ${$.name}, settings`, ""); 158 | if (/\/notifications\/apps\/\d+$/i.test(url.path)) { 159 | $.log(`🚧 ${$.name}, notifications/apps`, ""); 160 | } else $.log(`🚧 ${$.name}, unknown`, ""); 161 | } else if (/\/apps/i.test(url.path)) { // app info mod 162 | $.log(`🚧 ${$.name}, /apps`, ""); 163 | if (/\/apps$/i.test(url.path)) { 164 | $.log(`🚧 ${$.name}, /apps`, ""); 165 | switch (Settings.Universal) { // 通用 166 | case "true": 167 | $.log(`🚧 ${$.name}, 启用通用应用支持`, ""); 168 | if (body.error === null) { // 数据无错误 169 | $.log(`🚧 ${$.name}, 数据无错误`, ""); 170 | body.data = body.data.map(app => { 171 | if (app.previouslyTested !== false) { // 不是前测试人员 172 | $.log(`🚧 ${$.name}, 不是前测试人员`, ""); 173 | app.platforms = app.platforms.map(platform => { 174 | platform.build = modBuild(platform.build); 175 | return platform 176 | }); 177 | } 178 | return app 179 | }); 180 | }; 181 | break; 182 | case "false": 183 | default: 184 | break; 185 | }; 186 | } else if (/\/apps\/\d+\/builds\/\d+$/i.test(url.path)) { 187 | $.log(`🚧 ${$.name}, /app/bulids`, ""); 188 | switch (Settings.Universal) { // 通用 189 | case "true": 190 | $.log(`🚧 ${$.name}, 启用通用应用支持`, ""); 191 | if (body.error === null) { // 数据无错误 192 | $.log(`🚧 ${$.name}, 数据无错误`, ""); 193 | // 当前Bulid 194 | body.data.currentBuild = modBuild(body.data.currentBuild); 195 | // Build列表 196 | body.data.builds = body.data.builds.map(build => modBuild(build)); 197 | }; 198 | break; 199 | case "false": 200 | default: 201 | break; 202 | }; 203 | } else if (/\/apps\/\d+\/platforms\/\w+\/trains$/i.test(url.path)) { 204 | $.log(`🚧 ${$.name}, /app/platforms/trains`, ""); 205 | } else if (/\/apps\/\d+\/platforms\/\w+\/trains\/[\d.]+\/builds$/i.test(url.path)) { 206 | $.log(`🚧 ${$.name}, /app/platforms/trains/builds`, ""); 207 | switch (Settings.Universal) { // 通用 208 | case "true": 209 | $.log(`🚧 ${$.name}, 启用通用应用支持`, ""); 210 | if (body.error === null) { // 数据无错误 211 | $.log(`🚧 ${$.name}, 数据无错误`, ""); 212 | // 当前Bulid 213 | body.data = body.data.map(data => modBuild(data)); 214 | }; 215 | break; 216 | case "false": 217 | default: 218 | break; 219 | }; 220 | } else if (/\/apps\/\d+\/builds\/\d+\/install$/i.test(url.path)) { 221 | $.log(`🚧 ${$.name}, /app/bulids/install`, ""); 222 | } else if (/\/apps\/\d+\/builds\/\d+\/install\/status$/i.test(url.path)) { 223 | $.log(`🚧 ${$.name}, /app/bulids/install/status`, ""); 224 | } else $.log(`🚧 ${$.name}, unknown`, ""); 225 | }; 226 | }; 227 | break; 228 | }; 229 | break; 230 | }; 231 | $response.body = JSON.stringify(body); 232 | break; 233 | case "application/x-protobuf": 234 | case "application/grpc": 235 | case "application/grpc+proto": 236 | case "applecation/octet-stream": 237 | break; 238 | }; 239 | break; 240 | case "false": 241 | break; 242 | }; 243 | })() 244 | .catch((e) => $.logErr(e)) 245 | .finally(() => { 246 | switch ($response) { 247 | default: { // 有回复数据,返回回复数据 248 | const FORMAT = ($response?.headers?.["Content-Type"] ?? $response?.headers?.["content-type"])?.split(";")?.[0]; 249 | $.log(`🎉 ${$.name}, finally`, `$response`, `FORMAT: ${FORMAT}`, ""); 250 | //$.log(`🚧 ${$.name}, finally`, `$response: ${JSON.stringify($response)}`, ""); 251 | if ($response?.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; 252 | if ($response?.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; 253 | if ($.isQuanX()) { 254 | switch (FORMAT) { 255 | case undefined: // 视为无body 256 | // 返回普通数据 257 | $.done({ headers: $response.headers }); 258 | break; 259 | case "application/x-www-form-urlencoded": 260 | case "text/plain": 261 | case "text/html": 262 | case "text/xml": 263 | case "text/plist": 264 | case "application/xml": 265 | case "application/plist": 266 | case "application/x-plist": 267 | case "text/json": 268 | case "application/json": 269 | default: 270 | // 返回普通数据 271 | $.done({ headers: $response.headers, body: $response.body }); 272 | break; 273 | case "application/x-protobuf": 274 | case "application/grpc": 275 | case "application/grpc+proto": 276 | case "applecation/octet-stream": 277 | // 返回二进制数据 278 | //$.log(`${$response.bodyBytes.byteLength}---${$response.bodyBytes.buffer.byteLength}`); 279 | $.done({ headers: $response.headers, bodyBytes: $response.bodyBytes.buffer.slice($response.bodyBytes.byteOffset, $response.bodyBytes.byteLength + $response.bodyBytes.byteOffset) }); 280 | break; 281 | }; 282 | } else $.done($response); 283 | break; 284 | }; 285 | case undefined: { // 无回复数据 286 | break; 287 | }; 288 | }; 289 | }) 290 | 291 | /***************** Function *****************/ 292 | /** 293 | * Set Environment Variables 294 | * @author VirgilClyne 295 | * @param {String} name - Persistent Store Key 296 | * @param {String} platform - Platform Name 297 | * @param {Object} database - Default DataBase 298 | * @return {Object} { Settings, Caches, Configs } 299 | */ 300 | function setENV(name, platform, database) { 301 | $.log(`⚠ ${$.name}, Set Environment Variables`, ""); 302 | let { Settings, Caches, Configs } = getENV(name, platform, database); 303 | /***************** Prase *****************/ 304 | $.log(`🎉 ${$.name}, Set Environment Variables`, `Settings: ${typeof Settings}`, `Settings内容: ${JSON.stringify(Settings)}`, ""); 305 | /***************** Caches *****************/ 306 | /***************** Configs *****************/ 307 | Configs.Storefront = new Map(Configs.Storefront); 308 | return { Settings, Caches, Configs }; 309 | }; 310 | 311 | /** 312 | * mod Build 313 | * @author VirgilClyne 314 | * @param {Object} build - Build 315 | * @return {Object} 316 | */ 317 | function modBuild(build) { 318 | switch (build.platform || build.name) { 319 | case "ios": 320 | $.log(`🚧 ${$.name}, ios`, ""); 321 | build = Build(build); 322 | break; 323 | case "osx": 324 | $.log(`🚧 ${$.name}, osx`, ""); 325 | if (build.macBuildCompatibility.runsOnAppleSilicon === true) { // 是苹果芯片 326 | $.log(`🚧 ${$.name}, runsOnAppleSilicon`, ""); 327 | build = Build(build); 328 | } 329 | break; 330 | case "appletvos": 331 | $.log(`🚧 ${$.name}, appletvos`, ""); 332 | break; 333 | default: 334 | $.log(`🚧 ${$.name}, unknown platform: ${build.platform || build.name}`, ""); 335 | break; 336 | }; 337 | return build 338 | 339 | function Build(build) { 340 | if (build.universal === true) { 341 | build.compatible = true; 342 | build.platformCompatible = true; 343 | build.hardwareCompatible = true; 344 | build.osCompatible = true; 345 | if (build?.permission) build.permission = "install"; 346 | if (build?.deviceFamilyInfo) { 347 | build.deviceFamilyInfo = [ 348 | { 349 | "number": 1, 350 | "name": "iOS", 351 | "iconUrl": "https://itunesconnect-mr.itunes.apple.com/itc/img/device-icons/device_family_icon_1.png" 352 | }, 353 | { 354 | "number": 2, 355 | "name": "iPad", 356 | "iconUrl": "https://itunesconnect-mr.itunes.apple.com/itc/img/device-icons/device_family_icon_2.png" 357 | }, 358 | { 359 | "number": 3, 360 | "name": "Apple TV", 361 | "iconUrl": "https://itunesconnect-mr.itunes.apple.com/itc/img/device-icons/device_family_icon_3.png" 362 | } 363 | ]; 364 | } 365 | if (build?.compatibilityData?.compatibleDeviceFamilies) { 366 | build.compatibilityData.compatibleDeviceFamilies = [ 367 | { 368 | "name": "iPad", 369 | "minimumSupportedDevice": null, 370 | "unsupportedDevices": [] 371 | }, 372 | { 373 | "name": "iPhone", 374 | "minimumSupportedDevice": null, 375 | "unsupportedDevices": [] 376 | }, 377 | { 378 | "name": "iPod", 379 | "minimumSupportedDevice": null, 380 | "unsupportedDevices": [] 381 | }, 382 | { 383 | "name": "Mac", 384 | "minimumSupportedDevice": null, 385 | "unsupportedDevices": [] 386 | } 387 | ]; 388 | } 389 | }; 390 | return build 391 | }; 392 | }; 393 | 394 | /***************** Env *****************/ 395 | // prettier-ignore 396 | // https://github.com/chavyleung/scripts/blob/master/Env.min.js 397 | function Env(t,e){class s{constructor(t){this.env=t}send(t,e="GET"){t="string"==typeof t?{url:t}:t;let s=this.get;return"POST"===e&&(s=this.post),new Promise((e,a)=>{s.call(this,t,(t,s,r)=>{t?a(t):e(s)})})}get(t){return this.send.call(this.env,t)}post(t){return this.send.call(this.env,t,"POST")}}return new class{constructor(t,e){this.name=t,this.http=new s(this),this.data=null,this.dataFile="box.dat",this.logs=[],this.isMute=!1,this.isNeedRewrite=!1,this.logSeparator="\n",this.encoding="utf-8",this.startTime=(new Date).getTime(),Object.assign(this,e),this.log("",`\ud83d\udd14${this.name}, \u5f00\u59cb!`)}getEnv(){return"undefined"!=typeof $environment&&$environment["surge-version"]?"Surge":"undefined"!=typeof $environment&&$environment["stash-version"]?"Stash":"undefined"!=typeof module&&module.exports?"Node.js":"undefined"!=typeof $task?"Quantumult X":"undefined"!=typeof $loon?"Loon":"undefined"!=typeof $rocket?"Shadowrocket":void 0}isNode(){return"Node.js"===this.getEnv()}isQuanX(){return"Quantumult X"===this.getEnv()}isSurge(){return"Surge"===this.getEnv()}isLoon(){return"Loon"===this.getEnv()}isShadowrocket(){return"Shadowrocket"===this.getEnv()}isStash(){return"Stash"===this.getEnv()}toObj(t,e=null){try{return JSON.parse(t)}catch{return e}}toStr(t,e=null){try{return JSON.stringify(t)}catch{return e}}getjson(t,e){let s=e;const a=this.getdata(t);if(a)try{s=JSON.parse(this.getdata(t))}catch{}return s}setjson(t,e){try{return this.setdata(JSON.stringify(t),e)}catch{return!1}}getScript(t){return new Promise(e=>{this.get({url:t},(t,s,a)=>e(a))})}runScript(t,e){return new Promise(s=>{let a=this.getdata("@chavy_boxjs_userCfgs.httpapi");a=a?a.replace(/\n/g,"").trim():a;let r=this.getdata("@chavy_boxjs_userCfgs.httpapi_timeout");r=r?1*r:20,r=e&&e.timeout?e.timeout:r;const[i,o]=a.split("@"),n={url:`http://${o}/v1/scripting/evaluate`,body:{script_text:t,mock_type:"cron",timeout:r},headers:{"X-Key":i,Accept:"*/*"},timeout:r};this.post(n,(t,e,a)=>s(a))}).catch(t=>this.logErr(t))}loaddata(){if(!this.isNode())return{};{this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),a=!s&&this.fs.existsSync(e);if(!s&&!a)return{};{const a=s?t:e;try{return JSON.parse(this.fs.readFileSync(a))}catch(t){return{}}}}}writedata(){if(this.isNode()){this.fs=this.fs?this.fs:require("fs"),this.path=this.path?this.path:require("path");const t=this.path.resolve(this.dataFile),e=this.path.resolve(process.cwd(),this.dataFile),s=this.fs.existsSync(t),a=!s&&this.fs.existsSync(e),r=JSON.stringify(this.data);s?this.fs.writeFileSync(t,r):a?this.fs.writeFileSync(e,r):this.fs.writeFileSync(t,r)}}lodash_get(t,e,s){const a=e.replace(/\[(\d+)\]/g,".$1").split(".");let r=t;for(const t of a)if(r=Object(r)[t],void 0===r)return s;return r}lodash_set(t,e,s){return Object(t)!==t?t:(Array.isArray(e)||(e=e.toString().match(/[^.[\]]+/g)||[]),e.slice(0,-1).reduce((t,s,a)=>Object(t[s])===t[s]?t[s]:t[s]=Math.abs(e[a+1])>>0==+e[a+1]?[]:{},t)[e[e.length-1]]=s,t)}getdata(t){let e=this.getval(t);if(/^@/.test(t)){const[,s,a]=/^@(.*?)\.(.*?)$/.exec(t),r=s?this.getval(s):"";if(r)try{const t=JSON.parse(r);e=t?this.lodash_get(t,a,""):e}catch(t){e=""}}return e}setdata(t,e){let s=!1;if(/^@/.test(e)){const[,a,r]=/^@(.*?)\.(.*?)$/.exec(e),i=this.getval(a),o=a?"null"===i?null:i||"{}":"{}";try{const e=JSON.parse(o);this.lodash_set(e,r,t),s=this.setval(JSON.stringify(e),a)}catch(e){const i={};this.lodash_set(i,r,t),s=this.setval(JSON.stringify(i),a)}}else s=this.setval(t,e);return s}getval(t){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.read(t);case"Quantumult X":return $prefs.valueForKey(t);case"Node.js":return this.data=this.loaddata(),this.data[t];default:return this.data&&this.data[t]||null}}setval(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":return $persistentStore.write(t,e);case"Quantumult X":return $prefs.setValueForKey(t,e);case"Node.js":return this.data=this.loaddata(),this.data[e]=t,this.writedata(),!0;default:return this.data&&this.data[e]||null}}initGotEnv(t){this.got=this.got?this.got:require("got"),this.cktough=this.cktough?this.cktough:require("tough-cookie"),this.ckjar=this.ckjar?this.ckjar:new this.cktough.CookieJar,t&&(t.headers=t.headers?t.headers:{},void 0===t.headers.Cookie&&void 0===t.cookieJar&&(t.cookieJar=this.ckjar))}get(t,e=(()=>{})){switch(t.headers&&(delete t.headers["Content-Type"],delete t.headers["Content-Length"],delete t.headers["content-type"],delete t.headers["content-length"]),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient.get(t,(t,s,a)=>{!t&&s&&(s.body=a,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,a)});break;case"Quantumult X":this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:a,headers:r,body:i,bodyBytes:o}=t;e(null,{status:s,statusCode:a,headers:r,body:i,bodyBytes:o},i,o)},t=>e(t&&t.error||"UndefinedError"));break;case"Node.js":let s=require("iconv-lite");this.initGotEnv(t),this.got(t).on("redirect",(t,e)=>{try{if(t.headers["set-cookie"]){const s=t.headers["set-cookie"].map(this.cktough.Cookie.parse).toString();s&&this.ckjar.setCookieSync(s,null),e.cookieJar=this.ckjar}}catch(t){this.logErr(t)}}).then(t=>{const{statusCode:a,statusCode:r,headers:i,rawBody:o}=t,n=s.decode(o,this.encoding);e(null,{status:a,statusCode:r,headers:i,rawBody:o,body:n},n)},t=>{const{message:a,response:r}=t;e(a,r,r&&s.decode(r.rawBody,this.encoding))})}}post(t,e=(()=>{})){const s=t.method?t.method.toLocaleLowerCase():"post";switch(t.body&&t.headers&&!t.headers["Content-Type"]&&!t.headers["content-type"]&&(t.headers["content-type"]="application/x-www-form-urlencoded"),t.headers&&(delete t.headers["Content-Length"],delete t.headers["content-length"]),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:this.isSurge()&&this.isNeedRewrite&&(t.headers=t.headers||{},Object.assign(t.headers,{"X-Surge-Skip-Scripting":!1})),$httpClient[s](t,(t,s,a)=>{!t&&s&&(s.body=a,s.statusCode=s.status?s.status:s.statusCode,s.status=s.statusCode),e(t,s,a)});break;case"Quantumult X":t.method=s,this.isNeedRewrite&&(t.opts=t.opts||{},Object.assign(t.opts,{hints:!1})),$task.fetch(t).then(t=>{const{statusCode:s,statusCode:a,headers:r,body:i,bodyBytes:o}=t;e(null,{status:s,statusCode:a,headers:r,body:i,bodyBytes:o},i,o)},t=>e(t&&t.error||"UndefinedError"));break;case"Node.js":let a=require("iconv-lite");this.initGotEnv(t);const{url:r,...i}=t;this.got[s](r,i).then(t=>{const{statusCode:s,statusCode:r,headers:i,rawBody:o}=t,n=a.decode(o,this.encoding);e(null,{status:s,statusCode:r,headers:i,rawBody:o,body:n},n)},t=>{const{message:s,response:r}=t;e(s,r,r&&a.decode(r.rawBody,this.encoding))})}}time(t,e=null){const s=e?new Date(e):new Date;let a={"M+":s.getMonth()+1,"d+":s.getDate(),"H+":s.getHours(),"m+":s.getMinutes(),"s+":s.getSeconds(),"q+":Math.floor((s.getMonth()+3)/3),S:s.getMilliseconds()};/(y+)/.test(t)&&(t=t.replace(RegExp.$1,(s.getFullYear()+"").substr(4-RegExp.$1.length)));for(let e in a)new RegExp("("+e+")").test(t)&&(t=t.replace(RegExp.$1,1==RegExp.$1.length?a[e]:("00"+a[e]).substr((""+a[e]).length)));return t}queryStr(t){let e="";for(const s in t){let a=t[s];null!=a&&""!==a&&("object"==typeof a&&(a=JSON.stringify(a)),e+=`${s}=${a}&`)}return e=e.substring(0,e.length-1),e}msg(e=t,s="",a="",r){const i=t=>{switch(typeof t){case void 0:return t;case"string":switch(this.getEnv()){case"Surge":case"Stash":default:return{url:t};case"Loon":case"Shadowrocket":return t;case"Quantumult X":return{"open-url":t};case"Node.js":return}case"object":switch(this.getEnv()){case"Surge":case"Stash":case"Shadowrocket":default:{let e=t.url||t.openUrl||t["open-url"];return{url:e}}case"Loon":{let e=t.openUrl||t.url||t["open-url"],s=t.mediaUrl||t["media-url"];return{openUrl:e,mediaUrl:s}}case"Quantumult X":{let e=t["open-url"]||t.url||t.openUrl,s=t["media-url"]||t.mediaUrl,a=t["update-pasteboard"]||t.updatePasteboard;return{"open-url":e,"media-url":s,"update-pasteboard":a}}case"Node.js":return}default:return}};if(!this.isMute)switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":default:$notification.post(e,s,a,i(r));break;case"Quantumult X":$notify(e,s,a,i(r));break;case"Node.js":}if(!this.isMuteLog){let t=["","==============\ud83d\udce3\u7cfb\u7edf\u901a\u77e5\ud83d\udce3=============="];t.push(e),s&&t.push(s),a&&t.push(a),console.log(t.join("\n")),this.logs=this.logs.concat(t)}}log(...t){t.length>0&&(this.logs=[...this.logs,...t]),console.log(t.join(this.logSeparator))}logErr(t,e){switch(this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t);break;case"Node.js":this.log("",`\u2757\ufe0f${this.name}, \u9519\u8bef!`,t.stack)}}wait(t){return new Promise(e=>setTimeout(e,t))}done(t={}){const e=(new Date).getTime(),s=(e-this.startTime)/1e3;switch(this.log("",`\ud83d\udd14${this.name}, \u7ed3\u675f! \ud83d\udd5b ${s} \u79d2`),this.log(),this.getEnv()){case"Surge":case"Loon":case"Stash":case"Shadowrocket":case"Quantumult X":default:$done(t);break;case"Node.js":process.exit(1)}}}(t,e)} 398 | 399 | // https://github.com/DualSubs/URL/blob/main/URLs.embedded.min.js 400 | function URLs(s){return new class{constructor(s=[]){this.name="URL v1.0.2",this.opts=s,this.json={scheme:"",host:"",path:"",params:{}}}parse(s){let t=s.match(/(?.+):\/\/(?[^/]+)\/?(?[^?]+)?\??(?.*)?/)?.groups??null;return t?.path||(t.path=""),t?.params&&(t.params=Object.fromEntries(t.params.split("&").map((s=>s.split("="))))),t}stringify(s=this.json){return s?.params?s.scheme+"://"+s.host+"/"+s.path+"?"+Object.entries(s.params).map((s=>s.join("="))).join("&"):s.scheme+"://"+s.host+"/"+s.path}}(s)} 401 | 402 | /** 403 | * Get Environment Variables 404 | * @link https://github.com/VirgilClyne/VirgilClyne/blob/main/function/getENV/getENV.min.js 405 | * @author VirgilClyne 406 | * @param {String} t - Persistent Store Key 407 | * @param {String} e - Platform Name 408 | * @param {Object} n - Default Database 409 | * @return {Object} { Settings, Caches, Configs } 410 | */ 411 | function getENV(t,e,n){let i=$.getjson(t,n),s={};if("undefined"!=typeof $argument&&Boolean($argument)){let t=Object.fromEntries($argument.split("&").map((t=>t.split("="))));for(let e in t)l(s,e,t[e])}let g={...n?.Default?.Settings,...n?.[e]?.Settings,...i?.[e]?.Settings,...s},f={...n?.Default?.Configs,...n?.[e]?.Configs,...i?.[e]?.Configs},o=i?.[e]?.Caches||{};return"string"==typeof o&&(o=JSON.parse(o)),{Settings:g,Caches:o,Configs:f};function l(t,e,n){e.split(".").reduce(((t,i,s)=>t[i]=e.split(".").length===++s?n:t[i]||{}),t)}} 412 | -------------------------------------------------------------------------------- /arguments-builder.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@iringo/arguments-builder"; 2 | export default defineConfig({ 3 | output: { 4 | surge: { 5 | path: "./dist/iRingo.TestFlight.sgmodule", 6 | transformEgern: { 7 | enable: true, 8 | path: "./dist/iRingo.TestFlight.yaml", 9 | }, 10 | }, 11 | loon: { 12 | path: "./dist/iRingo.TestFlight.plugin", 13 | }, 14 | customItems: [ 15 | { 16 | path: "./dist/iRingo.TestFlight.snippet", 17 | template: "./template/quantumultx.handlebars", 18 | }, 19 | { 20 | path: "./dist/iRingo.TestFlight.stoverride", 21 | template: "./template/stash.handlebars", 22 | }, 23 | { 24 | path: "./dist/iRingo.TestFlight.srmodule", 25 | template: "./template/shadowrocket.handlebars", 26 | }, 27 | ], 28 | dts: { 29 | isExported: true, 30 | path: "./src/types.d.ts", 31 | }, 32 | boxjsSettings: { 33 | path: "./template/boxjs.settings.json", 34 | scope: "@iRingo.TestFlight.Settings", 35 | }, 36 | }, 37 | args: [ 38 | { 39 | key: "CountryCode", 40 | name: "国家或地区代码", 41 | defaultValue: "US", 42 | type: "string", 43 | options: [ 44 | { key: "AUTO", label: "🇺🇳自动(跟随地区检测结果)" }, 45 | { key: "CN", label: "🇨🇳中国大陆" }, 46 | { key: "HK", label: "🇭🇰香港" }, 47 | { key: "TW", label: "🇹🇼台湾" }, 48 | { key: "SG", label: "🇸🇬新加坡" }, 49 | { key: "US", label: "🇺🇸美国" }, 50 | { key: "JP", label: "🇯🇵日本" }, 51 | { key: "AU", label: "🇦🇺澳大利亚" }, 52 | { key: "GB", label: "🇬🇧英国" }, 53 | { key: "KR", label: "🇰🇷韩国" }, 54 | { key: "CA", label: "🇨🇦加拿大" }, 55 | { key: "IE", label: "🇮🇪爱尔兰" }, 56 | ], 57 | description: "不同国家或地区提供的内容或有差别。", 58 | }, 59 | { 60 | key: "MultiAccount", 61 | name: "启用多账号支持", 62 | defaultValue: false, 63 | type: "boolean", 64 | description: "启用多账号支持会自动保存保存更新当前账号信息。", 65 | }, 66 | { 67 | key: "Universal", 68 | name: "启用通用应用支持", 69 | defaultValue: true, 70 | type: "boolean", 71 | description: "启用通用应用支持会解除 TestFlight app 的 iOS/iPadOS/macOS(AppleSilicon) 平台限制。", 72 | }, 73 | { 74 | key: "AlwaysShowInstall", 75 | name: "总是显示安装选项", 76 | defaultValue: false, 77 | type: "boolean", 78 | description: "当 Testflight 无法加入时,也总是显示app详情页面的安装选项。", 79 | }, 80 | { 81 | key: "MergeNotifications", 82 | name: "合并通知开关", 83 | defaultValue: false, 84 | type: "boolean", 85 | description: "同步开关全平台的电子邮件通知,如关闭 iOS 的 Testflight 更新电子邮件通知,也会同时关闭 tvOS 的 Testflight 更新电子邮件通知。", 86 | }, 87 | { 88 | key: "LogLevel", 89 | name: "[调试] 日志等级", 90 | type: "string", 91 | defaultValue: "WARN", 92 | description: "选择脚本日志的输出等级,低于所选等级的日志将全部输出。", 93 | options: [ 94 | { key: "OFF", label: "🔴 关闭" }, 95 | { key: "ERROR", label: "❌ 错误" }, 96 | { key: "WARN", label: "⚠️ 警告" }, 97 | { key: "INFO", label: "ℹ️ 信息" }, 98 | { key: "DEBUG", label: "🅱️ 调试" }, 99 | { key: "ALL", label: "全部" }, 100 | ], 101 | }, 102 | ], 103 | }); 104 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "files": { 4 | "ignore": [ 5 | "**/*.bundle.js" 6 | ], 7 | "ignoreUnknown": false 8 | }, 9 | "formatter": { 10 | "enabled": true, 11 | "indentStyle": "tab", 12 | "indentWidth": 2, 13 | "lineEnding": "lf", 14 | "lineWidth": 320 15 | }, 16 | "javascript": { 17 | "formatter": { 18 | "arrowParentheses": "asNeeded", 19 | "bracketSameLine": true, 20 | "quoteStyle": "double" 21 | } 22 | }, 23 | "json": { 24 | "parser": { 25 | "allowComments": true, 26 | "allowTrailingCommas": true 27 | } 28 | }, 29 | "linter": { 30 | "enabled": true, 31 | "rules": { 32 | "complexity": { 33 | "noForEach": "off", 34 | "noStaticOnlyClass": "off", 35 | "noUselessSwitchCase": "off", 36 | "useArrowFunction": "info", 37 | "useFlatMap": "off", 38 | "useLiteralKeys": "info" 39 | }, 40 | "correctness": { 41 | "noInnerDeclarations": "info", 42 | "noSelfAssign": "off", 43 | "noSwitchDeclarations": "info", 44 | "noUnsafeOptionalChaining": "info" 45 | }, 46 | "performance": { 47 | "noDelete": "info" 48 | }, 49 | "recommended": true, 50 | "style": { 51 | "noNegationElse": "off", 52 | "noParameterAssign": "off", 53 | "noUselessElse": "off", 54 | "noVar": "info", 55 | "useDefaultParameterLast": "info", 56 | "useForOf": "error", 57 | "useNodejsImportProtocol": "error", 58 | "useNumberNamespace": "error", 59 | "useSingleVarDeclarator": "off" 60 | }, 61 | "suspicious": { 62 | "noAssignInExpressions": "info", 63 | "noDoubleEquals": "info", 64 | "noFallthroughSwitchClause": "info", 65 | "noGlobalIsNan": "off", 66 | "useDefaultSwitchClauseLast": "off" 67 | } 68 | } 69 | }, 70 | "organizeImports": { 71 | "enabled": true 72 | }, 73 | "vcs": { 74 | "clientKind": "git", 75 | "enabled": true, 76 | "useIgnoreFile": true 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nsringo/testflight", 3 | "organizationName": " iRingo", 4 | "displayName": " iRingo: ✈ TestFlight", 5 | "description": "1.自定义TestFlight登录商店地区\n2.多账户保存及切换(需配置BoxJs使用)\n3.让通用应用在任意苹果处理器设备上可安装", 6 | "homepage": "https://NSRingo.github.io/guide/test-flight", 7 | "openUrl": "http://boxjs.com/#/app/iRingo.TestFlight", 8 | "icon": "https://developer.apple.com/assets/elements/icons/testflight/testflight-128x128.png", 9 | "keywords": [], 10 | "contributors": [ 11 | "VirgilClyne[https://github.com/VirgilClyne]" 12 | ], 13 | "system": [ 14 | "iOS", 15 | "iPadOS", 16 | "tvOS", 17 | "macOS", 18 | "watchOS" 19 | ], 20 | "author": "VirgilClyne", 21 | "license": "Apache-2.0", 22 | "type": "module", 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/NSRingo/TestFlight.git" 26 | }, 27 | "directories": { 28 | "example": "example" 29 | }, 30 | "scripts": { 31 | "build": "rspack build", 32 | "build:dev": "rspack build -c rspack.dev.config.js --mode=development", 33 | "build:args": "arguments-builder build", 34 | "dts": "arguments-builder dts" 35 | }, 36 | "browserslist": [ 37 | "iOS >= 15" 38 | ], 39 | "devDependencies": { 40 | "@iringo/arguments-builder": "^1.8.5", 41 | "@rspack/cli": "^1.1.4", 42 | "@rspack/core": "^1.1.4", 43 | "node-polyfill-webpack-plugin": "^4.0.0" 44 | }, 45 | "dependencies": { 46 | "@nsnanocat/url": "^1.2.4", 47 | "@nsnanocat/util": "^1.8.8" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rspack.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@rspack/cli"; 2 | import rspack from "@rspack/core"; 3 | import NodePolyfillPlugin from "node-polyfill-webpack-plugin"; 4 | import pkg from "./package.json" with { type: "json" }; 5 | 6 | export default defineConfig({ 7 | entry: { 8 | "request": "./src/request.js", 9 | "response": "./src/response.js", 10 | }, 11 | output: { 12 | filename: "[name].bundle.js", 13 | }, 14 | plugins: [ 15 | new NodePolyfillPlugin({ 16 | //additionalAliases: ['console'], 17 | }), 18 | new rspack.BannerPlugin({ 19 | banner: `console.log('Date: ${new Date().toLocaleString('zh-CN', {timeZone: 'PRC'})}');`, 20 | raw: true, 21 | }), 22 | new rspack.BannerPlugin({ 23 | banner: `console.log('Version: ${pkg.version}');`, 24 | raw: true, 25 | }), 26 | new rspack.BannerPlugin({ 27 | banner: "console.log('[file]');", 28 | raw: true, 29 | }), 30 | new rspack.BannerPlugin({ 31 | banner: `console.log('${pkg.displayName}');`, 32 | raw: true, 33 | }), 34 | new rspack.BannerPlugin({ 35 | banner: pkg.homepage, 36 | }), 37 | ], 38 | devtool: false, 39 | performance: false, 40 | }); 41 | -------------------------------------------------------------------------------- /rspack.dev.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "@rspack/cli"; 2 | import rspack from "@rspack/core"; 3 | import NodePolyfillPlugin from "node-polyfill-webpack-plugin"; 4 | import pkg from "./package.json" with { type: "json" }; 5 | 6 | export default defineConfig({ 7 | entry: { 8 | "request": "./src/request.dev.js", 9 | "response": "./src/response.dev.js", 10 | }, 11 | output: { 12 | filename: "[name].bundle.js", 13 | }, 14 | optimization: { 15 | minimize: false, 16 | usedExports: true, 17 | }, 18 | plugins: [ 19 | new NodePolyfillPlugin({ 20 | //additionalAliases: ['console'], 21 | }), 22 | new rspack.BannerPlugin({ 23 | banner: `console.log('Date: ${new Date().toLocaleString('zh-CN', {timeZone: 'PRC'})}');`, 24 | raw: true, 25 | }), 26 | new rspack.BannerPlugin({ 27 | banner: `console.log('Version: ${pkg.version}');`, 28 | raw: true, 29 | }), 30 | new rspack.BannerPlugin({ 31 | banner: "console.log('[file]');", 32 | raw: true, 33 | }), 34 | new rspack.BannerPlugin({ 35 | banner: `console.log('${pkg.displayName} β');`, 36 | raw: true, 37 | }), 38 | new rspack.BannerPlugin({ 39 | banner: pkg.homepage, 40 | }), 41 | ], 42 | devtool: false, 43 | performance: false, 44 | }); 45 | -------------------------------------------------------------------------------- /src/function/database.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | Testflight: { 3 | Settings: { 4 | CountryCode: "US", 5 | MultiAccount: false, 6 | Universal: true, 7 | AlwaysShowInstall: false, 8 | MergeNotifications: false, 9 | }, 10 | }, 11 | Default: { 12 | Settings: { 13 | LogLevel: "WARN", 14 | }, 15 | Configs: { 16 | Storefront: { 17 | AE: "143481", 18 | AF: "143610", 19 | AG: "143540", 20 | AI: "143538", 21 | AL: "143575", 22 | AM: "143524", 23 | AO: "143564", 24 | AR: "143505", 25 | AT: "143445", 26 | AU: "143460", 27 | AZ: "143568", 28 | BA: "143612", 29 | BB: "143541", 30 | BD: "143490", 31 | BE: "143446", 32 | BF: "143578", 33 | BG: "143526", 34 | BH: "143559", 35 | BJ: "143576", 36 | BM: "143542", 37 | BN: "143560", 38 | BO: "143556", 39 | BR: "143503", 40 | BS: "143539", 41 | BT: "143577", 42 | BW: "143525", 43 | BY: "143565", 44 | BZ: "143555", 45 | CA: "143455", 46 | CD: "143613", 47 | CG: "143582", 48 | CH: "143459", 49 | CI: "143527", 50 | CL: "143483", 51 | CM: "143574", 52 | CN: "143465", 53 | CO: "143501", 54 | CR: "143495", 55 | CV: "143580", 56 | CY: "143557", 57 | CZ: "143489", 58 | DE: "143443", 59 | DK: "143458", 60 | DM: "143545", 61 | DO: "143508", 62 | DZ: "143563", 63 | EC: "143509", 64 | EE: "143518", 65 | EG: "143516", 66 | ES: "143454", 67 | FI: "143447", 68 | FJ: "143583", 69 | FM: "143591", 70 | FR: "143442", 71 | GA: "143614", 72 | GB: "143444", 73 | GD: "143546", 74 | GF: "143615", 75 | GH: "143573", 76 | GM: "143584", 77 | GR: "143448", 78 | GT: "143504", 79 | GW: "143585", 80 | GY: "143553", 81 | HK: "143463", 82 | HN: "143510", 83 | HR: "143494", 84 | HU: "143482", 85 | ID: "143476", 86 | IE: "143449", 87 | IL: "143491", 88 | IN: "143467", 89 | IQ: "143617", 90 | IS: "143558", 91 | IT: "143450", 92 | JM: "143511", 93 | JO: "143528", 94 | JP: "143462", 95 | KE: "143529", 96 | KG: "143586", 97 | KH: "143579", 98 | KN: "143548", 99 | KP: "143466", 100 | KR: "143466", 101 | KW: "143493", 102 | KY: "143544", 103 | KZ: "143517", 104 | TC: "143552", 105 | TD: "143581", 106 | TJ: "143603", 107 | TH: "143475", 108 | TM: "143604", 109 | TN: "143536", 110 | TO: "143608", 111 | TR: "143480", 112 | TT: "143551", 113 | TW: "143470", 114 | TZ: "143572", 115 | LA: "143587", 116 | LB: "143497", 117 | LC: "143549", 118 | LI: "143522", 119 | LK: "143486", 120 | LR: "143588", 121 | LT: "143520", 122 | LU: "143451", 123 | LV: "143519", 124 | LY: "143567", 125 | MA: "143620", 126 | MD: "143523", 127 | ME: "143619", 128 | MG: "143531", 129 | MK: "143530", 130 | ML: "143532", 131 | MM: "143570", 132 | MN: "143592", 133 | MO: "143515", 134 | MR: "143590", 135 | MS: "143547", 136 | MT: "143521", 137 | MU: "143533", 138 | MV: "143488", 139 | MW: "143589", 140 | MX: "143468", 141 | MY: "143473", 142 | MZ: "143593", 143 | NA: "143594", 144 | NE: "143534", 145 | NG: "143561", 146 | NI: "143512", 147 | NL: "143452", 148 | NO: "143457", 149 | NP: "143484", 150 | NR: "143606", 151 | NZ: "143461", 152 | OM: "143562", 153 | PA: "143485", 154 | PE: "143507", 155 | PG: "143597", 156 | PH: "143474", 157 | PK: "143477", 158 | PL: "143478", 159 | PT: "143453", 160 | PW: "143595", 161 | PY: "143513", 162 | QA: "143498", 163 | RO: "143487", 164 | RS: "143500", 165 | RU: "143469", 166 | RW: "143621", 167 | SA: "143479", 168 | SB: "143601", 169 | SC: "143599", 170 | SE: "143456", 171 | SG: "143464", 172 | SI: "143499", 173 | SK: "143496", 174 | SL: "143600", 175 | SN: "143535", 176 | SR: "143554", 177 | ST: "143598", 178 | SV: "143506", 179 | SZ: "143602", 180 | UA: "143492", 181 | UG: "143537", 182 | US: "143441", 183 | UY: "143514", 184 | UZ: "143566", 185 | VC: "143550", 186 | VE: "143502", 187 | VG: "143543", 188 | VN: "143471", 189 | VU: "143609", 190 | XK: "143624", 191 | YE: "143571", 192 | ZA: "143472", 193 | ZM: "143622", 194 | ZW: "143605", 195 | }, 196 | }, 197 | }, 198 | }; 199 | -------------------------------------------------------------------------------- /src/function/detectPlatform.mjs: -------------------------------------------------------------------------------- 1 | import { Console } from "@nsnanocat/util"; 2 | 3 | export default function detectPlatform(userAgent) { 4 | Console.log("☑️ Detect Platform"); 5 | /***************** Platform *****************/ 6 | let Platform = "ios"; 7 | switch (true) { 8 | case /iPhone|iPad|iOS|iPadOS/i.test(userAgent): 9 | Platform = "ios"; 10 | break; 11 | case /Macintosh|macOS/i.test(userAgent): 12 | Platform = "osx"; 13 | break; 14 | case /Apple TV|tvOS/i.test(userAgent): 15 | Platform = "appletvos"; 16 | break; 17 | case /Watch|watchOS/i.test(userAgent): 18 | Platform = "watchos"; 19 | break; 20 | case /Vision Pro|xrOS/i.test(userAgent): 21 | Platform = "xros"; 22 | break; 23 | } 24 | Console.log("✅ Detect Platform", `Platform: ${Platform}`); 25 | return Platform; 26 | } 27 | -------------------------------------------------------------------------------- /src/function/setENV.mjs: -------------------------------------------------------------------------------- 1 | import { Console, getStorage, Lodash as _ } from "@nsnanocat/util"; 2 | 3 | /** 4 | * Set Environment Variables 5 | * @author VirgilClyne 6 | * @param {String} name - Persistent Store Key 7 | * @param {Array} platforms - Platform Names 8 | * @param {Object} database - Default DataBase 9 | * @return {Object} { Settings, Caches, Configs } 10 | */ 11 | export default function setENV(name, platforms, database) { 12 | Console.log("☑️ Set Environment Variables"); 13 | const { Settings, Caches, Configs } = getStorage(name, platforms, database); 14 | /***************** Settings *****************/ 15 | Console.info(`typeof Settings: ${typeof Settings}`, `Settings: ${JSON.stringify(Settings, null, 2)}`); 16 | /***************** Caches *****************/ 17 | //Console.debug(`typeof Caches: ${typeof Caches}`, `Caches: ${JSON.stringify(Caches)}`); 18 | /***************** Configs *****************/ 19 | //Configs.Storefront = new Map(Configs.Storefront); 20 | if (Configs.Locale) Configs.Locale = new Map(Configs.Locale); 21 | if (Configs.i18n) for (const type in Configs.i18n) Configs.i18n[type] = new Map(Configs.i18n[type]); 22 | Console.log("✅ Set Environment Variables"); 23 | return { Settings, Caches, Configs }; 24 | } 25 | -------------------------------------------------------------------------------- /src/request.dev.js: -------------------------------------------------------------------------------- 1 | import { $app, Console, done, Lodash as _, Storage } from "@nsnanocat/util"; 2 | import { URL } from "@nsnanocat/url"; 3 | import database from "./function/database.mjs"; 4 | import setENV from "./function/setENV.mjs"; 5 | import detectPlatform from "./function/detectPlatform.mjs"; 6 | // 构造回复数据 7 | // biome-ignore lint/style/useConst: 8 | let $response = undefined; 9 | /***************** Processing *****************/ 10 | // 解构URL 11 | const url = new URL($request.url); 12 | Console.info(`url: ${url.toJSON()}`); 13 | // 获取连接参数 14 | const PATHs = url.pathname.split("/").filter(Boolean); 15 | Console.info(`PATHs: ${PATHs}`); 16 | // 解析格式 17 | const FORMAT = ($request.headers?.["Content-Type"] ?? $request.headers?.["content-type"])?.split(";")?.[0]; 18 | Console.info(`FORMAT: ${FORMAT}`); 19 | (async () => { 20 | /** 21 | * @type {{Settings: import('./types').Settings}} 22 | */ 23 | const { Settings, Caches, Configs } = setENV("iRingo", "TestFlight", database); 24 | Console.logLevel = Settings.LogLevel; 25 | // 创建空数据 26 | let body = {}; 27 | // 方法判断 28 | switch ($request.method) { 29 | case "POST": 30 | case "PUT": 31 | case "PATCH": 32 | // biome-ignore lint/suspicious/noFallthroughSwitchClause: 33 | case "DELETE": 34 | // 格式判断 35 | switch (FORMAT) { 36 | case undefined: // 视为无body 37 | break; 38 | case "application/x-www-form-urlencoded": 39 | case "text/plain": 40 | default: 41 | break; 42 | case "application/x-mpegURL": 43 | case "application/x-mpegurl": 44 | case "application/vnd.apple.mpegurl": 45 | case "audio/mpegurl": 46 | //body = M3U8.parse($request.body); 47 | //Console.debug(`body: ${JSON.stringify(body)}`); 48 | //$request.body = M3U8.stringify(body); 49 | break; 50 | case "text/xml": 51 | case "text/html": 52 | case "text/plist": 53 | case "application/xml": 54 | case "application/plist": 55 | case "application/x-plist": 56 | //body = XML.parse($request.body); 57 | //Console.debug(`body: ${JSON.stringify(body)}`); 58 | //$request.body = XML.stringify(body); 59 | break; 60 | case "text/vtt": 61 | case "application/vtt": 62 | //body = VTT.parse($request.body); 63 | //Console.debug(`body: ${JSON.stringify(body)}`); 64 | //$request.body = VTT.stringify(body); 65 | break; 66 | case "text/json": 67 | case "application/json": 68 | body = JSON.parse($request.body ?? "{}"); 69 | Console.debug(`body: ${JSON.stringify(body)}`); 70 | switch (url.hostname) { 71 | case "testflight.apple.com": 72 | switch (url.pathname) { 73 | case "/v1/session/authenticate": 74 | /* 75 | if (Settings.storeCookies) { // 使用Cookies 76 | Console.debug(`storeCookies`); 77 | if (Caches?.dsId && Caches?.storeCookies) { // 有 DS ID和iTunes Store Cookie 78 | Console.debug(`有Caches, DS ID和iTunes Store Cookie`); 79 | if (body.dsId !== Caches?.dsId) { // DS ID不相等,覆盖iTunes Store Cookie 80 | Console.debug(`DS ID不相等,覆盖DS ID和iTunes Store Cookie`); 81 | body.dsId = Caches.dsId; 82 | body.deviceModel = Caches.deviceModel; 83 | body.storeCookies = Caches.storeCookies; 84 | body.deviceVendorId = Caches.deviceVendorId; 85 | body.deviceName = Caches.deviceName; 86 | } else Storage.setItem("@iRingo.TestFlight.Caches", { ...Caches, ...body }); // DS ID相等,刷新缓存 87 | } else Storage.setItem("@iRingo.TestFlight.Caches", { ...Caches, ...body }); // Caches空 88 | } 89 | */ 90 | if (Settings.CountryCode !== "AUTO") body.storeFrontIdentifier = body.storeFrontIdentifier.replace(/\d{6}/, Configs.Storefront[Settings.CountryCode]); 91 | break; 92 | case "/v1/properties/testflight": 93 | break; 94 | case "/v1/devices": 95 | case "/v1/devices/apns": 96 | case "/v1/devices/add": 97 | case "/v1/devices/remove": 98 | break; 99 | default: 100 | switch (PATHs[0]) { 101 | case "v1": 102 | case "v2": 103 | case "v3": 104 | switch (PATHs[1]) { 105 | case "accounts": 106 | switch (PATHs[2]) { 107 | case "settings": 108 | switch (PATHs[3]) { 109 | case "notifications": 110 | switch (PATHs[4]) { 111 | case "apps": 112 | switch (PATHs[5]) { 113 | case undefined: 114 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/settings/notifications/apps`); 115 | break; 116 | default: 117 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/settings/notifications/apps/${PATHs[5]}`); 118 | switch (Settings.MergeNotifications) { 119 | case true: 120 | default: { 121 | Console.info("合并通知"); 122 | const Platform = detectPlatform($request.headers?.["User-Agent"] ?? $request.headers?.["user-agent"]); 123 | const EmailEnabled = _.get(body, `platformUpdates.${Platform}.emailEnabled`); 124 | Console.info(`EmailEnabled: ${EmailEnabled}`); 125 | _.set(body, "platformUpdates.appletvos.emailEnabled", EmailEnabled); 126 | _.set(body, "platformUpdates.ios.emailEnabled", EmailEnabled); 127 | _.set(body, "platformUpdates.osx.emailEnabled", EmailEnabled); 128 | _.set(body, "platformUpdates.xros.emailEnabled", EmailEnabled); 129 | break; 130 | } 131 | case false: 132 | Console.info("不合并通知"); 133 | break; 134 | } 135 | break; 136 | } 137 | 138 | break; 139 | } 140 | break; 141 | } 142 | break; 143 | case Caches?.data?.accountId: // UUID 144 | default: 145 | switch (PATHs[3]) { 146 | case "apps": 147 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/`); 148 | switch (PATHs[4]) { 149 | default: 150 | switch (PATHs[5]) { 151 | case "builds": 152 | switch (PATHs[7]) { 153 | case undefined: 154 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/builds/${PATHs[6]}`); 155 | break; 156 | case "install": 157 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/builds/${PATHs[6]}/install`); 158 | if (Settings.CountryCode !== "AUTO") body.storefrontId = body.storefrontId.replace(/\d{6}/, Configs.Storefront[Settings.CountryCode]); 159 | break; 160 | default: 161 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/builds/${PATHs[6]}/${PATHs[7]}`); 162 | break; 163 | } 164 | break; 165 | } 166 | break; 167 | } 168 | break; 169 | } 170 | break; 171 | } 172 | break; 173 | } 174 | break; 175 | } 176 | break; 177 | } 178 | break; 179 | } 180 | $request.body = JSON.stringify(body); 181 | break; 182 | case "application/protobuf": 183 | case "application/x-protobuf": 184 | case "application/vnd.google.protobuf": 185 | case "application/grpc": 186 | case "application/grpc+proto": 187 | case "applecation/octet-stream": 188 | break; 189 | } 190 | //break; // 不中断,继续处理URL 191 | case "GET": 192 | case "HEAD": 193 | case "OPTIONS": 194 | default: 195 | // 主机判断 196 | switch (url.hostname) { 197 | case "testflight.apple.com": 198 | // 路径判断 199 | switch (url.pathname) { 200 | case "/v1/session/authenticate": 201 | break; 202 | case "v1/properties/testflight": 203 | //$request.headers["X-Apple-Rosetta-Available"] = Settings.Rosetta; 204 | break; 205 | case "/v1/devices": 206 | case "/v1/devices/apns": 207 | case "/v1/devices/add": 208 | case "/v1/devices/remove": 209 | break; 210 | default: 211 | // headers auth mod 212 | switch (Settings.MultiAccount) { 213 | case true: { 214 | Console.info("启用多账号支持"); 215 | const IfNoneMatch = $request?.headers?.["If-None-Match"] ?? $request?.headers?.["if-none-match"]; 216 | const XRequestId = $request?.headers?.["X-Request-Id"] ?? $request?.headers?.["x-request-id"]; 217 | const XSessionId = $request?.headers?.["X-Session-Id"] ?? $request?.headers?.["x-session-id"]; 218 | const XSessionDigest = $request?.headers?.["X-Session-Digest"] ?? $request?.headers?.["x-session-digest"]; 219 | if (Caches.data) { 220 | // Caches.data存在 221 | Console.info("Caches.data存在,读取"); 222 | switch (PATHs[0]) { 223 | case "v1": 224 | case "v2": 225 | case "v3": 226 | switch (PATHs[1]) { 227 | case "accounts": 228 | case "messages": 229 | case "apps": 230 | default: 231 | switch (PATHs[2]) { 232 | case "settings": 233 | case undefined: 234 | default: 235 | switch (/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/.test(PATHs[2])) { 236 | // biome-ignore lint/suspicious/noFallthroughSwitchClause: 237 | case true: // PATHs[2]是UUID 238 | Console.info("PATHs[2]是UUID,替换url.pathname"); 239 | url.pathname = url.pathname.replace(/\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\//i, `/${Caches.data.accountId}/`); 240 | //break; // 不中断,继续处理 241 | case false: // PATHs[2]不是UUID 242 | if (XSessionId !== Caches.headers["X-Session-Id"]) { 243 | // sessionId不同 244 | Console.info("sessionId不同,替换$request.headers"); 245 | if (IfNoneMatch) { 246 | delete $request.headers?.["If-None-Match"]; 247 | delete $request.headers?.["if-none-match"]; 248 | } 249 | if (XRequestId) { 250 | if ($request.headers?.["X-Request-Id"]) $request.headers["X-Request-Id"] = Caches.headers["X-Request-Id"]; 251 | if ($request.headers?.["x-request-id"]) $request.headers["x-request-id"] = Caches.headers["X-Request-Id"]; 252 | } 253 | if (XSessionId) { 254 | if ($request.headers?.["X-Session-Id"]) $request.headers["X-Session-Id"] = Caches.headers["X-Session-Id"]; 255 | if ($request.headers?.["x-session-id"]) $request.headers["x-session-id"] = Caches.headers["X-Session-Id"]; 256 | } 257 | if (XSessionDigest) { 258 | if ($request.headers?.["X-Session-Digest"]) $request.headers["X-Session-Digest"] = Caches.headers["X-Session-Digest"]; 259 | if ($request.headers?.["x-session-digest"]) $request.headers["x-session-digest"] = Caches.headers["X-Session-Digest"]; 260 | } 261 | } 262 | } 263 | break; 264 | case Caches?.data?.accountId: // PATHs[2]有UUID且与accountId相同 265 | Console.info("PATHs[2]与accountId相同,更新Caches"); 266 | Caches.headers = { 267 | "X-Request-Id": XRequestId, 268 | "X-Session-Id": XSessionId, 269 | "X-Session-Digest": XSessionDigest, 270 | }; 271 | Storage.setItem("@iRingo.TestFlight.Caches", Caches); 272 | break; 273 | } 274 | break; 275 | case "tc": // termsAndConditions 276 | break; 277 | } 278 | break; 279 | } 280 | break; 281 | } else { 282 | // Caches空 283 | Console.info("Caches空,新写入"); 284 | Caches.headers = { 285 | "X-Request-Id": XRequestId, 286 | "X-Session-Id": XSessionId, 287 | "X-Session-Digest": XSessionDigest, 288 | }; 289 | if (/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/.test(PATHs[2])) { 290 | Caches.data = { 291 | accountId: PATHs[2], 292 | sessionId: XSessionId, 293 | }; 294 | } 295 | Storage.setItem("@iRingo.TestFlight.Caches", Caches); 296 | } 297 | break; 298 | } 299 | case false: 300 | default: 301 | Console.info("关闭多账号支持"); 302 | break; 303 | } 304 | break; 305 | } 306 | break; 307 | } 308 | break; 309 | case "CONNECT": 310 | case "TRACE": 311 | break; 312 | } 313 | $request.url = url.toString(); 314 | Console.debug(`$request.url: ${$request.url}`); 315 | })() 316 | .catch(e => Console.error(e)) 317 | .finally(() => { 318 | switch (typeof $response) { 319 | case "object": // 有构造回复数据,返回构造的回复数据 320 | //Console.debug("finally", `echo $response: ${JSON.stringify($response, null, 2)}`); 321 | if ($response.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; 322 | if ($response.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; 323 | switch ($app) { 324 | default: 325 | done({ response: $response }); 326 | break; 327 | case "Quantumult X": 328 | if (!$response.status) $response.status = "HTTP/1.1 200 OK"; 329 | delete $response.headers?.["Content-Length"]; 330 | delete $response.headers?.["content-length"]; 331 | delete $response.headers?.["Transfer-Encoding"]; 332 | done($response); 333 | break; 334 | } 335 | break; 336 | case "undefined": // 无构造回复数据,发送修改的请求数据 337 | //Console.debug("finally", `$request: ${JSON.stringify($request, null, 2)}`); 338 | done($request); 339 | break; 340 | default: 341 | Console.error(`不合法的 $response 类型: ${typeof $response}`); 342 | break; 343 | } 344 | }); 345 | -------------------------------------------------------------------------------- /src/request.js: -------------------------------------------------------------------------------- 1 | import { $app, Console, done, Lodash as _, Storage } from "@nsnanocat/util"; 2 | import { URL } from "@nsnanocat/url"; 3 | import database from "./function/database.mjs"; 4 | import setENV from "./function/setENV.mjs"; 5 | import detectPlatform from "./function/detectPlatform.mjs"; 6 | // 构造回复数据 7 | // biome-ignore lint/style/useConst: 8 | let $response = undefined; 9 | /***************** Processing *****************/ 10 | // 解构URL 11 | const url = new URL($request.url); 12 | Console.info(`url: ${url.toJSON()}`); 13 | // 获取连接参数 14 | const PATHs = url.pathname.split("/").filter(Boolean); 15 | Console.info(`PATHs: ${PATHs}`); 16 | // 解析格式 17 | const FORMAT = ($request.headers?.["Content-Type"] ?? $request.headers?.["content-type"])?.split(";")?.[0]; 18 | Console.info(`FORMAT: ${FORMAT}`); 19 | (async () => { 20 | /** 21 | * @type {{Settings: import('./types').Settings}} 22 | */ 23 | const { Settings, Caches, Configs } = setENV("iRingo", "TestFlight", database); 24 | Console.logLevel = Settings.LogLevel; 25 | // 创建空数据 26 | let body = {}; 27 | // 方法判断 28 | switch ($request.method) { 29 | case "POST": 30 | case "PUT": 31 | case "PATCH": 32 | // biome-ignore lint/suspicious/noFallthroughSwitchClause: 33 | case "DELETE": 34 | // 格式判断 35 | switch (FORMAT) { 36 | case undefined: // 视为无body 37 | break; 38 | case "application/x-www-form-urlencoded": 39 | case "text/plain": 40 | default: 41 | break; 42 | case "application/x-mpegURL": 43 | case "application/x-mpegurl": 44 | case "application/vnd.apple.mpegurl": 45 | case "audio/mpegurl": 46 | break; 47 | case "text/xml": 48 | case "text/html": 49 | case "text/plist": 50 | case "application/xml": 51 | case "application/plist": 52 | case "application/x-plist": 53 | break; 54 | case "text/vtt": 55 | case "application/vtt": 56 | break; 57 | case "text/json": 58 | case "application/json": 59 | body = JSON.parse($request.body ?? "{}"); 60 | switch (url.hostname) { 61 | case "testflight.apple.com": 62 | switch (url.pathname) { 63 | case "/v1/session/authenticate": 64 | if (Settings.CountryCode !== "AUTO") body.storeFrontIdentifier = body.storeFrontIdentifier.replace(/\d{6}/, Configs.Storefront[Settings.CountryCode]); 65 | break; 66 | case "/v1/properties/testflight": 67 | break; 68 | case "/v1/devices": 69 | case "/v1/devices/apns": 70 | case "/v1/devices/add": 71 | case "/v1/devices/remove": 72 | break; 73 | default: 74 | switch (PATHs[0]) { 75 | case "v1": 76 | case "v2": 77 | case "v3": 78 | switch (PATHs[1]) { 79 | case "accounts": 80 | switch (PATHs[2]) { 81 | case "settings": 82 | switch (PATHs[3]) { 83 | case "notifications": 84 | switch (PATHs[4]) { 85 | case "apps": 86 | switch (PATHs[5]) { 87 | case undefined: 88 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/settings/notifications/apps`); 89 | break; 90 | default: 91 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/settings/notifications/apps/${PATHs[5]}`); 92 | switch (Settings.MergeNotifications) { 93 | case true: 94 | default: { 95 | Console.info("合并通知"); 96 | const Platform = detectPlatform($request.headers?.["User-Agent"] ?? $request.headers?.["user-agent"]); 97 | const EmailEnabled = _.get(body, `platformUpdates.${Platform}.emailEnabled`); 98 | Console.info(`EmailEnabled: ${EmailEnabled}`); 99 | _.set(body, "platformUpdates.appletvos.emailEnabled", EmailEnabled); 100 | _.set(body, "platformUpdates.ios.emailEnabled", EmailEnabled); 101 | _.set(body, "platformUpdates.osx.emailEnabled", EmailEnabled); 102 | _.set(body, "platformUpdates.xros.emailEnabled", EmailEnabled); 103 | break; 104 | } 105 | case false: 106 | Console.info("不合并通知"); 107 | break; 108 | } 109 | break; 110 | } 111 | 112 | break; 113 | } 114 | break; 115 | } 116 | break; 117 | case Caches?.data?.accountId: // UUID 118 | default: 119 | switch (PATHs[3]) { 120 | case "apps": 121 | switch (PATHs[4]) { 122 | default: 123 | switch (PATHs[5]) { 124 | case "builds": 125 | switch (PATHs[7]) { 126 | case undefined: 127 | break; 128 | case "install": 129 | if (Settings.CountryCode !== "AUTO") body.storefrontId = body.storefrontId.replace(/\d{6}/, Configs.Storefront[Settings.CountryCode]); 130 | break; 131 | default: 132 | break; 133 | } 134 | break; 135 | } 136 | break; 137 | } 138 | break; 139 | } 140 | break; 141 | } 142 | break; 143 | } 144 | break; 145 | } 146 | break; 147 | } 148 | break; 149 | } 150 | $request.body = JSON.stringify(body); 151 | break; 152 | case "application/protobuf": 153 | case "application/x-protobuf": 154 | case "application/vnd.google.protobuf": 155 | case "application/grpc": 156 | case "application/grpc+proto": 157 | case "applecation/octet-stream": 158 | break; 159 | } 160 | //break; // 不中断,继续处理URL 161 | case "GET": 162 | case "HEAD": 163 | case "OPTIONS": 164 | default: 165 | // 主机判断 166 | switch (url.hostname) { 167 | case "testflight.apple.com": 168 | // 路径判断 169 | switch (url.pathname) { 170 | case "/v1/session/authenticate": 171 | break; 172 | case "v1/properties/testflight": 173 | //$request.headers["X-Apple-Rosetta-Available"] = Settings.Rosetta; 174 | break; 175 | case "/v1/devices": 176 | case "/v1/devices/apns": 177 | case "/v1/devices/add": 178 | case "/v1/devices/remove": 179 | break; 180 | default: 181 | // headers auth mod 182 | switch (Settings.MultiAccount) { 183 | case true: { 184 | Console.info("启用多账号支持"); 185 | const IfNoneMatch = $request?.headers?.["If-None-Match"] ?? $request?.headers?.["if-none-match"]; 186 | const XRequestId = $request?.headers?.["X-Request-Id"] ?? $request?.headers?.["x-request-id"]; 187 | const XSessionId = $request?.headers?.["X-Session-Id"] ?? $request?.headers?.["x-session-id"]; 188 | const XSessionDigest = $request?.headers?.["X-Session-Digest"] ?? $request?.headers?.["x-session-digest"]; 189 | if (Caches.data) { 190 | // Caches.data存在 191 | Console.info("Caches.data存在,读取"); 192 | switch (PATHs[0]) { 193 | case "v1": 194 | case "v2": 195 | case "v3": 196 | switch (PATHs[1]) { 197 | case "accounts": 198 | case "messages": 199 | case "apps": 200 | default: 201 | switch (PATHs[2]) { 202 | case "settings": 203 | case undefined: 204 | default: 205 | switch (/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/.test(PATHs[2])) { 206 | // biome-ignore lint/suspicious/noFallthroughSwitchClause: 207 | case true: // PATHs[2]是UUID 208 | Console.info("PATHs[2]是UUID,替换url.pathname"); 209 | url.pathname = url.pathname.replace(/\/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}\//i, `/${Caches.data.accountId}/`); 210 | //break; // 不中断,继续处理 211 | case false: // PATHs[2]不是UUID 212 | if (XSessionId !== Caches.headers["X-Session-Id"]) { 213 | // sessionId不同 214 | Console.info("sessionId不同,替换$request.headers"); 215 | if (IfNoneMatch) { 216 | delete $request.headers?.["If-None-Match"]; 217 | delete $request.headers?.["if-none-match"]; 218 | } 219 | if (XRequestId) { 220 | if ($request.headers?.["X-Request-Id"]) $request.headers["X-Request-Id"] = Caches.headers["X-Request-Id"]; 221 | if ($request.headers?.["x-request-id"]) $request.headers["x-request-id"] = Caches.headers["X-Request-Id"]; 222 | } 223 | if (XSessionId) { 224 | if ($request.headers?.["X-Session-Id"]) $request.headers["X-Session-Id"] = Caches.headers["X-Session-Id"]; 225 | if ($request.headers?.["x-session-id"]) $request.headers["x-session-id"] = Caches.headers["X-Session-Id"]; 226 | } 227 | if (XSessionDigest) { 228 | if ($request.headers?.["X-Session-Digest"]) $request.headers["X-Session-Digest"] = Caches.headers["X-Session-Digest"]; 229 | if ($request.headers?.["x-session-digest"]) $request.headers["x-session-digest"] = Caches.headers["X-Session-Digest"]; 230 | } 231 | } 232 | } 233 | break; 234 | case Caches?.data?.accountId: // PATHs[2]有UUID且与accountId相同 235 | Console.info("PATHs[2]与accountId相同,更新Caches"); 236 | Caches.headers = { 237 | "X-Request-Id": XRequestId, 238 | "X-Session-Id": XSessionId, 239 | "X-Session-Digest": XSessionDigest, 240 | }; 241 | Storage.setItem("@iRingo.TestFlight.Caches", Caches); 242 | break; 243 | } 244 | break; 245 | case "tc": // termsAndConditions 246 | break; 247 | } 248 | break; 249 | } 250 | break; 251 | } else { 252 | // Caches空 253 | Console.info("Caches空,新写入"); 254 | Caches.headers = { 255 | "X-Request-Id": XRequestId, 256 | "X-Session-Id": XSessionId, 257 | "X-Session-Digest": XSessionDigest, 258 | }; 259 | if (/[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}/.test(PATHs[2])) { 260 | Caches.data = { 261 | accountId: PATHs[2], 262 | sessionId: XSessionId, 263 | }; 264 | } 265 | Storage.setItem("@iRingo.TestFlight.Caches", Caches); 266 | } 267 | break; 268 | } 269 | case false: 270 | default: 271 | Console.info("关闭多账号支持"); 272 | break; 273 | } 274 | break; 275 | } 276 | break; 277 | } 278 | break; 279 | case "CONNECT": 280 | case "TRACE": 281 | break; 282 | } 283 | $request.url = url.toString(); 284 | Console.debug(`$request.url: ${$request.url}`); 285 | })() 286 | .catch(e => Console.error(e)) 287 | .finally(() => { 288 | switch (typeof $response) { 289 | case "object": // 有构造回复数据,返回构造的回复数据 290 | //Console.debug("finally", `echo $response: ${JSON.stringify($response, null, 2)}`); 291 | if ($response.headers?.["Content-Encoding"]) $response.headers["Content-Encoding"] = "identity"; 292 | if ($response.headers?.["content-encoding"]) $response.headers["content-encoding"] = "identity"; 293 | switch ($app) { 294 | default: 295 | done({ response: $response }); 296 | break; 297 | case "Quantumult X": 298 | if (!$response.status) $response.status = "HTTP/1.1 200 OK"; 299 | delete $response.headers?.["Content-Length"]; 300 | delete $response.headers?.["content-length"]; 301 | delete $response.headers?.["Transfer-Encoding"]; 302 | done($response); 303 | break; 304 | } 305 | break; 306 | case "undefined": // 无构造回复数据,发送修改的请求数据 307 | //Console.debug("finally", `$request: ${JSON.stringify($request, null, 2)}`); 308 | done($request); 309 | break; 310 | default: 311 | Console.error(`不合法的 $response 类型: ${typeof $response}`); 312 | break; 313 | } 314 | }); 315 | -------------------------------------------------------------------------------- /src/response.dev.js: -------------------------------------------------------------------------------- 1 | import { Console, done, Lodash as _, Storage } from "@nsnanocat/util"; 2 | import { URL } from "@nsnanocat/url"; 3 | import database from "./function/database.mjs"; 4 | import setENV from "./function/setENV.mjs"; 5 | /***************** Processing *****************/ 6 | // 解构URL 7 | const url = new URL($request.url); 8 | Console.info(`url: ${url.toJSON()}`); 9 | // 获取连接参数 10 | const PATHs = url.pathname.split("/").filter(Boolean); 11 | Console.info(`PATHs: ${PATHs}`); 12 | // 解析格式 13 | const FORMAT = ($response.headers?.["Content-Type"] ?? $response.headers?.["content-type"])?.split(";")?.[0]; 14 | Console.info(`FORMAT: ${FORMAT}`); 15 | (async () => { 16 | /** 17 | * @type {{Settings: import('./types').Settings}} 18 | */ 19 | const { Settings, Caches, Configs } = setENV("iRingo", "TestFlight", database); 20 | Console.logLevel = Settings.LogLevel; 21 | // 创建空数据 22 | let body = {}; 23 | // 格式判断 24 | switch (FORMAT) { 25 | case undefined: // 视为无body 26 | break; 27 | case "application/x-www-form-urlencoded": 28 | case "text/plain": 29 | default: 30 | break; 31 | case "application/x-mpegURL": 32 | case "application/x-mpegurl": 33 | case "application/vnd.apple.mpegurl": 34 | case "audio/mpegurl": 35 | //body = M3U8.parse($response.body); 36 | //Console.debug(`body: ${JSON.stringify(body)}`); 37 | //$response.body = M3U8.stringify(body); 38 | break; 39 | case "text/xml": 40 | case "text/html": 41 | case "text/plist": 42 | case "application/xml": 43 | case "application/plist": 44 | case "application/x-plist": 45 | //body = XML.parse($response.body); 46 | //Console.debug(`body: ${JSON.stringify(body)}`); 47 | //$response.body = XML.stringify(body); 48 | break; 49 | case "text/vtt": 50 | case "application/vtt": 51 | //body = VTT.parse($response.body); 52 | //Console.debug(`body: ${JSON.stringify(body)}`); 53 | //$response.body = VTT.stringify(body); 54 | break; 55 | case "text/json": 56 | case "application/json": 57 | body = JSON.parse($response.body ?? "{}"); 58 | Console.debug(`body: ${JSON.stringify(body)}`); 59 | // 主机判断 60 | switch (url.hostname) { 61 | case "testflight.apple.com": 62 | // 路径判断 63 | switch (url.pathname) { 64 | case "/v1/session/authenticate": 65 | switch (Settings.MultiAccount) { 66 | case true: { 67 | Console.info("启用多账号支持"); 68 | const XRequestId = $request?.headers?.["X-Request-Id"] ?? $request?.headers?.["x-request-id"]; 69 | const XSessionId = $request?.headers?.["X-Session-Id"] ?? $request?.headers?.["x-session-id"]; 70 | const XSessionDigest = $request?.headers?.["X-Session-Digest"] ?? $request?.headers?.["x-session-digest"]; 71 | if (Caches?.data) { 72 | //有data 73 | Console.info("有Caches.data"); 74 | if (body?.data?.accountId === Caches?.data?.accountId) { 75 | // Account ID相等,刷新缓存 76 | Console.info("Account ID相等,刷新缓存"); 77 | Caches.headers = { 78 | "X-Request-Id": XRequestId, 79 | "X-Session-Id": XSessionId, 80 | "X-Session-Digest": XSessionDigest, 81 | }; 82 | Caches.data = body.data; 83 | Caches.data.termsAndConditions = null; 84 | Caches.data.hasNewTermsAndConditions = false; 85 | Storage.setItem("@iRingo.TestFlight.Caches", Caches); 86 | } 87 | /* 88 | else { // Account ID不相等,覆盖 89 | Console.info(`Account ID不相等,覆盖data(accountId和sessionId)`); 90 | body.data = Caches.data; 91 | } 92 | */ 93 | } else { 94 | // Caches空 95 | Console.debug("Caches空,写入"); 96 | Caches.headers = { 97 | "X-Request-Id": XRequestId, 98 | "X-Session-Id": XSessionId, 99 | "X-Session-Digest": XSessionDigest, 100 | }; 101 | Caches.data = body.data; 102 | Caches.data.termsAndConditions = null; 103 | Caches.data.hasNewTermsAndConditions = false; 104 | Storage.setItem("@iRingo.TestFlight.Caches", Caches); 105 | } 106 | break; 107 | } 108 | case false: 109 | default: 110 | Console.info("关闭多账号支持"); 111 | break; 112 | } 113 | break; 114 | case "/v1/devices": 115 | case "/v1/devices/apns": 116 | case "/v1/devices/add": 117 | case "/v1/devices/remove": 118 | break; 119 | default: 120 | switch (PATHs[0]) { 121 | case "v1": 122 | case "v2": 123 | case "v3": 124 | switch (PATHs[1]) { 125 | case "accounts": 126 | switch (PATHs[2]) { 127 | case "settings": 128 | switch (PATHs[3]) { 129 | case undefined: 130 | Console.debug(`/${PATHs[0]}/accounts/settings`); 131 | break; 132 | case "notifications": 133 | switch (PATHs[4]) { 134 | case "apps": 135 | Console.debug(`/${PATHs[0]}/accounts/settings/notifications/apps/`); 136 | break; 137 | } 138 | break; 139 | default: 140 | Console.debug(`/${PATHs[0]}/accounts/settings/${PATHs[3]}/`); 141 | break; 142 | } 143 | break; 144 | case Caches?.data?.accountId: // UUID 145 | default: 146 | switch (PATHs[3]) { 147 | case undefined: 148 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}`); 149 | break; 150 | case "apps": 151 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/`); 152 | switch (PATHs[4]) { 153 | case undefined: 154 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps`); 155 | switch (Settings.Universal) { 156 | case true: 157 | default: 158 | Console.info("启用通用应用支持"); 159 | if (body.error === null) { 160 | // 数据无错误 161 | Console.debug("数据无错误"); 162 | body.data = body.data.map(app => { 163 | if (app.previouslyTested !== false) { 164 | // 不是前测试人员 165 | Console.debug("不是前测试人员"); 166 | app.platforms = app.platforms.map(platform => { 167 | platform.build = modBuild(platform.build); 168 | return platform; 169 | }); 170 | } 171 | return app; 172 | }); 173 | } 174 | break; 175 | case false: 176 | Console.info("启用通用应用支持"); 177 | break; 178 | } 179 | break; 180 | default: 181 | switch (PATHs[5]) { 182 | case undefined: 183 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}`); 184 | break; 185 | case "builds": 186 | switch (PATHs[7]) { 187 | case undefined: 188 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/builds/${PATHs[6]}`); 189 | switch (Settings.Universal) { 190 | case true: 191 | default: 192 | Console.info("启用通用应用支持"); 193 | if (body.error === null) { 194 | // 数据无错误 195 | Console.debug("数据无错误"); 196 | // 当前Bulid 197 | body.data.currentBuild = modBuild(body.data.currentBuild); 198 | // Build列表 199 | body.data.builds = body.data.builds.map(build => modBuild(build)); 200 | } 201 | break; 202 | case false: 203 | Console.info("关闭通用应用支持"); 204 | break; 205 | } 206 | break; 207 | case "install": 208 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/builds/${PATHs[6]}/install`); 209 | break; 210 | default: 211 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/builds/${PATHs[6]}/${PATHs[7]}`); 212 | break; 213 | } 214 | break; 215 | case "platforms": 216 | switch (PATHs[6]) { 217 | case "ios": 218 | case "osx": 219 | case "appletvos": 220 | default: 221 | switch (PATHs[7]) { 222 | case undefined: 223 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/platforms/${PATHs[6]}`); 224 | break; 225 | case "trains": 226 | switch (PATHs[9]) { 227 | case undefined: 228 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/platforms/${PATHs[6]}/trains/${PATHs[8]}`); 229 | break; 230 | case "builds": 231 | switch (PATHs[10]) { 232 | case undefined: 233 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/platforms/${PATHs[6]}/trains/${PATHs[8]}/builds`); 234 | switch (Settings.Universal) { 235 | case true: 236 | default: 237 | Console.info("启用通用应用支持"); 238 | if (body.error === null) { 239 | // 数据无错误 240 | Console.debug("数据无错误"); 241 | // 当前Bulid 242 | body.data = body.data.map(data => modBuild(data)); 243 | } 244 | break; 245 | case false: 246 | Console.info("关闭通用应用支持"); 247 | break; 248 | } 249 | break; 250 | default: 251 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/platforms/${PATHs[6]}/trains/${PATHs[8]}/builds/${PATHs[10]}`); 252 | break; 253 | } 254 | break; 255 | default: 256 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/platforms/${PATHs[6]}/trains/${PATHs[8]}/${PATHs[9]}`); 257 | break; 258 | } 259 | break; 260 | default: 261 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/platforms/${PATHs[6]}/${PATHs[7]}`); 262 | break; 263 | } 264 | break; 265 | } 266 | break; 267 | default: 268 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/apps/${PATHs[4]}/${PATHs[5]}`); 269 | break; 270 | } 271 | break; 272 | } 273 | break; 274 | case "ru": 275 | switch (Settings.AlwaysShowInstall) { 276 | case true: 277 | Console.info("总是显示安装选项"); 278 | switch (body.data?.status) { 279 | case "OPEN": 280 | break; 281 | case "FULL": 282 | body.data.status = "OPEN"; 283 | //body.data.message = "此 Beta 版本的测试员已满。"; 284 | body.data.app.eligibility = { 285 | status: "NO_CRITERIA", 286 | criteria: null, 287 | }; 288 | break; 289 | case undefined: 290 | default: 291 | break; 292 | } 293 | break; 294 | case false: 295 | default: 296 | Console.info("不显示安装选项"); 297 | break; 298 | } 299 | break; 300 | default: 301 | Console.debug(`/${PATHs[0]}/accounts/${PATHs[2]}/${PATHs[3]}/`); 302 | break; 303 | } 304 | break; 305 | } 306 | break; 307 | case "apps": 308 | switch (PATHs[3]) { 309 | case "install": 310 | switch (PATHs[4]) { 311 | case undefined: 312 | Console.debug(`/${PATHs[0]}/apps/install`); 313 | break; 314 | case "status": 315 | Console.debug(`/${PATHs[0]}/apps/install/status`); 316 | break; 317 | default: 318 | Console.debug(`/${PATHs[0]}/apps/install/${PATHs[4]}`); 319 | break; 320 | } 321 | break; 322 | } 323 | break; 324 | case "messages": 325 | switch (PATHs[2]) { 326 | case Caches?.data?.accountId: // UUID 327 | default: 328 | Console.debug(`/${PATHs[0]}/messages/${PATHs[2]}`); 329 | switch (PATHs[3]) { 330 | case undefined: 331 | Console.debug(`/${PATHs[0]}/messages/${PATHs[2]}`); 332 | break; 333 | case "read": 334 | Console.debug(`/${PATHs[0]}/messages/${PATHs[2]}/read`); 335 | break; 336 | default: 337 | Console.debug(`/${PATHs[0]}/messages/${PATHs[2]}/${PATHs[3]}`); 338 | break; 339 | } 340 | break; 341 | } 342 | break; 343 | } 344 | break; 345 | } 346 | break; 347 | } 348 | break; 349 | } 350 | $response.body = JSON.stringify(body); 351 | break; 352 | case "application/protobuf": 353 | case "application/x-protobuf": 354 | case "application/vnd.google.protobuf": 355 | case "application/grpc": 356 | case "application/grpc+proto": 357 | case "application/octet-stream": 358 | break; 359 | } 360 | })() 361 | .catch(e => Console.error(e)) 362 | .finally(() => done($response)); 363 | 364 | /***************** Function *****************/ 365 | /** 366 | * mod Build 367 | * @author VirgilClyne 368 | * @param {Object} build - Build 369 | * @return {Object} 370 | */ 371 | function modBuild(build) { 372 | Console.debug(build.platform || build.name); 373 | switch (build.platform || build.name) { 374 | case "ios": 375 | build = Build(build); 376 | break; 377 | case "osx": 378 | if (build?.macBuildCompatibility?.runsOnAppleSilicon === true) { 379 | // 是苹果芯片 380 | Console.debug("runsOnAppleSilicon"); 381 | build = Build(build); 382 | } 383 | break; 384 | case "appletvos": 385 | break; 386 | default: 387 | break; 388 | } 389 | return build; 390 | 391 | function Build(build) { 392 | //if (build.universal === true) { 393 | build.compatible = true; 394 | build.platformCompatible = true; 395 | build.hardwareCompatible = true; 396 | build.osCompatible = true; 397 | if (build?.permission) build.permission = "install"; 398 | if (build?.deviceFamilyInfo) { 399 | build.deviceFamilyInfo = [ 400 | { 401 | number: 1, 402 | name: "iOS", 403 | iconUrl: "https://itunesconnect-mr.itunes.apple.com/itc/img/device-icons/device_family_icon_1.png", 404 | }, 405 | { 406 | number: 2, 407 | name: "iPad", 408 | iconUrl: "https://itunesconnect-mr.itunes.apple.com/itc/img/device-icons/device_family_icon_2.png", 409 | }, 410 | { 411 | number: 3, 412 | name: "Apple TV", 413 | iconUrl: "https://itunesconnect-mr.itunes.apple.com/itc/img/device-icons/device_family_icon_3.png", 414 | }, 415 | ]; 416 | } 417 | if (build?.compatibilityData?.compatibleDeviceFamilies) { 418 | build.compatibilityData.compatibleDeviceFamilies = [ 419 | { 420 | name: "iPad", 421 | minimumSupportedDevice: null, 422 | unsupportedDevices: [], 423 | }, 424 | { 425 | name: "iPhone", 426 | minimumSupportedDevice: null, 427 | unsupportedDevices: [], 428 | }, 429 | { 430 | name: "iPod", 431 | minimumSupportedDevice: null, 432 | unsupportedDevices: [], 433 | }, 434 | { 435 | name: "Mac", 436 | minimumSupportedDevice: null, 437 | unsupportedDevices: [], 438 | }, 439 | ]; 440 | } 441 | if (build.macBuildCompatibility) { 442 | build.macBuildCompatibility.runsOnIntel = true; 443 | build.macBuildCompatibility.runsOnAppleSilicon = true; 444 | /* 445 | build.macBuildCompatibility = { 446 | "macArchitectures": ["AppleSilicon", "Intel"], 447 | "rosettaCompatible": true, 448 | "runsOnIntel": true, 449 | "runsOnAppleSilicon": true, 450 | "requiresRosetta": false 451 | }; 452 | */ 453 | } 454 | //}; 455 | return build; 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /src/response.js: -------------------------------------------------------------------------------- 1 | import { Console, done, Lodash as _, Storage } from "@nsnanocat/util"; 2 | import { URL } from "@nsnanocat/url"; 3 | import database from "./function/database.mjs"; 4 | import setENV from "./function/setENV.mjs"; 5 | /***************** Processing *****************/ 6 | // 解构URL 7 | const url = new URL($request.url); 8 | Console.info(`url: ${url.toJSON()}`); 9 | // 获取连接参数 10 | const PATHs = url.pathname.split("/").filter(Boolean); 11 | Console.info(`PATHs: ${PATHs}`); 12 | // 解析格式 13 | const FORMAT = ($response.headers?.["Content-Type"] ?? $response.headers?.["content-type"])?.split(";")?.[0]; 14 | Console.info(`FORMAT: ${FORMAT}`); 15 | (async () => { 16 | /** 17 | * @type {{Settings: import('./types').Settings}} 18 | */ 19 | const { Settings, Caches, Configs } = setENV("iRingo", "TestFlight", database); 20 | Console.logLevel = Settings.LogLevel; 21 | // 创建空数据 22 | let body = {}; 23 | // 格式判断 24 | switch (FORMAT) { 25 | case undefined: // 视为无body 26 | break; 27 | case "application/x-www-form-urlencoded": 28 | case "text/plain": 29 | default: 30 | break; 31 | case "application/x-mpegURL": 32 | case "application/x-mpegurl": 33 | case "application/vnd.apple.mpegurl": 34 | case "audio/mpegurl": 35 | break; 36 | case "text/xml": 37 | case "text/html": 38 | case "text/plist": 39 | case "application/xml": 40 | case "application/plist": 41 | case "application/x-plist": 42 | break; 43 | case "text/vtt": 44 | case "application/vtt": 45 | break; 46 | case "text/json": 47 | case "application/json": 48 | body = JSON.parse($response.body ?? "{}"); 49 | // 主机判断 50 | switch (url.hostname) { 51 | case "testflight.apple.com": 52 | // 路径判断 53 | switch (url.pathname) { 54 | case "/v1/session/authenticate": 55 | switch (Settings.MultiAccount) { 56 | case true: { 57 | Console.info("启用多账号支持"); 58 | const XRequestId = $request?.headers?.["X-Request-Id"] ?? $request?.headers?.["x-request-id"]; 59 | const XSessionId = $request?.headers?.["X-Session-Id"] ?? $request?.headers?.["x-session-id"]; 60 | const XSessionDigest = $request?.headers?.["X-Session-Digest"] ?? $request?.headers?.["x-session-digest"]; 61 | if (Caches?.data) { 62 | //有data 63 | Console.info("有Caches.data"); 64 | if (body?.data?.accountId === Caches?.data?.accountId) { 65 | // Account ID相等,刷新缓存 66 | Console.info("Account ID相等,刷新缓存"); 67 | Caches.headers = { 68 | "X-Request-Id": XRequestId, 69 | "X-Session-Id": XSessionId, 70 | "X-Session-Digest": XSessionDigest, 71 | }; 72 | Caches.data = body.data; 73 | Caches.data.termsAndConditions = null; 74 | Caches.data.hasNewTermsAndConditions = false; 75 | Storage.setItem("@iRingo.TestFlight.Caches", Caches); 76 | } 77 | } else { 78 | // Caches空 79 | Console.debug("Caches空,写入"); 80 | Caches.headers = { 81 | "X-Request-Id": XRequestId, 82 | "X-Session-Id": XSessionId, 83 | "X-Session-Digest": XSessionDigest, 84 | }; 85 | Caches.data = body.data; 86 | Caches.data.termsAndConditions = null; 87 | Caches.data.hasNewTermsAndConditions = false; 88 | Storage.setItem("@iRingo.TestFlight.Caches", Caches); 89 | } 90 | break; 91 | } 92 | case false: 93 | default: 94 | break; 95 | } 96 | break; 97 | case "/v1/devices": 98 | case "/v1/devices/apns": 99 | case "/v1/devices/add": 100 | case "/v1/devices/remove": 101 | break; 102 | default: 103 | switch (PATHs[0]) { 104 | case "v1": 105 | case "v2": 106 | case "v3": 107 | switch (PATHs[1]) { 108 | case "accounts": 109 | switch (PATHs[2]) { 110 | case "settings": 111 | switch (PATHs[3]) { 112 | case undefined: 113 | break; 114 | case "notifications": 115 | switch (PATHs[4]) { 116 | case "apps": 117 | break; 118 | } 119 | break; 120 | default: 121 | break; 122 | } 123 | break; 124 | case Caches?.data?.accountId: // UUID 125 | default: 126 | switch (PATHs[3]) { 127 | case undefined: 128 | break; 129 | case "apps": 130 | switch (PATHs[4]) { 131 | case undefined: 132 | switch (Settings.Universal) { 133 | case true: 134 | default: 135 | Console.info("启用通用应用支持"); 136 | if (body.error === null) { 137 | // 数据无错误 138 | Console.debug("数据无错误"); 139 | body.data = body.data.map(app => { 140 | if (app.previouslyTested !== false) { 141 | // 不是前测试人员 142 | Console.debug("不是前测试人员"); 143 | app.platforms = app.platforms.map(platform => { 144 | platform.build = modBuild(platform.build); 145 | return platform; 146 | }); 147 | } 148 | return app; 149 | }); 150 | } 151 | break; 152 | case false: 153 | Console.info("启用通用应用支持"); 154 | break; 155 | } 156 | break; 157 | default: 158 | switch (PATHs[5]) { 159 | case undefined: 160 | break; 161 | case "builds": 162 | switch (PATHs[7]) { 163 | case undefined: 164 | switch (Settings.Universal) { 165 | case true: 166 | default: 167 | Console.info("启用通用应用支持"); 168 | if (body.error === null) { 169 | // 数据无错误 170 | Console.debug("数据无错误"); 171 | // 当前Bulid 172 | body.data.currentBuild = modBuild(body.data.currentBuild); 173 | // Build列表 174 | body.data.builds = body.data.builds.map(build => modBuild(build)); 175 | } 176 | break; 177 | case false: 178 | Console.info("关闭通用应用支持"); 179 | break; 180 | } 181 | break; 182 | case "install": 183 | break; 184 | default: 185 | break; 186 | } 187 | break; 188 | case "platforms": 189 | switch (PATHs[6]) { 190 | case "ios": 191 | case "osx": 192 | case "appletvos": 193 | default: 194 | switch (PATHs[7]) { 195 | case undefined: 196 | break; 197 | case "trains": 198 | switch (PATHs[9]) { 199 | case undefined: 200 | break; 201 | case "builds": 202 | switch (PATHs[10]) { 203 | case undefined: 204 | switch (Settings.Universal) { 205 | case true: 206 | default: 207 | Console.info("启用通用应用支持"); 208 | if (body.error === null) { 209 | // 数据无错误 210 | Console.debug("数据无错误"); 211 | // 当前Bulid 212 | body.data = body.data.map(data => modBuild(data)); 213 | } 214 | break; 215 | case false: 216 | Console.info("关闭通用应用支持"); 217 | break; 218 | } 219 | break; 220 | default: 221 | break; 222 | } 223 | break; 224 | default: 225 | break; 226 | } 227 | break; 228 | default: 229 | break; 230 | } 231 | break; 232 | } 233 | break; 234 | default: 235 | break; 236 | } 237 | break; 238 | } 239 | break; 240 | case "ru": 241 | switch (Settings.AlwaysShowInstall) { 242 | case true: 243 | Console.info("总是显示安装选项"); 244 | switch (body.data?.status) { 245 | case "OPEN": 246 | break; 247 | case "FULL": 248 | body.data.status = "OPEN"; 249 | //body.data.message = "此 Beta 版本的测试员已满。"; 250 | body.data.app.eligibility = { 251 | status: "NO_CRITERIA", 252 | criteria: null, 253 | }; 254 | break; 255 | case undefined: 256 | default: 257 | break; 258 | } 259 | break; 260 | case false: 261 | default: 262 | Console.info("不显示安装选项"); 263 | break; 264 | } 265 | break; 266 | default: 267 | break; 268 | } 269 | break; 270 | } 271 | break; 272 | case "apps": 273 | switch (PATHs[3]) { 274 | case "install": 275 | switch (PATHs[4]) { 276 | case undefined: 277 | break; 278 | case "status": 279 | break; 280 | default: 281 | break; 282 | } 283 | break; 284 | } 285 | break; 286 | case "messages": 287 | switch (PATHs[2]) { 288 | case Caches?.data?.accountId: // UUID 289 | default: 290 | switch (PATHs[3]) { 291 | case undefined: 292 | break; 293 | case "read": 294 | break; 295 | default: 296 | break; 297 | } 298 | break; 299 | } 300 | break; 301 | } 302 | break; 303 | } 304 | break; 305 | } 306 | break; 307 | } 308 | $response.body = JSON.stringify(body); 309 | break; 310 | case "application/protobuf": 311 | case "application/x-protobuf": 312 | case "application/vnd.google.protobuf": 313 | case "application/grpc": 314 | case "application/grpc+proto": 315 | case "application/octet-stream": 316 | break; 317 | } 318 | })() 319 | .catch(e => Console.error(e)) 320 | .finally(() => done($response)); 321 | 322 | /***************** Function *****************/ 323 | /** 324 | * mod Build 325 | * @author VirgilClyne 326 | * @param {Object} build - Build 327 | * @return {Object} 328 | */ 329 | function modBuild(build) { 330 | Console.debug(build.platform || build.name); 331 | switch (build.platform || build.name) { 332 | case "ios": 333 | build = Build(build); 334 | break; 335 | case "osx": 336 | if (build?.macBuildCompatibility?.runsOnAppleSilicon === true) { 337 | // 是苹果芯片 338 | Console.debug("runsOnAppleSilicon"); 339 | build = Build(build); 340 | } 341 | break; 342 | case "appletvos": 343 | break; 344 | default: 345 | break; 346 | } 347 | return build; 348 | 349 | function Build(build) { 350 | //if (build.universal === true) { 351 | build.compatible = true; 352 | build.platformCompatible = true; 353 | build.hardwareCompatible = true; 354 | build.osCompatible = true; 355 | if (build?.permission) build.permission = "install"; 356 | if (build?.deviceFamilyInfo) { 357 | build.deviceFamilyInfo = [ 358 | { 359 | number: 1, 360 | name: "iOS", 361 | iconUrl: "https://itunesconnect-mr.itunes.apple.com/itc/img/device-icons/device_family_icon_1.png", 362 | }, 363 | { 364 | number: 2, 365 | name: "iPad", 366 | iconUrl: "https://itunesconnect-mr.itunes.apple.com/itc/img/device-icons/device_family_icon_2.png", 367 | }, 368 | { 369 | number: 3, 370 | name: "Apple TV", 371 | iconUrl: "https://itunesconnect-mr.itunes.apple.com/itc/img/device-icons/device_family_icon_3.png", 372 | }, 373 | ]; 374 | } 375 | if (build?.compatibilityData?.compatibleDeviceFamilies) { 376 | build.compatibilityData.compatibleDeviceFamilies = [ 377 | { 378 | name: "iPad", 379 | minimumSupportedDevice: null, 380 | unsupportedDevices: [], 381 | }, 382 | { 383 | name: "iPhone", 384 | minimumSupportedDevice: null, 385 | unsupportedDevices: [], 386 | }, 387 | { 388 | name: "iPod", 389 | minimumSupportedDevice: null, 390 | unsupportedDevices: [], 391 | }, 392 | { 393 | name: "Mac", 394 | minimumSupportedDevice: null, 395 | unsupportedDevices: [], 396 | }, 397 | ]; 398 | } 399 | if (build.macBuildCompatibility) { 400 | build.macBuildCompatibility.runsOnIntel = true; 401 | build.macBuildCompatibility.runsOnAppleSilicon = true; 402 | /* 403 | build.macBuildCompatibility = { 404 | "macArchitectures": ["AppleSilicon", "Intel"], 405 | "rosettaCompatible": true, 406 | "runsOnIntel": true, 407 | "runsOnAppleSilicon": true, 408 | "requiresRosetta": false 409 | }; 410 | */ 411 | } 412 | //}; 413 | return build; 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Settings { 2 | /** 3 | * 国家或地区代码 4 | * 5 | * 不同国家或地区提供的内容或有差别。 6 | * 7 | * @remarks 8 | * 9 | * Possible values: 10 | * - `'AUTO'` - 🇺🇳自动(跟随地区检测结果) 11 | * - `'CN'` - 🇨🇳中国大陆 12 | * - `'HK'` - 🇭🇰香港 13 | * - `'TW'` - 🇹🇼台湾 14 | * - `'SG'` - 🇸🇬新加坡 15 | * - `'US'` - 🇺🇸美国 16 | * - `'JP'` - 🇯🇵日本 17 | * - `'AU'` - 🇦🇺澳大利亚 18 | * - `'GB'` - 🇬🇧英国 19 | * - `'KR'` - 🇰🇷韩国 20 | * - `'CA'` - 🇨🇦加拿大 21 | * - `'IE'` - 🇮🇪爱尔兰 22 | * 23 | * @defaultValue "US" 24 | */ 25 | CountryCode?: 'AUTO' | 'CN' | 'HK' | 'TW' | 'SG' | 'US' | 'JP' | 'AU' | 'GB' | 'KR' | 'CA' | 'IE'; 26 | /** 27 | * 启用多账号支持 28 | * 29 | * 是否启用多账号支持,会自动保存保存更新当前账号信息。 30 | * 31 | * @defaultValue false 32 | */ 33 | MultiAccount?: boolean; 34 | /** 35 | * 启用通用应用支持 36 | * 37 | * 是否启用通用应用支持,解除 TestFlight app 的 iOS/iPadOS/macOS(AppleSilicon) 平台限制。 38 | * 39 | * @defaultValue true 40 | */ 41 | Universal?: boolean; 42 | /** 43 | * [调试] 日志等级 44 | * 45 | * 选择脚本日志的输出等级,低于所选等级的日志将全部输出。 46 | * 47 | * @remarks 48 | * 49 | * Possible values: 50 | * - `'OFF'` - 关闭 51 | * - `'ERROR'` - ❌ 错误 52 | * - `'WARN'` - ⚠️ 警告 53 | * - `'INFO'` - ℹ️ 信息 54 | * - `'DEBUG'` - 🅱️ 调试 55 | * - `'ALL'` - 全部 56 | * 57 | * @defaultValue "WARN" 58 | */ 59 | LogLevel?: 'OFF' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'ALL'; 60 | } 61 | -------------------------------------------------------------------------------- /template/boxjs.settings.json: -------------------------------------------------------------------------------- 1 | [{"id":"@iRingo.TestFlight.Settings.CountryCode","name":"国家或地区代码","type":"selects","val":"US","items":[{"key":"AUTO","label":"🇺🇳自动(跟随地区检测结果)"},{"key":"CN","label":"🇨🇳中国大陆"},{"key":"HK","label":"🇭🇰香港"},{"key":"TW","label":"🇹🇼台湾"},{"key":"SG","label":"🇸🇬新加坡"},{"key":"US","label":"🇺🇸美国"},{"key":"JP","label":"🇯🇵日本"},{"key":"AU","label":"🇦🇺澳大利亚"},{"key":"GB","label":"🇬🇧英国"},{"key":"KR","label":"🇰🇷韩国"},{"key":"CA","label":"🇨🇦加拿大"},{"key":"IE","label":"🇮🇪爱尔兰"}],"desc":"不同国家或地区提供的内容或有差别。"},{"id":"@iRingo.TestFlight.Settings.MultiAccount","name":"启用多账号支持","type":"boolean","val":false,"desc":"启用多账号支持会自动保存保存更新当前账号信息。"},{"id":"@iRingo.TestFlight.Settings.Universal","name":"启用通用应用支持","type":"boolean","val":true,"desc":"启用通用应用支持会解除 TestFlight app 的 iOS/iPadOS/macOS(AppleSilicon) 平台限制。"},{"id":"@iRingo.TestFlight.Settings.AlwaysShowInstall","name":"总是显示安装选项","type":"boolean","val":false,"desc":"当 Testflight 无法加入时,也总是显示app详情页面的安装选项。"},{"id":"@iRingo.TestFlight.Settings.MergeNotifications","name":"合并通知开关","type":"boolean","val":false,"desc":"同步开关全平台的电子邮件通知,如关闭 iOS 的 Testflight 更新电子邮件通知,也会同时关闭 tvOS 的 Testflight 更新电子邮件通知。"},{"id":"@iRingo.TestFlight.Settings.LogLevel","name":"[调试] 日志等级","type":"selects","val":"WARN","items":[{"key":"OFF","label":"🔴 关闭"},{"key":"ERROR","label":"❌ 错误"},{"key":"WARN","label":"⚠️ 警告"},{"key":"INFO","label":"ℹ️ 信息"},{"key":"DEBUG","label":"🅱️ 调试"},{"key":"ALL","label":"全部"}],"desc":"选择脚本日志的输出等级,低于所选等级的日志将全部输出。"}] -------------------------------------------------------------------------------- /template/loon.handlebars: -------------------------------------------------------------------------------- 1 | #!name = {{@package 'displayName'}} 2 | #!desc = {{inline (@package 'description')}} 3 | #!openUrl = {{@package 'openUrl'}} 4 | #!author = {{@package 'contributors'}} 5 | #!homepage = {{@package 'homepage'}} 6 | #!icon = {{@package 'icon'}} 7 | #!tag = {{@package 'organizationName'}} 8 | #!system = {{@package 'system'}} 9 | #!date = {{now "yyyy-MM-dd HH:mm:ss"}} 10 | #!version = {{@package 'version'}} 11 | 12 | [Argument] 13 | {{{arguments}}} 14 | 15 | [General] 16 | skip-proxy = iosapps.itunes.apple.com 17 | 18 | [Script] 19 | http-request ^https?:\/\/testflight\.apple\.com\/ script-path=https://github.com/NSRingo/TestFlight/releases/download/v{{@package 'version'}}/request.bundle.js, requires-body=true, tag=✈ TestFlight.request, argument={{{scriptParams}}} 20 | http-response ^https?:\/\/testflight\.apple\.com\/ script-path=https://github.com/NSRingo/TestFlight/releases/download/v{{@package 'version'}}/response.bundle.js, requires-body=true, tag=✈ TestFlight.response, argument={{{scriptParams}}} 21 | 22 | [MITM] 23 | hostname = testflight.apple.com 24 | -------------------------------------------------------------------------------- /template/quantumultx.handlebars: -------------------------------------------------------------------------------- 1 | #!name = {{@package 'displayName'}} 2 | #!desc = {{inline (@package 'description')}} 3 | #!openUrl = {{@package 'openUrl'}} 4 | #!author = {{@package 'contributors'}} 5 | #!homepage = {{@package 'homepage'}} 6 | #!icon = {{@package 'icon'}} 7 | #!category = {{@package 'organizationName'}} 8 | #!date = {{now "yyyy-MM-dd HH:mm:ss"}} 9 | #!version = {{@package 'version'}} 10 | 11 | #[rewrite_local] 12 | # ✈ TestFlight 13 | ^https?:\/\/testflight\.apple\.com\/ url script-request-body https://github.com/NSRingo/TestFlight/releases/download/v{{@package 'version'}}/request.bundle.js 14 | ^https?:\/\/testflight\.apple\.com\/ url script-response-body https://github.com/NSRingo/TestFlight/releases/download/v{{@package 'version'}}/response.bundle.js 15 | 16 | #[mitm] 17 | hostname = testflight.apple.com 18 | -------------------------------------------------------------------------------- /template/shadowrocket.handlebars: -------------------------------------------------------------------------------- 1 | #!name = {{@package 'displayName'}} 2 | #!desc = {{inline (@package 'description')}} 3 | #!openUrl = {{@package 'openUrl'}} 4 | #!author = {{@package 'contributors'}} 5 | #!homepage = {{@package 'homepage'}} 6 | #!icon = {{@package 'icon'}} 7 | #!category = {{@package 'organizationName'}} 8 | #!date = {{now "yyyy-MM-dd HH:mm:ss"}} 9 | #!version = {{@package 'version'}} 10 | 11 | [General] 12 | skip-proxy = %APPEND% iosapps.itunes.apple.com 13 | 14 | [Script] 15 | ✈ TestFlight.request = type=http-request, pattern=^https?:\/\/testflight\.apple\.com\/, requires-body=1, script-path=https://github.com/NSRingo/TestFlight/releases/download/v{{@package 'version'}}/request.bundle.js, argument= 16 | ✈ TestFlight.response = type=http-response, pattern=^https?:\/\/testflight\.apple\.com\/, requires-body=1, script-path=https://github.com/NSRingo/TestFlight/releases/download/v{{@package 'version'}}/response.bundle.js, argument= 17 | 18 | [MITM] 19 | hostname = %APPEND% testflight.apple.com 20 | -------------------------------------------------------------------------------- /template/stash.handlebars: -------------------------------------------------------------------------------- 1 | name: "{{@package 'displayName'}}" 2 | desc: |- 3 | {{#each (split (@package 'description') "\n")}} 4 | {{{this}}} 5 | {{/each}} 6 | openUrl: "{{@package 'openUrl'}}" 7 | author: |- 8 | {{#each (@package 'contributors')}} 9 | {{{this}}} 10 | {{/each}} 11 | homepage: "{{@package 'homepage'}}" 12 | icon: "{{@package 'icon'}}" 13 | category: "{{@package 'organizationName'}}" 14 | date: "{{now "yyyy-MM-dd HH:mm:ss"}}" 15 | version: "{{@package 'version'}}" 16 | 17 | http: 18 | mitm: 19 | - "testflight.apple.com" 20 | script: 21 | - match: ^https?:\/\/testflight\.apple\.com\/ 22 | name: ✈ TestFlight.request 23 | type: request 24 | require-body: true 25 | argument: 26 | - match: ^https?:\/\/testflight\.apple\.com\/ 27 | name: ✈ TestFlight.response 28 | type: response 29 | require-body: true 30 | argument: 31 | 32 | script-providers: 33 | ✈ TestFlight.request: 34 | url: https://github.com/NSRingo/TestFlight/releases/download/v{{@package 'version'}}/request.bundle.js 35 | interval: 86400 36 | ✈ TestFlight.response: 37 | url: https://github.com/NSRingo/TestFlight/releases/download/v{{@package 'version'}}/response.bundle.js 38 | interval: 86400 39 | -------------------------------------------------------------------------------- /template/surge.handlebars: -------------------------------------------------------------------------------- 1 | #!name = {{@package 'displayName'}} 2 | #!desc = {{inline (@package 'description')}} 3 | #!openUrl = {{@package 'openUrl'}} 4 | #!author = {{@package 'contributors'}} 5 | #!homepage = {{@package 'homepage'}} 6 | #!icon = {{@package 'icon'}} 7 | #!category = {{@package 'organizationName'}} 8 | #!date = {{now "yyyy-MM-dd HH:mm:ss"}} 9 | #!version = {{@package 'version'}} 10 | #!arguments = {{{arguments}}} 11 | #!arguments-desc = {{{argumentsDesc}}} 12 | 13 | [General] 14 | skip-proxy = %APPEND% iosapps.itunes.apple.com 15 | 16 | [Script] 17 | ✈ TestFlight.request = type=http-request, pattern=^https?:\/\/testflight\.apple\.com\/, requires-body=1, script-path=https://github.com/NSRingo/TestFlight/releases/download/v{{@package 'version'}}/request.bundle.js, argument={{{scriptParams}}} 18 | ✈ TestFlight.response = type=http-response, pattern=^https?:\/\/testflight\.apple\.com\/, requires-body=1, script-path=https://github.com/NSRingo/TestFlight/releases/download/v{{@package 'version'}}/response.bundle.js, argument={{{scriptParams}}} 19 | 20 | [MITM] 21 | hostname = %APPEND% testflight.apple.com 22 | --------------------------------------------------------------------------------