diff --git a/script/boxjs.json b/script/boxjs.json index 7c48bd2cb..a87933adc 100644 --- a/script/boxjs.json +++ b/script/boxjs.json @@ -130,9 +130,7 @@ { "id": "blackmatrix7.bark", "name": "Bark推送", - "keys": [ - "magic_bark_url" - ], + "keys": ["magic_bark_url"], "settings": [ { "id": "magic_bark_url", @@ -330,10 +328,7 @@ { "id": "blackmatrix7.luka", "name": "Luka阅读养成", - "keys": [ - "luka_checkin_cookie", - "luka_signin_auth" - ], + "keys": ["luka_checkin_cookie", "luka_signin_auth"], "author": "@blackmatrix7", "repo": "https://github.com/blackmatrix7/ios_rule_script/tree/master/script/luka", "icons": [ @@ -393,7 +388,7 @@ { "id": "smzdm_signin", "name": "执行每日签到", - "val": true, + "val": false, "type": "boolean", "desc": "" }, @@ -416,10 +411,7 @@ { "id": "blackmatrix7.tieba", "name": "百度贴吧", - "keys": [ - "tieba_signin_cookie", - "tieba_sync_qinglong" - ], + "keys": ["tieba_signin_cookie", "tieba_sync_qinglong"], "author": "@blackmatrix7", "repo": "https://github.com/blackmatrix7/ios_rule_script/tree/master/script/tieba", "icons": [ @@ -449,12 +441,7 @@ { "id": "blackmatrix7.synology", "name": "Synology", - "keys": [ - "syno_https_url", - "syno_account", - "syno_passwd", - "syno_sid" - ], + "keys": ["syno_https_url", "syno_account", "syno_passwd", "syno_sid"], "settings": [ { "id": "syno_https_url", @@ -548,6 +535,122 @@ "https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/applestore/icon/applestore_dark.png", "https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/applestore/icon/applestore.png" ] + }, + { + "id": "blackmatrix7.testflight", + "name": "TestFlight", + "keys": [ + "tf_app_id", + "tf_join_mode", + "tf_loop_count", + "tf_session_info", + "tf_joined_app_id", + "tf_invalid_app_id", + "tf_check_account_id", + "tf_join_concurrency", + "tf_app_use_account_id", + "tf_check_session_time", + "tf_check_session_time_diff" + ], + "author": "@blackmatrix7", + "repo": "https://github.com/blackmatrix7/ios_rule_script/tree/master/script/testflight", + "settings": [ + { + "id": "tf_sync_qinglong", + "name": "同步数据到青龙面板", + "val": false, + "type": "boolean", + "desc": "将必要数据同步至青龙面板,以执行作业" + }, + { + "id": "tf_join_mode", + "name": "执行模式", + "val": "1", + "type": "radios", + "items": [ + { + "key": "0", + "label": "温和" + }, + { + "key": "1", + "label": "标准" + }, + { + "key": "2", + "label": "暴力" + } + ], + "desc": "推荐使用温和与标准模式,暴力模式风险极高" + }, + { + "id": "tf_app_id", + "name": "TestFlight App愿望清单", + "val": "", + "type": "input", + "placeholder": "", + "autoGrow": true, + "desc": "等待加入的TestFlight APP Id,多个APP Id以;分隔" + }, + { + "id": "tf_join_concurrency", + "name": "并发数量", + "val": 2, + "type": "number", + "placeholder": "", + "autoGrow": true, + "desc": "当App的Beta可加入时,每个账户并发多少个请求尝试加入" + }, + { + "id": "tf_loop_count", + "name": "每次运行循环次数", + "val": 1, + "type": "number", + "placeholder": "", + "autoGrow": true, + "desc": "每次运行脚本时循环多少次,每次循环间隔800毫秒。NodeJS(非青龙面板)可设置为0,表示持续运行" + }, + { + "id": "tf_check_session_time_diff", + "name": "检查Session有效性的间隔", + "val": 7200, + "type": "number", + "placeholder": "", + "autoGrow": true, + "desc": "每间隔多少秒检查一次账户的有效性,默认7200秒" + }, + { + "id": "tf_joined_app_id", + "name": "TestFlight每个账户已加入成功的AppId", + "val": "", + "type": "textarea", + "placeholder": "", + "autoGrow": true, + "desc": "多账户的TestFlight已加入的AppId,不建议手动修改" + }, + { + "id": "tf_session_info", + "name": "TestFlight Session数据", + "val": "", + "type": "textarea", + "placeholder": "", + "autoGrow": true, + "desc": "多账户的TestFlight数据,仅供查看,请勿手动修改以免引发异常" + }, + { + "id": "tf_check_account_id", + "name": "用于检查App Beta可否加入的AccountId", + "val": "", + "type": "input", + "placeholder": "", + "autoGrow": true, + "desc": "选择一个账号专门用于检测TF是否可以加入,以保护主号" + } + ], + "icons": [ + "https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/icon/testflight.png", + "https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/icon/testflight.png" + ] } ] -} \ No newline at end of file +} diff --git a/script/testflight/README.md b/script/testflight/README.md new file mode 100644 index 000000000..0ad2e54b8 --- /dev/null +++ b/script/testflight/README.md @@ -0,0 +1,284 @@ +# TestFlight + +## 前言 + +TestFlight多账户共存与自动加入脚本,主要满足个人需要两个脚本共存的需求。 + +**感谢NobyDa、DecoAri、chouchoui、lodepuly等大佬的源代码。** + +本脚本部分参考了上述大佬的脚本源代码。 + +相对于原版,做了以下改动: + +**TestFlight 自动加入** + +1. 支持NodeJS,含青龙面板 +2. 支持多账户 +3. 支持MITM Over HTTP/1.1 与 HTTP/2 +4. 支持控制加入TF的并发数量 +5. 支持Session失效检测 +6. 支持满员的TF自动加入任务 +7. 支持温和、标准和暴力三种加入模式 +8. 标准模式下,支持指定账号检测TF可否加入 + +**TestFlight 多账户共存** + +1. 支持多账户同步 +2. 去除共享功能 +3. 保留当前账户“以前测试过”的列表 + +## 部署 + +**Surge** + +```ini +https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.sgmodule +``` + +**Quantumult X** + +```ini +https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.snippet +``` + +**Loon** + +```ini +https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.lnplugin +``` + +**Stash** + +```ini +https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.stoverride +``` + +## 入门 + +安装对应的模块/重写/插件/覆写 + +### **TestFlight 多账户共存** + +在AppStore中切换不同的账号,每切换一个账号需要重新打开TestFlight,直至弹出获取必要信息成功的通知。 + +随后即可在TestFlight App中查看所有共存账号的App。 + +### **TestFlight 自动加入** + +在AppStore中登录需要加入TestFlight的账号,打开TestFlight,弹出获取必要信息成功的通知。 + +打开需要加入的TestFlight App链接,如果已满员会自动加入任务列表。 + +脚本主要运行平台为青龙面板,所以在默认的配置中不带有计划任务。需要根据运行平台不同,手动为脚本设置一个定时任务。 + +#### **Surge** + +```ini +[Script] +TestFlight_自动加入 = type=cron,cronexp=0/5 * * * * *,script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js +``` + +#### **Quantumult X** + +```ini +[task_local] +0/5 * * * * * https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js, tag=TestFlight_自动加入, enabled=true +``` + +#### **Loon** + +```ini +[Script] +cron "0/5 * * * * *" script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js,timeout=5,TestFlight_自动加入 +``` + +#### **Stash** + +```yaml +cron: + script: + - name: testflight.js + cron: '0/5 * * * * *' + argument: '' + timeout: 10 +``` + +#### **青龙面板** + +在Web界面中,选择“定时任务”,选择新增,定时规则为“*/5 * * * * *”,即5秒运行一次。 + +### **大功告成** + +如果你不想太过折腾,那么到这来配置就结束了,不需要再往下阅读。 + +## 进阶 + +### TestFlight 多账户共存 + +#### 多设备互相同步 + +脚本支持通过青龙面板,将多账户共存的信息在不同设备、不同客户端间互相同步。 + +举个例子,你有几台iPhone、iPad,它们使用不同的客户端,但是都使用同个脚本,连接同个青龙面板。那么当开启“同步数据到青龙面板”时,TestFlight的账户数据会在多个设备间互相同步。 + +更具体地说,你在iPhone上,登录A、B两个账号;在iPad上,登录C、D两个账号。开启同步后,你的iPhone、iPad和青龙面板将同时存储A、B、C、D四个账号的信息。这样无论你打开哪台设备的TestFlight的App,它们的数据是完全一致的。 + +当然,你也可以一台iPhone登录国区账号,一台iPhone登录外区账号,两台间互相同步。 + +**特别说明** + +1. 数据同步可能会影响TestFlight的加载速度,建议只在必要时开启。 +2. 如果多设备间数据冲突时,以青龙面板中存储的数据为准。 +3. 如果需要删除某个账号,需要在青龙面板的magic.json文件中删除,在iOS设备中删除无效,下次还会从青龙面板同步回来。 + +### TestFlight 自动加入 + +#### 手动编辑需要加入的AppId + +通过`tf_app_id`变量来存储需要加入的AppId,以;分隔。你可以在BoxJS,或magic.json文件中编辑它。 + +**需要注意原版是以,分隔,此处配置不同。** + +#### 同步至青龙面板 + +https://github.com/blackmatrix7/ios_rule_script/tree/master/script#readme + +根据上述文档,配置青龙面板,然后在BoxJS中打开同步至青龙面板的开关。配置完成后,每次打开TestFlight,登录信息变化时都会同步到青龙面板中。同时,在打开已满员的Beta链接时,也会将App ID同步至青龙面板。 + +**特别说明** + +1. TestFlight中的App ID与青龙面板的同步是单向的,只能从设备同步到青龙面板。 +2. 青龙面板最终存储的App ID为所有设备同步数据的合集。 + +#### 并发数量 + +指当App的Beta为可加入状态时,**每个账户**并发多少个请求来尝试加入。默认为2个,并发数量并不是越高越好。 + +#### 三种模式的区别 + +#### 温和 + +查询App的Beta状态时,不携带用户信息,仅在App可加入时才会使用存储的用户信息发起申请,理论上可以极大减少AppleID被Ban的几率。缺点为ResponeBody较大,查询的速度相较标准模式可能会稍慢。 + +**此模式未经验证。** + +#### 标准 + +默认采用标准模式。 + +同原版一致的逻辑,查询App的Beta状态时,会带入一个用户的信息进行查询。在App可加入时,使用存储的用户信息发起申请。缺点是每次查询都会携带用户信息,理论上有导致AppleID被Ban的风险,不过目前并无先例。 + +**你可以指定一个小号专门用于检测可用性,具体见后文。** + +#### 暴力 + +不查询App的Beta状态,直接使用存储的用户信息申请加入,如果无法加入会引发409的异常。优点是没有查询过程,抢App的Beta测试员成功率可能会更高。缺点是会持续不断的产生异常的请求,理论上被Ban的风险最大。 + +**此模式未经过验证。慎选,建议使用新注册的账号试验。** + +#### 多账户自动加入 + +可以通过在iOS的设置中,切换AppStore账户,再打开TestFlight App来实现多账户加入。 + +多账户并不能提高加入的成功率,相反因为需要尝试加入的账户数量变多,反而会降低成功率。 + +多账户存储的数据格式如下,其中类似"xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"为AccountId,非常重要,务必记牢。 + +```json +"tf_session_info": { + "magic_session": true, + "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": { + "X-Session-Id": "xxxxxxxxxxxxxxxx", + "X-Session-Digest": "xxxxxxxxxxxxxx", + "X-Request-Id": "xxxxxxxxxxxxx" + }, + "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxoo": { + "X-Session-Id": "xxxxxxxxxxxxx", + "X-Session-Digest": "xxxxxxxxxxxxxx", + "X-Request-Id": "xxxxxxxxxxxxx" + } +} +``` + +**不同账户加入不同的App Beta测试员** + +脚本通过`tf_joined_app_id`存储的数据来判断哪些账户需要加入哪些AppID。 + +tf_joined_app_id的数据格式如下: + +```json +"tf_joined_app_id": { + "magic_session": true, + "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": [ + "*", + "PTbaSXrf", + "J3awzdqd", + "aL5ZWE8A", + "iSTXkF4K" + ], + "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxoo": [ + "*" + ] +} +``` + +magic_session 不可修改和删除,xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx为你的账户AccountId,对应的数组则是已经加入成功的AppId。 + +假设不想让AccountId为xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx的账户加入PTbaSXrf的测试,在对应的数组中写入AppId即可。此时如果存在其他账户,则其他账户依旧会尝试加入PTbaSXrf的测试,直至成功。 + +如果某个账户,不希望它加入任何TF,则给它加一个"*",表示任何APP的测试都不加入。 + +#### 专用账户检查TF + +在标准模式下,可以指定一个账号专门用于检查App的TestFlight是否可加入。在BoxJS或magic.json中,为`tf_check_account_id`这个变量分配一个account id,将这个账户用来检查App的Beta成员是否已满。只有当Beta可加入时,才会用真正的账户来尝试加入,以达到保护主账号的目的。 + +举个例子,你可以注册一个新的账号,专门用于检查TF可否加入,当可以加入时,再用你的主账号尝试加入。 + +需要注意的是以下几点: + +1. 专门用于检查的账号也需要获取Session,同时这个账号也会尝试加入TF,除非你根据上一条的`tf_joined_app_id`配置,排除掉。 +2. 不用于检查的主账号,最好隔段时间就刷新一下Session,以免会话过期。 + +#### 连续运行 + +配置`tf_loop_count`来设置脚本每次启动时,执行多少次任务再退出,每次任务间隔固定800毫秒。通过配置连续运行次数,可以减少青龙面板的脚本执行频次,避免因为日志过多导致青龙面板没有响应。 + +如果是NodeJS环境(非青龙面板),可以设置为0,脚本将持续运行不会终止。 + +不要在手机上将`tf_loop_count`设置为0,否则脚本会一直运行直到超时,或被手动中止。 + +#### Session检测 + +因为脚本的主要运行环境是青龙面板,所以可能存在Session因为长期未更新导致失效的情况。为此加入每2小时检测一次Session有效期的功能,如果失效会推送通知到手机,需要再手动获取信息,并同步到青龙面板。 + +检测间隔可以通过变量`tf_check_session_time_diff`配置,单位秒,不要配置过短,以免影响脚本正常功能。 + +## 其他 + +如果有问题欢迎反馈。 + +### Loon暂不支持MITM Over HTTP/2 + +已经和作者反馈,需要等待Loon更新。 + +### Shadowrocket未经验证 + +Shadowrocket未经验证,不确保能够正常使用 + +## 脚本变量 + +当前脚本使用的变量,你可以根据这些Key,在magic.json中配置数据。 + +| 名称 | 类型 | 默认值 | 说明 | +| -------------------------- | ------ | ------ | ------------------------------------------------------------ | +| tf_session_info | Json | 无 | 由脚本自动获取的Session信息,如需修改请整个删除重新获取 | +| tf_app_id | String | 无 | 需要加入Beta的AppId,多个以;分隔 | +| tf_join_mode | Int | 1 | 执行默认 0 温和 1 标准 2 暴力 | +| tf_join_concurrency | Int | 2 | 每个账户并发加入TestFlight Beta测试员的请求数 | +| tf_joined_app_id | Json | 无 | 存储每个账户已经成功加入的AppId | +| tf_check_account_id | String | 无 | 专门用于检查TF可用性的Id,如果为空则从已登录的账户中随机选择 | +| tf_loop_count | Int | 1 | 每次运行脚本时循环多少次,每次循环间隔800毫秒,NodeJS(非青龙面板)可设置为0,表示持续运行 | +| tf_check_session_time_diff | Int | 7200 | 每间隔多少秒检查一次账户的有效性,默认7200秒 | +| tf_check_session_time | Int | 无 | 上次检查账户有效期的时间,自动生成,不需要配置 | +| tf_invalid_app_id | Json | 无 | 验证无效的AppId,自动生成,不需要配置 | + diff --git a/script/testflight/icon/testflight.png b/script/testflight/icon/testflight.png new file mode 100644 index 000000000..5cd9572cb Binary files /dev/null and b/script/testflight/icon/testflight.png differ diff --git a/script/testflight/testflight.js b/script/testflight/testflight.js new file mode 100644 index 000000000..682ea6c24 --- /dev/null +++ b/script/testflight/testflight.js @@ -0,0 +1,707 @@ +const scriptName = "TestFlight"; +const tfAppIdKey = "tf_app_id"; +const tfJoinedAppIdKey = "tf_joined_app_id"; +const tfInvalidAppIdKey = "tf_invalid_app_id"; +const tfSessionInfoKey = "tf_session_info"; +const tfCheckSessionTimeKey = "tf_check_session_time"; +const tfCheckSessionTimeDiffKey = "tf_check_session_time_diff"; +const tfAppUseAccountIdKey = "tf_app_use_account_id"; +const getSessionRegex = /^https:\/\/testflight\.apple\.com\/v3\/accounts\/(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})\/apps$/; +const getFullAppIdRegex = /^https:\/\/testflight\.apple\.com\/v3\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/ru\/([a-zA-Z0-9]{8})$/; +const modifyTFRequest = /^https:\/\/testflight\.apple\.com\/v\d\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/apps\/\d+/ +const modifySettingsTFRequest = /^https:\/\/testflight\.apple\.com\/v\d\/accounts\/settings\/.*\/apps\/\d+/ +const modifyInstallTFRequest = /^https:\/\/testflight\.apple\.com\/v\d\/apps\/\d+\/\d+\/install\/status$/ +const $ = MagicJS(scriptName, "INFO"); +const blankSessionInfo = { + "x-session-id": "", "x-session-digest": "", "x-request-id": "", "valid": false +}; +// 加入TestFlight的模式,0: 温和 1: 标准 2: 暴力 +const tfJoinMode = parseInt($.data.read("tf_join_mode", 1)); +// 尝试加入TF时并发请求数量 +const tfJoinConcurrency = parseInt($.data.read("tf_join_concurrency", 2)); +// 每次执行循环次数 +const tfLoopCount = parseInt($.data.read("tf_loop_count", 5)); +// 用于检测TF可用性的专用账号 +let tfCheckAccountId = $.data.read('tf_check_account_id', ''); +// 同步数据到青龙面板 +const syncQingLong = $.data.read("tf_sync_qinglong", false); + + +function removeHeaders(config) { + let headers = {...config["headers"]}; + delete headers["valid"]; + delete headers["if-none-match"]; + delete headers["If-None-Match"]; + config["headers"] = headers; + return config; +} + +$.http.interceptors.request.use(removeHeaders); + +function modifyHeaders(accountId, headers, session, url = "") { + let newHeaders = {...headers, ...session}; + delete newHeaders["valid"]; + if ($.env.isQuanX || $.env.isStash) { + newHeaders = $.http.convertHeadersToCamelCase(newHeaders); + // delete newHeaders["Content-Length"]; + delete headers["If-None-Match"]; + } else { + newHeaders = $.http.convertHeadersToLowerCase(newHeaders); + // delete newHeaders["content-length"]; + delete headers["if-none-match"]; + } + // if ("x-apple-store-front" in newHeaders) { + // newHeaders["x-apple-store-front"] = "143441-19,29"; + // } + // if ($.env.isLoon && /accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/.test(url)) { + // newHeaders[":path"] = url.replace("https://testflight.apple.com", "").replace(/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/, `accounts/${accountId}`); + // $.logger.debug(`修改请求头:path\n${JSON.stringify(newHeaders)}`); + // } + $.logger.info(`修改请求头\n${JSON.stringify(newHeaders)}`); + return newHeaders; +} + +function modifyRequest() { + $.logger.info(`请求地址\n${$.request.url}`); + let url = $.request.url; + let headers = $.request.headers; + let body = $.request.body ? JSON.parse($.request.body) : {}; + const result = /apps\/(\d+)/.exec($.request.url); + if (result) { + const appAdamId = result[1]; + const appUseAccountId = $.data.read(tfAppUseAccountIdKey, {}); + if (appAdamId in appUseAccountId) { + const accountId = appUseAccountId[appAdamId]; + const session = $.data.read(tfSessionInfoKey, blankSessionInfo, accountId); + url = url.replace(/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/, `accounts/${accountId}`); + $.logger.info(`修改请求地址为\n${url}`); + headers = modifyHeaders(accountId, headers, session, url); + } + } + if (/\/install$/.test($.request.url) && Object.keys(body).length > 0) { + body.storefrontId = "143441-19,29"; + } + return [url, headers, JSON.stringify(body)]; +} + +function getAccountAppData(currentAccountId, accountId, headers) { + return new Promise(resolve => { + $.http.get({ + url: `https://testflight.apple.com/v3/accounts/${accountId}/apps`, headers: headers + }).then(resp => { + let obj = resp.body; + obj["account_id"] = accountId; + // 当前账户保留"以前测试过的"App数据 + if (currentAccountId !== accountId) { + obj["data"] = obj["data"].filter(app => app["previouslyTested"] === false); + } + $.logger.info(`账户 ${accountId} 获取到 ${obj["data"].length} 个App`); + resolve(obj); + }).catch(async err => { + if (err.response.status === 401) { + $.notification.post(`AccountId ${accountId} 的 Session 已过期`); + let sessionInfo = $.data.read(tfSessionInfoKey, blankSessionInfo, accountId); + sessionInfo["valid"] = false; + $.data.write(tfSessionInfoKey, sessionInfo, accountId); + if (syncQingLong === true) { + let qlSessionInfo = await $.qinglong.get(tfSessionInfoKey, {}, accountId); + if (qlSessionInfo["x-request-id"] === sessionInfo["x-request-id"] && qlSessionInfo["valid"] === true) { + await $.qinglong.write(tfSessionInfoKey, sessionInfo, accountId); + $.notification.post(`已同步将青龙面板中AccountId ${accountId}设置为失效`); + } + } + resolve({data: [], account_id: accountId}); + } else { + $.logger.error(`获取 AccountId ${accountId} 的App列表出现异常\n${JSON.stringify(err)}`); + resolve({data: [], account_id: accountId}); + } + }) + }) +} + +async function getAllAppData() { + try { + const currentAccountId = $.request.url.match(getSessionRegex)[1]; + $.logger.info(`当前账户为:${currentAccountId}`); + let obj = $.response.status === 200 ? JSON.parse($.response.body) : {"data": [], "error": null}; + $.logger.debug(`请求到的App列表为:\n${JSON.stringify(obj)}`); + let allSessions = $.data.read(tfSessionInfoKey, null, "", true); + let accountIdList = Object.keys(allSessions); + // 确保当前账户排在数组中的第一位,以保证优先使用当前账户的App数据 + if ($.response.status === 304) { + accountIdList = accountIdList.filter(accountId => accountId !== currentAccountId); + accountIdList.unshift(currentAccountId); + } + const promises = accountIdList.filter(accountId => { + return accountId !== 'magic_session' && ($.response.status !== 200 || accountId !== currentAccountId) && allSessions[accountId]['valid'] === true + }).map(accountId => { + let session = allSessions[accountId]; + return getAccountAppData(currentAccountId, accountId, modifyHeaders(accountId, $.request.headers, session)); + }); + + $.logger.info(`共有 ${promises.length} 个账户需要获取App列表`); + + await Promise.all(promises) + .then((results) => { + let appAccountMap = new Map(); + for (let result of results) { + if (result !== undefined && typeof result["data"] !== undefined) { + for (let appData of result["data"]) { + const appAdamId = appData["appAdamId"].toString(); + if (appAccountMap.has(appAdamId) === false) { + appAccountMap.set(appAdamId, result["account_id"]); + obj["data"].push(appData); + } + } + } else { + $.logger.warning(`获取到的App数据为undefined`); + } + } + $.data.write(tfAppUseAccountIdKey, Object.fromEntries(appAccountMap)); + }) + .catch(error => { + // 处理错误 + $.logger.error(`获取App数据出现异常\n${JSON.stringify(error)}`); + }); + $.logger.debug(`全部有效账户的App数据\n${JSON.stringify(obj)}`); + return {body: JSON.stringify(obj)}; + } catch (err) { + $.logger.error(`获取全部有效账户的App数据出现异常\n${err}`); + return {body: $.response.body}; + } +} + +async function syncToQingLong(currentAccountId = "", currentSessionInfo = null) { + + if (syncQingLong === true) { + + let silentSync = true; // 静默同步,本次同步不弹出通知 + let [qlAllSessions, qlAppIds] = await $.qinglong.batchRead([tfSessionInfoKey, {}, "", true], [tfAppIdKey, ""]); + let batchWriteData = []; + + // 同步本地的Session至青龙面板 + if (currentAccountId !== "" && currentSessionInfo !== null) { + try { + const qlSessionInfo = qlAllSessions[currentAccountId]; + $.logger.info(`青龙面板中旧的SessionInfo\n${JSON.stringify(qlSessionInfo)}`); + if (qlSessionInfo["x-session-id"] !== currentSessionInfo["x-session-id"] || qlSessionInfo["valid"] === false) { + batchWriteData.push([tfSessionInfoKey, currentSessionInfo, currentAccountId]); + } + if (qlSessionInfo["valid"] === false) { + silentSync = false; + } + } catch + (error) { + $.logger.error(`同步当前Session至青龙面板出现异常\n${error}`); + } + } + + // 同步AppId至青龙面板 + try { + let tfAppIds = $.data.read(tfAppIdKey, "").split(";"); + qlAppIds = qlAppIds.split(";").filter(appId => appId !== ""); + // 判断是否有在青龙面板上不存在的AppId + let newAppIds = tfAppIds.filter(x => !qlAppIds.includes(x)); + if (newAppIds.length > 0) { + qlAppIds = qlAppIds.concat(tfAppIds); + qlAppIds = [...new Set(qlAppIds)]; + $.logger.info(`本次需要更新到青龙面板的AppId\n${qlAppIds.join(";")}`); + let strAppIds = ""; + if (qlAppIds.length === 1) { + strAppIds = qlAppIds[0]; + } else if (qlAppIds.length > 1) { + strAppIds = qlAppIds.join(";") + } + batchWriteData.push([tfAppIdKey, strAppIds]); + silentSync = false; + } else { + $.logger.info(`所有的AppId都已经存在于青龙面板中,本次同步不进行同步!`); + } + } catch (error) { + $.logger.error(`同步AppId至青龙面板出现异常\n${error}`); + } + + // 同步青龙面板的数据至本地 + try { + $.logger.debug(`青龙面板中所有的SessionInfo\n${JSON.stringify(qlAllSessions)}`); + // 遍历青龙面板存储的Session + for (let accountId of Object.keys(qlAllSessions)) { + // 与本地的比较,如果不一致则更新本地的Session + if (accountId !== "magic_session" && qlAllSessions[accountId]["valid"] === true && accountId !== currentAccountId) { + const localSession = $.data.read(tfSessionInfoKey, {}, accountId); + const qlSession = qlAllSessions[accountId]; + if (localSession["x-request-id"] !== qlSession["x-request-id"] || localSession["valid"] === false) { + $.data.write(tfSessionInfoKey, qlSession, accountId); + $.logger.info(`将青龙面板 ${accountId} 的 Session 同步到本地\n`); + } + } + } + } catch (error) { + $.logger.error(`同步青龙面板的Session至本地出现异常\n${JSON.stringify(error)}`); + } + + // 批量写入数据 + const result = await $.qinglong.batchWrite(...batchWriteData); + if (result === true) { + if (silentSync === false) { + $.notification.post( + `${scriptName} ${currentAccountId}`, + "", + `已将您的信息同步至青龙面板:\n${$.qinglong.url}\n如上述地址不是您所配置,则信息已泄露!\n请立即停用脚本,更改密码!\n检查青龙面板配置是否被篡改!`); + } else { + $.logger.info(`青龙面板已经存在相同信息,故本次同步不弹出通知!\n已将您的信息同步至青龙面板:\n${$.qinglong.url}\n如上述地址不是您所配置,则信息已泄露!\n请立即停用脚本,更改密码!\n检查青龙面板配置是否被篡改!`); + } + } else { + $.notification.post(`将您的信息同步至青龙面板失败:\n${$.qinglong.url}\n请检查青龙面板配置!`); + } + } +} + +async function getTFSessionInfo() { + try { + const matches = $.request.url.match(/\/accounts\/([a-zA-Z0-9-]+)\/apps/); + if (matches && matches.length > 1) { + // 账户Id + const accountId = matches[1]; + // 获取旧的TestFlight Session数据 + const oldSessionInfo = $.data.read(tfSessionInfoKey, blankSessionInfo, accountId); + $.logger.info(`旧的SessionInfo:\n${JSON.stringify(oldSessionInfo)}`); + // 获取新的TestFlight Session数据 + $.logger.info(`当前的Headers:\n${JSON.stringify($.request.headers)}`); + const newSessionInfo = { + "valid": true, + "x-session-id": $.request.headers["x-session-id"] || $.request.headers["X-Session-Id"], + "x-session-digest": $.request.headers["x-session-digest"] || $.request.headers["X-Session-Digest"], + "x-request-id": $.request.headers["x-request-id"] || $.request.headers["X-Request-Id"], + }; + $.logger.info(`新的SessionInfo\n${JSON.stringify(newSessionInfo)}`); + // Session数据不同时写入 + if (oldSessionInfo["x-session-id"] !== newSessionInfo["x-session-id"]) { + $.data.write(tfSessionInfoKey, newSessionInfo, accountId); + const tempSessionInfo = $.data.read(tfSessionInfoKey, blankSessionInfo, accountId); + if (newSessionInfo["x-session-id"] === tempSessionInfo["x-session-id"]) { + $.notification.post(`${scriptName} - ${accountId}`, "", "写入TestFlight必要信息成功"); + } else { + $.notification.post(`${scriptName} - ${accountId}`, "", "写入TestFlight必要信息失败"); + } + } + // 同步数据至青龙面板 + await syncToQingLong(accountId, newSessionInfo); + } else { + $.logger.error(`无法获取TestFlight的AccountId,请检查配置。`); + } + } catch (err) { + const errMsg = `获取TestFlight必要信息出现异常`; + $.notification.post(errMsg); + $.logger.error(`${errMsg}\n${err}`); + } +} + +async function autoAddAppId() { + let obj = JSON.parse($.response.body); + const match = $.request.url.match(getFullAppIdRegex); + const appId = match[1]; + let tfAppIds = $.data.read(tfAppIdKey, "").split(";"); + tfAppIds = tfAppIds.filter(appId => { + return appId !== "" + }); + $.logger.info(`已存在的TF愿望清单App:${JSON.stringify(tfAppIds)}`); + if (obj.data.status === "FULL") { + if (!tfAppIds.includes(appId)) { + tfAppIds.push(appId); + let strAppIds = ""; + if (tfAppIds.length === 1) { + strAppIds = appId; + } else if (tfAppIds.length > 1) { + strAppIds = tfAppIds.join(";") + } + $.data.write(tfAppIdKey, strAppIds); + const msg = `应用 [${appId}] 已满员,自动加入TestFlight愿望清单`; + $.logger.info(msg); + $.notification.post(msg) + } else { + $.logger.info(`应用 [${appId}] 已存在于TestFlight愿望清单,本次不会重复加入`); + } + await syncToQingLong(); + } else { + $.logger.info(`应用 [${appId}] 未满员或不开放,请自行加入`); + } +} + +function getAppTestFlightHtml(appId) { + return new Promise((resolve) => { + const url = `https://testflight.apple.com/join/${appId}`; + $.http + .get({url, timeout: 5000}) + .then((resp) => { + if (resp.status === 200 && (resp.body.indexOf("此 Beta 版本的测试员已满") > 0 || resp.body.indexOf("This beta is full") > 0)) { + $.logger.info(`AppId: [${appId}] 已满员`); + resolve("FULL"); + } else { + $.logger.info(`AppId: [${appId}] 未满员`); + $.logger.info(`当前的Response:\n${resp.body}`); + resolve("NOT_FULL"); + } + }) + .catch((err) => { + if (err.response.status === 404) { + $.logger.warning(`AppId: [${appId}] 不存在`); + resolve("NO_EXIST"); + } else { + $.logger.warning(`获取TestFlight信息出现异常\n${JSON.stringify(err)}`); + resolve("ERROR"); + } + }); + }); +} + +function getAppTestFlightJson(accountId, sessionInfo, appId) { + return new Promise((resolve) => { + const url = `https://testflight.apple.com/v3/accounts/${accountId}/ru/${appId}`; + $.http + .get({ + url, headers: {...sessionInfo}, timeout: 5000 + }) + .then((resp) => { + if (resp.status === 200) { + if (resp.body.data.status === "FULL") { + $.logger.info(`应用 [${resp.body.data.app.name}] 已满员`); + resolve([false, resp.status]); + } else if (resp.body.data.status === "OPEN") { + $.logger.info(`应用 [${resp.body.data.app.name}] 未满员`); + resolve([true, resp.status]); + } else { + $.logger.info(`应用 [${resp.body.data.app.name}] 状态未知:${resp.body.data.status}`); + resolve([false, resp.status]); + } + } else { + $.logger.warning(`获取App Beta测试状态出现错误`); + resolve([false, resp.status]); + } + }) + .catch((err) => { + if (err.response.status === 404) { + $.logger.warning(`应用${appId}不存在`); + let invalidAppIds = $.data.read(tfInvalidAppIdKey, []); + invalidAppIds.push(appId); + $.data.write(tfInvalidAppIdKey, invalidAppIds); + resolve([false, err.response.status]); + } else if (err.response.status === 401) { + $.logger.warning(`获取App Beta测试状态出现未授权,请更新Session`); + resolve([false, err.response.status]); + } else if (err.response && err.response.status) { + $.logger.warning(`获取App Beta测试状态出现错误:\n${JSON.stringify(err)},http code: ${err.response.status}`); + resolve([false, err.response.status]); + } else { + $.logger.warning(`获取App Beta测试状态出现错误:\n${err}`); + resolve([false, undefined]); + } + }); + }); +} + +function joinTestFlight(accountId, sessionInfo, appId) { + return new Promise((resolve) => { + const url = `https://testflight.apple.com/v3/accounts/${accountId}/ru/${appId}/accept`; + $.http + .post({ + url, headers: sessionInfo, + }) + .then((resp) => { + if (resp.status === 200) { + // 存储已加入的AppId + let tfJoinedAppIds = $.data.read(tfJoinedAppIdKey, [], accountId); + if (!tfJoinedAppIds.includes(appId)) { + tfJoinedAppIds.push(appId); + tfJoinedAppIds = [...new Set(tfJoinedAppIds)]; + $.data.write(tfJoinedAppIdKey, tfJoinedAppIds, accountId); + } + $.logger.info(`成功加入[${resp.body.data.name}]`); + resolve([true, appId, resp.body.data.name, accountId]); + } else { + $.logger.info(`加入[${appId}]失败`); + $.notification.post(`加入[${appId}]失败`); + resolve([false, appId, resp.body.data.name, accountId]); + } + }) + .catch((err) => { + if (err && err.response && err.response.status === 409) { + $.logger.warning(`强制加入应用${appId}失败`); + } + $.logger.warning(`加入应用${appId}出现异常\n${JSON.stringify(err)}`); + $.notification.post(`加入[${appId}]失败`); + resolve([false, appId, "", accountId]); + }); + }); +} + +async function startJoinAppTestFlight(accountIds, accountsSessionInfo, appId) { + if (accountIds.length === 0) { + return []; + } + let joinPromise = []; + for (let accountId of accountIds) { + // 遍历N次,尝试加入TestFlight + for (let i = 0; i < tfJoinConcurrency; i++) { + joinPromise.push(joinTestFlight(accountId, accountsSessionInfo[accountId], appId)); + } + } + return joinPromise; +} + +async function checkAppIsOpen(accountId, sessionInfo, appId) { + let allowJoin = false; + let httpCode; + return new Promise(async resolve => { + if (tfJoinMode === 0) { + const tfStatus = await getAppTestFlightHtml(appId); + if (tfStatus === "NOT_FULL") { + allowJoin = true; + $.logger.info(`温和模式下,未满员的应用 [${appId}] 尝试加入`); + } else { + allowJoin = false; + $.logger.info(`温和模式下,已满员的应用 [${appId}] 不尝试加入`); + } + } else if (tfJoinMode === 1) { + [allowJoin, httpCode] = await getAppTestFlightJson(accountId, sessionInfo, appId); + if (allowJoin === false) { + $.logger.info(`标准模式下,已满员的应用 [${appId}] 不尝试加入`); + } else { + $.logger.info(`标准模式下,未满员的应用 [${appId}] 尝试加入`); + } + } else { + $.logger.info(`暴力模式下,不检查AppId的状态,直接强制请求加入应用 [${appId}],可能会被封号,慎用`); + } + $.logger.debug(`AppId [${appId}] 是否允许加入:${allowJoin}`); + resolve(allowJoin === true ? {'app_id': appId, 'http_code': httpCode} : {}); + }).catch((err) => { + $.logger.warning(`检查应用${appId}是否开放出现异常\n${JSON.stringify(err)}`); + return {}; + }); +} + +async function crowd() { + try { + // 获取配置好的AccountId + let allAccountIds = $.data.allSessionNames(tfSessionInfoKey); + // 获取需要加入的App Id + let appIds = $.data.read(tfAppIdKey, "").split(";"); + appIds = appIds.filter(appId => { + return appId !== "" + }); + + if (appIds.length === 0) { + $.notification.post(`没有检测到有效的AppId,请检查配置。`); + return; + } + + if (allAccountIds.length === 0) { + $.notification.post(`没有检测到有效的账户信息,请检查配置。`); + return; + } + + // 获取每个账户的基础数据 + let accountsJoinedAppIds = $.data.read(tfJoinedAppIdKey, null, "", true); + let accountsSessionInfo = $.data.read(tfSessionInfoKey, null, "", true); + + // 账户Session有效性检查 + const checkSessionTime = $.data.read(tfCheckSessionTimeKey, 0); + const checkSessionTimeDiff = parseInt($.data.read(tfCheckSessionTimeDiffKey, 7200)); + const ts = Math.floor(Date.now() / 1000); + if ((ts - checkSessionTime) >= checkSessionTimeDiff) { + // 随机选择一个作为检查用的AppId + const randomIndex = Math.floor(Math.random() * appIds.length); + const appId = appIds[randomIndex]; + for (let accountId of allAccountIds) { + let sessionInfo = accountsSessionInfo[accountId]; + // 只检查有效的账户 + if (sessionInfo["valid"] === true) { + let [_, httpCode] = await getAppTestFlightJson(accountId, sessionInfo, appId); + if (httpCode === 401) { + // 修改账户Session为失效 + sessionInfo["valid"] = false; + $.data.write(tfSessionInfoKey, sessionInfo, accountId); + $.notification.post(`${accountId} 的 Session 已过期,请重新登录`); + } else if (httpCode === 404) { + $.logger.warning(`Session有效性检查异常,所配置的AppId不正确`); + } else if (httpCode === 200) { + // 修改账户Session为有效 + sessionInfo["valid"] = true; + $.data.write(tfSessionInfoKey, sessionInfo, accountId); + $.logger.info(`${accountId} Session 有效性检查通过`); + } + } + } + // 更新检查时间 + $.data.write(tfCheckSessionTimeKey, ts); + } + + let invalidAppIds = $.data.read(tfInvalidAppIdKey, []); + + // 移除已被判断为无效的AppId + appIds = appIds.filter((appId) => appId && !invalidAppIds.includes(appId)); + allAccountIds = allAccountIds.filter((accountId) => { + return accountsSessionInfo[accountId]["valid"] === true + }); + + // 判断是否存在专门用于检查TF是否可加入的账户 + let checkAccountSessionInfo = blankSessionInfo; + if (tfCheckAccountId && !allAccountIds.includes(tfCheckAccountId)) { + // 移除检查账户 + $.logger.error(`存在用于检查的账户 ${tfCheckAccountId} ,但是该账户未配置Session`); + return; + } else if (tfCheckAccountId) { + // 从账户Session中获取用于检查TF可用性账户的session + checkAccountSessionInfo = accountsSessionInfo[tfCheckAccountId]; + $.logger.info(`使用账户 ${tfCheckAccountId} 检查TF可用性`); + } else { + // 未指定用于检查的账户,随机获取一个账户 + tfCheckAccountId = allAccountIds[Math.floor(Math.random() * allAccountIds.length)]; + $.logger.info(`随机使用账户 ${tfCheckAccountId} 检查TF可用性`); + checkAccountSessionInfo = accountsSessionInfo[tfCheckAccountId]; + } + + let appJoinAccountIds = {}; + let openAppInfo = {}; // 可加入的AppId + let checkAppPromise = []; + // 获取本次需要尝试加入的AppId + for (let appId of appIds) { + const accountIds = allAccountIds.filter(accountId => ( + accountsJoinedAppIds[accountId] && + !accountsJoinedAppIds[accountId].includes(appId) && + !accountsJoinedAppIds[accountId].includes("*") + ) || + !accountsJoinedAppIds[accountId] + ); + if (accountIds.length > 0) { + appJoinAccountIds[appId] = accountIds; + checkAppPromise.push(checkAppIsOpen(tfCheckAccountId, checkAccountSessionInfo, appId)); + } else { + $.logger.info(`没有任何账户可以加入App [${appId}] 的Beta测试员\n可能已全部加入完成,或账户Session已过期`); + } + } + await Promise.all(checkAppPromise).then(appInfos => { + for (let _appInfo of appInfos) { + if (_appInfo && _appInfo["app_id"] && _appInfo["http_code"] === 200) { + openAppInfo[_appInfo["app_id"]] = appJoinAccountIds[_appInfo["app_id"]]; + } + } + }).catch(e => { + $.logger.error(`检查App是否可加入异常:${e}`); + }); + + // 尝试加入所有开放的AppId + let joinPromise = []; + for (let appId in openAppInfo) { + if (appId !== "") { + let temp = await startJoinAppTestFlight(openAppInfo[appId], accountsSessionInfo, appId) + joinPromise = joinPromise.concat(temp); + } + } + // 执行加入Beta测试员 + await Promise.all(joinPromise).then(results => { + let messages = {}; + for (let val of results) { + if (val[0] === true) { + const key = `${val[1]}-${val[3]}` + if (!(key in messages)) { + messages[key] = val; + } + } + } + // 发送通知 + for (let key in messages) { + $.notification.post(`${scriptName}-${messages[key][3]}`, `已成功加入[${messages[key][2]}]的Beta测试员`); + } + }).catch(err => { + $.logger.error(err); + }) + + } catch (err) { + $.logger.info(`自动加入TestFlight出现异常\n${err}`); + } +} + +async function loop() { + let i = 0; + while (true) { + if (tfLoopCount > 0 && i >= tfLoopCount) { + break; + } + i++; + await new Promise(resolve => setTimeout(async () => { + await crowd(); + resolve(); + }, 800)); + } +} + +(async () => { + let response = null; + let url; + let headers; + let body; + try { + // $.notification.post(`链接匹配:${$.request.url}`); + if ($.isResponse) { + if (getFullAppIdRegex.test($.request.url)) { + await autoAddAppId(); + } else if (getSessionRegex.test($.request.url)) { + await getTFSessionInfo(); + response = await getAllAppData(); + } else { + $.logger.warning(`意外的HTTP Request匹配,URL:${$.request.url}\n请检查配置是否正确`); + } + } else if ($.isRequest) { + if (modifyTFRequest.test($.request.url) || + modifySettingsTFRequest.test($.request.url) || + modifyInstallTFRequest.test($.request.url)) { + [url, headers, body] = modifyRequest(); + } else { + [url, headers, body] = [$.request.url, $.request.headers, $.request.body]; + $.logger.warning(`意外的HTTP Response匹配,URL:${$.request.url}\n请检查配置是否正确`); + } + } else { + $.logger.info(`开始监控App的Beta测试状态`); + await loop(); + } + } catch (err) { + console.log("出错了\n" + err); + } finally { + if ($.isResponse) { + if (response) { + $.done(response); + } else { + $.done(); + } + } else if ($.isRequest) { + $.done({url, headers, body}); + } else { + $.done(); + } + } +})(); + +/** + * + * $$\ $$\ $$\ $$$$$\ $$$$$$\ $$$$$$\ + * $$$\ $$$ | \__| \__$$ |$$ __$$\ $$ ___$$\ + * $$$$\ $$$$ | $$$$$$\ $$$$$$\ $$\ $$$$$$$\ $$ |$$ / \__| \_/ $$ | + * $$\$$\$$ $$ | \____$$\ $$ __$$\ $$ |$$ _____| $$ |\$$$$$$\ $$$$$ / + * $$ \$$$ $$ | $$$$$$$ |$$ / $$ |$$ |$$ / $$\ $$ | \____$$\ \___$$\ + * $$ |\$ /$$ |$$ __$$ |$$ | $$ |$$ |$$ | $$ | $$ |$$\ $$ | $$\ $$ | + * $$ | \_/ $$ |\$$$$$$$ |\$$$$$$$ |$$ |\$$$$$$$\\$$$$$$ |\$$$$$$ | \$$$$$$ | + * \__| \__| \_______| \____$$ |\__| \_______|\______/ \______/ \______/ + * $$\ $$ | + * \$$$$$$ | + * \______/ + * + */ +// @formatter:off +function MagicJS(e="MagicJS",t="INFO"){const i=()=>{const e=typeof $loon!=="undefined";const t=typeof $task!=="undefined";const n=typeof module!=="undefined";const i=typeof $httpClient!=="undefined"&&!e;const s=typeof $storm!=="undefined";const r=typeof $environment!=="undefined"&&typeof $environment["stash-build"]!=="undefined";const o=i||e||s||r;const u=typeof importModule!=="undefined";return{isLoon:e,isQuanX:t,isNode:n,isSurge:i,isStorm:s,isStash:r,isSurgeLike:o,isScriptable:u,get name(){if(e){return"Loon"}else if(t){return"QuantumultX"}else if(n){return"NodeJS"}else if(i){return"Surge"}else if(u){return"Scriptable"}else{return"unknown"}},get build(){if(i){return $environment["surge-build"]}else if(r){return $environment["stash-build"]}else if(s){return $storm.buildVersion}},get language(){if(i||r){return $environment["language"]}},get version(){if(i){return $environment["surge-version"]}else if(r){return $environment["stash-version"]}else if(s){return $storm.appVersion}else if(n){return process.version}},get system(){if(i){return $environment["system"]}else if(n){return process.platform}},get systemVersion(){if(s){return $storm.systemVersion}},get deviceName(){if(s){return $storm.deviceName}}}};const s=(n,e="INFO")=>{let i=e;const s={SNIFFER:6,DEBUG:5,INFO:4,NOTIFY:3,WARNING:2,ERROR:1,CRITICAL:0,NONE:-1};const r={SNIFFER:"",DEBUG:"",INFO:"",NOTIFY:"",WARNING:"❗ ",ERROR:"❌ ",CRITICAL:"❌ ",NONE:""};const t=(e,t="INFO")=>{if(!(s[i]{i=e};return{setLevel:o,sniffer:e=>{t(e,"SNIFFER")},debug:e=>{t(e,"DEBUG")},info:e=>{t(e,"INFO")},notify:e=>{t(e,"NOTIFY")},warning:e=>{t(e,"WARNING")},error:e=>{t(e,"ERROR")},retry:e=>{t(e,"RETRY")}}};return new class{constructor(e,t){this._startTime=Date.now();this.version="3.0.0";this.scriptName=e;this.env=i();this.logger=s(e,t);this.http=typeof MagicHttp==="function"?MagicHttp(this.env,this.logger):undefined;this.data=typeof MagicData==="function"?MagicData(this.env,this.logger):undefined;this.notification=typeof MagicNotification==="function"?MagicNotification(this.scriptName,this.env,this.logger):undefined;this.utils=typeof MagicUtils==="function"?MagicUtils(this.env,this.logger):undefined;this.qinglong=typeof MagicQingLong==="function"?MagicQingLong(this.env,this.data,this.logger):undefined;if(typeof this.data!=="undefined"){let e=this.data.read("magic_loglevel");const n=this.data.read("magic_bark_url");if(e){this.logger.setLevel(e.toUpperCase())}if(n){this.notification.setBark(n)}}}get isRequest(){return typeof $request!=="undefined"&&typeof $response==="undefined"}get isResponse(){return typeof $response!=="undefined"}get isDebug(){return this.logger.level==="DEBUG"}get request(){if(typeof $request!=="undefined"){this.logger.sniffer(`RESPONSE:\n${JSON.stringify($request)}`);return $request}}get response(){if(typeof $response!=="undefined"){if($response.hasOwnProperty("status"))$response["statusCode"]=$response["status"];if($response.hasOwnProperty("statusCode"))$response["status"]=$response["statusCode"];this.logger.sniffer(`RESPONSE:\n${JSON.stringify($response)}`);return $response}else{return undefined}}done=(e={})=>{this._endTime=Date.now();let t=(this._endTime-this._startTime)/1e3;this.logger.info(`SCRIPT COMPLETED: ${t} S.`);if(typeof $done!=="undefined"){$done(e)}}}(e,t)} +function MagicHttp(c,l){const e="Mozilla/5.0 (iPhone; CPU iPhone OS 13_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.5 Mobile/15E148 Safari/604.1";const t="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36 Edg/84.0.522.59";let r;if(c.isNode){const S=require("axios");r=S.create()}class s{constructor(e=true){this.handlers=[];this.isRequest=e}use(e,t,r){this.handlers.push({fulfilled:e,rejected:t,synchronous:r?r.synchronous:false,runWhen:r?r.runWhen:null});return this.handlers.length-1}eject(e){if(this.handlers[e]){this.handlers[e]=null}}forEach(t){this.handlers.forEach(e=>{if(e!==null){t(e)}})}}function n(e){let r={...e};if(!!r.params){if(!c.isNode){let e=Object.keys(r.params).map(e=>{const t=encodeURIComponent(e);r.url=r.url.replace(new RegExp(`${e}=[^&]*`,"ig"),"");r.url=r.url.replace(new RegExp(`${t}=[^&]*`,"ig"),"");return`${t}=${encodeURIComponent(r.params[e])}`}).join("&");if(r.url.indexOf("?")<0)r.url+="?";if(!/(&|\?)$/g.test(r.url)){r.url+="&"}r.url+=e;delete r.params;l.debug(`Params to QueryString: ${r.url}`)}}return r}const d=(e,t)=>{let r=typeof t==="object"?{headers:{},...t}:{url:t,headers:{}};if(!r.method){r["method"]=e}r=n(r);if(r["rewrite"]===true){if(c.isSurge){r.headers["X-Surge-Skip-Scripting"]=false;delete r["rewrite"]}else if(c.isQuanX){r["hints"]=false;delete r["rewrite"]}}if(c.isSurge){if(r["method"]!=="GET"&&r.headers["Content-Type"].indexOf("application/json")>=0&&r.body instanceof Array){r.body=JSON.stringify(r.body);l.debug(`Convert Array object to String: ${r.body}`)}}else if(c.isQuanX){if(r.hasOwnProperty("body")&&typeof r["body"]!=="string")r["body"]=JSON.stringify(r["body"]);r["method"]=e}else if(c.isNode){if(e==="POST"||e==="PUT"||e==="PATCH"||e==="DELETE"){r.data=r.data||r.body}else if(e==="GET"){r.params=r.params||r.body}delete r.body}return r};const f=(t,r=null)=>{if(t){let e={...t,config:t.config||r,status:t.statusCode||t.status,body:t.body||t.data,headers:t.headers||t.header};if(typeof e.body==="string"){try{e.body=JSON.parse(e.body)}catch{}}delete t.data;return e}else{return t}};const o=r=>{return Object.keys(r).reduce((e,t)=>{e[t.toLowerCase()]=r[t];return e},{})};const i=s=>{return Object.keys(s).reduce((e,t)=>{const r=t.split("-").map(e=>e[0].toUpperCase()+e.slice(1)).join("-");e[r]=s[t];return e},{})};const h=(t,r=null)=>{if(!!t&&t.status>=400){l.debug(`Raise exception when status code is ${t.status}`);let e={name:"RequestException",message:`Request failed with status code ${t.status}`,config:r||t.config,response:t};return e}};const a={request:new s,response:new s(false)};let p=[];let y=[];let g=true;function m(e){e=n(e);l.debug(`HTTP ${e["method"].toUpperCase()}:\n${JSON.stringify(e)}`);return e}function b(e){try{e=!!e?f(e):e;l.sniffer(`HTTP ${e.config["method"].toUpperCase()}:\n${JSON.stringify(e.config)}\nSTATUS CODE:\n${e.status}\nRESPONSE:\n${typeof e.body==="object"?JSON.stringify(e.body):e.body}`);const t=h(e);if(!!t){return Promise.reject(t)}return e}catch(t){l.error(t);return e}}const T=t=>{try{p=[];y=[];a.request.forEach(e=>{if(typeof e.runWhen==="function"&&e.runWhen(t)===false){return}g=g&&e.synchronous;p.unshift(e.fulfilled,e.rejected)});a.response.forEach(e=>{y.push(e.fulfilled,e.rejected)})}catch(e){l.error(`Failed to register interceptors: ${e}.`)}};const u=(e,s)=>{let n;const t=e.toUpperCase();s=d(t,s);if(c.isNode){n=r}else{if(c.isSurgeLike){n=o=>{return new Promise((s,n)=>{$httpClient[e.toLowerCase()](o,(t,r,e)=>{if(t){let e={name:t.name||t,message:t.message||t,stack:t.stack||t,config:o,response:f(r)};n(e)}else{r.config=o;r.body=e;s(r)}})})}}else{n=n=>{return new Promise((r,s)=>{$task.fetch(n).then(e=>{e=f(e,n);const t=h(e,n);if(t){return Promise.reject(t)}r(e)}).catch(e=>{let t={name:e.message||e.error,message:e.message||e.error,stack:e.error,config:n,response:!!e.response?f(e.response):null};s(t)})})}}}let o;T(s);const i=[m,undefined];const a=[b,undefined];if(!g){l.debug("Interceptors are executed in asynchronous mode.");let r=[n,undefined];Array.prototype.unshift.apply(r,i);Array.prototype.unshift.apply(r,p);r=r.concat(a);r=r.concat(y);o=Promise.resolve(s);while(r.length){try{let e=r.shift();let t=r.shift();if(!c.isNode&&s["timeout"]&&e===n){o=u(s)}else{o=o.then(e,t)}}catch(e){l.error(`request exception: ${e}`)}}return o}else{l.debug("Interceptors are executed in synchronous mode.");Array.prototype.unshift.apply(p,i);p=p.concat([m,undefined]);while(p.length){let e=p.shift();let t=p.shift();try{s=e(s)}catch(e){t(e);break}}try{if(!c.isNode&&s["timeout"]){o=u(s)}else{o=n(s)}}catch(e){return Promise.reject(e)}Array.prototype.unshift.apply(y,a);while(y.length){o=o.then(y.shift(),y.shift())}return o}function u(r){try{const e=new Promise((e,t)=>{setTimeout(()=>{let e={message:`timeout of ${r["timeout"]}ms exceeded.`,config:r};t(e)},r["timeout"])});return Promise.race([n(r),e])}catch(e){l.error(`Request Timeout exception: ${e}.`)}}};return{request:u,interceptors:a,convertHeadersToLowerCase:o,convertHeadersToCamelCase:i,modifyResponse:f,get:e=>{return u("GET",e)},post:e=>{return u("POST",e)},put:e=>{return u("PUT",e)},patch:e=>{return u("PATCH",e)},delete:e=>{return u("DELETE",e)},head:e=>{return u("HEAD",e)},options:e=>{return u("OPTIONS",e)}}} +function MagicData(i,u){let f={fs:undefined,data:{}};if(i.isNode){f.fs=require("fs");try{f.fs.accessSync("./magic.json",f.fs.constants.R_OK|f.fs.constants.W_OK)}catch(e){f.fs.writeFileSync("./magic.json","{}",{encoding:"utf8"})}f.data=require("./magic.json")}const o=(e,t)=>{if(typeof t==="object"){return false}else{return e===t}};const a=e=>{if(e==="true"){return true}else if(e==="false"){return false}else if(typeof e==="undefined"){return null}else{return e}};const c=(e,t,s,n)=>{if(s){try{if(typeof e==="string")e=JSON.parse(e);if(e["magic_session"]===true){e=e[s]}else{e=null}}catch{e=null}}if(typeof e==="string"&&e!=="null"){try{e=JSON.parse(e)}catch{}}if(n===false&&!!e&&e["magic_session"]===true){e=null}if((e===null||typeof e==="undefined")&&t!==null&&typeof t!=="undefined"){e=t}e=a(e);return e};const l=t=>{if(typeof t==="string"){let e={};try{e=JSON.parse(t);const s=typeof e;if(s!=="object"||e instanceof Array||s==="bool"||e===null){e={}}}catch{}return e}else if(t instanceof Array||t===null||typeof t==="undefined"||t!==t||typeof t==="boolean"){return{}}else{return t}};const y=(e,t=null,s="",n=false,r=null)=>{let l=r||f.data;if(!!l&&typeof l[e]!=="undefined"&&l[e]!==null){val=l[e]}else{val=!!s?{}:null}val=c(val,t,s,n);return val};const d=(e,t=null,s="",n=false,r=null)=>{let l="";if(r||i.isNode){l=y(e,t,s,n,r)}else{if(i.isSurgeLike){l=$persistentStore.read(e)}else if(i.isQuanX){l=$prefs.valueForKey(e)}l=c(l,t,s,n)}u.debug(`READ DATA [${e}]${!!s?`[${s}]`:""} <${typeof l}>\n${JSON.stringify(l)}`);return l};const p=(t,s,n="",e=null)=>{let r=e||f.data;r=l(r);if(!!n){let e=l(r[t]);e["magic_session"]=true;e[n]=s;r[t]=e}else{r[t]=s}if(e!==null){e=r}return r};const S=(e,t,s="",n=null)=>{if(typeof t==="undefined"||t!==t){return false}if(!i.isNode&&(typeof t==="boolean"||typeof t==="number")){t=String(t)}let r="";if(n||i.isNode){r=p(e,t,s,n)}else{if(!s){r=t}else{if(i.isSurgeLike){r=!!$persistentStore.read(e)?$persistentStore.read(e):r}else if(i.isQuanX){r=!!$prefs.valueForKey(e)?$prefs.valueForKey(e):r}r=l(r);r["magic_session"]=true;r[s]=t}}if(!!r&&typeof r==="object"){r=JSON.stringify(r,null,4)}u.debug(`WRITE DATA [${e}]${s?`[${s}]`:""} <${typeof t}>\n${JSON.stringify(t)}`);if(!n){if(i.isSurgeLike){return $persistentStore.write(r,e)}else if(i.isQuanX){return $prefs.setValueForKey(r,e)}else if(i.isNode){try{f.fs.writeFileSync("./magic.json",r);return true}catch(e){u.error(e);return false}}}return true};const e=(t,s,n,r=o,l=null)=>{s=a(s);const e=d(t,null,n,false,l);if(r(e,s)===true){return false}else{const i=S(t,s,n,l);let e=d(t,null,n,false,l);if(r===o&&typeof e==="object"){return i}return r(s,e)}};const g=(e,t,s)=>{let n=s||f.data;n=l(n);if(!!t){obj=l(n[e]);delete obj[t];n[e]=obj}else{delete n[e]}if(!!s){s=n}return n};const t=(e,t="",s=null)=>{let n={};if(s||i.isNode){n=g(e,t,s);if(!s){f.fs.writeFileSync("./magic.json",JSON.stringify(n,null,4))}else{s=n}}else{if(!t){if(i.isStorm){return $persistentStore.remove(e)}else if(i.isSurgeLike){return $persistentStore.write(null,e)}else if(i.isQuanX){return $prefs.removeValueForKey(e)}}else{if(i.isSurgeLike){n=$persistentStore.read(e)}else if(i.isQuanX){n=$prefs.valueForKey(e)}n=l(n);delete n[t];const r=JSON.stringify(n,null,4);S(e,r)}}u.debug(`DELETE KEY [${e}]${!!t?`[${t}]`:""}`)};const s=(e,t=null)=>{let s=[];let n=d(e,null,null,true,t);n=l(n);if(n["magic_session"]!==true){s=[]}else{s=Object.keys(n).filter(e=>e!=="magic_session")}u.debug(`READ ALL SESSIONS [${e}] <${typeof s}>\n${JSON.stringify(s,null,4)}`);return s};const n=(e,t=null)=>{let s={};let n=d(e,null,null,true,t);n=l(n);if(n["magic_session"]===true){s={...n};delete s["magic_session"]}u.debug(`READ ALL SESSIONS [${e}] <${typeof s}>\n${JSON.stringify(s,null,4)}`);return s};return{read:d,write:S,del:t,update:e,allSessions:n,allSessionNames:s,defaultValueComparator:o,convertToObject:l}} +function MagicNotification(r,f,l){let s=null;let u=null;const c=typeof MagicHttp==="function"?MagicHttp(f,l):undefined;const e=t=>{try{let e=t.replace(/\/+$/g,"");s=`${/^https?:\/\/([^/]*)/.exec(e)[0]}/push`;u=/\/([^\/]+)\/?$/.exec(e)[1]}catch(e){l.error(`Bark url error: ${e}.`)}};function t(e=r,t="",i="",o=""){const n=i=>{try{let t={};if(typeof i==="string"){if(f.isLoon)t={openUrl:i};else if(f.isQuanX)t={"open-url":i};else if(f.isSurge)t={url:i}}else if(typeof i==="object"){if(f.isLoon){t["openUrl"]=!!i["open-url"]?i["open-url"]:"";t["mediaUrl"]=!!i["media-url"]?i["media-url"]:""}else if(f.isQuanX){t=!!i["open-url"]||!!i["media-url"]?i:{}}else if(f.isSurge){let e=i["open-url"]||i["openUrl"];t=e?{url:e}:{}}}return t}catch(e){l.error(`Failed to convert notification option, ${e}`)}return i};o=n(o);if(arguments.length==1){e=r;t="",i=arguments[0]}l.notify(`title:${e}\nsubTitle:${t}\nbody:${i}\noptions:${typeof o==="object"?JSON.stringify(o):o}`);if(f.isSurge){$notification.post(e,t,i,o)}else if(f.isLoon){if(!!o)$notification.post(e,t,i,o);else $notification.post(e,t,i)}else if(f.isQuanX){$notify(e,t,i,o)}if(s&&u&&typeof c!=="undefined"){p(e,t,i)}}function i(e=r,t="",i="",o=""){if(l.level==="DEBUG"){if(arguments.length==1){e=r;t="",i=arguments[0]}this.notify(e,t,i,o)}}function p(e=r,t="",i="",o=""){if(typeof c==="undefined"||typeof c.post==="undefined"){throw"Bark notification needs to import MagicHttp module."}let n={url:s,headers:{"Content-Type":"application/json; charset=utf-8"},body:{title:e,body:t?`${t}\n${i}`:i,device_key:u}};c.post(n).catch(e=>{l.error(`Bark notify error: ${e}`)})}return{post:t,debug:i,bark:p,setBark:e}} +function MagicUtils(r,h){const e=(o,i=5,l=0,a=null)=>{return(...e)=>{return new Promise((s,r)=>{function n(...t){Promise.resolve().then(()=>o.apply(this,t)).then(e=>{if(typeof a==="function"){Promise.resolve().then(()=>a(e)).then(()=>{s(e)}).catch(e=>{if(i>=1){if(l>0)setTimeout(()=>n.apply(this,t),l);else n.apply(this,t)}else{r(e)}i--})}else{s(e)}}).catch(e=>{h.error(e);if(i>=1&&l>0){setTimeout(()=>n.apply(this,t),l)}else if(i>=1){n.apply(this,t)}else{r(e)}i--})}n.apply(this,e)})}};const t=(e,t="yyyy-MM-dd hh:mm:ss")=>{let s={"M+":e.getMonth()+1,"d+":e.getDate(),"h+":e.getHours(),"m+":e.getMinutes(),"s+":e.getSeconds(),"q+":Math.floor((e.getMonth()+3)/3),S:e.getMilliseconds()};if(/(y+)/.test(t))t=t.replace(RegExp.$1,(e.getFullYear()+"").substr(4-RegExp.$1.length));for(let e in s)if(new RegExp("("+e+")").test(t))t=t.replace(RegExp.$1,RegExp.$1.length==1?s[e]:("00"+s[e]).substr((""+s[e]).length));return t};const s=()=>{return t(new Date,"yyyy-MM-dd hh:mm:ss")};const n=()=>{return t(new Date,"yyyy-MM-dd")};const o=t=>{return new Promise(e=>setTimeout(e,t))};const i=(e,t=null)=>{if(r.isNode){const s=require("assert");if(t)s(e,t);else s(e)}else{if(e!==true){let e=`AssertionError: ${t||"The expression evaluated to a falsy value"}`;h.error(e)}}};return{retry:e,formatTime:t,now:s,today:n,sleep:o,assert:i}} +function MagicQingLong(e,s,o){let i="";let l="";let c="";let u="";let d="";let n="";const g="magic.json";const r=3e3;const f=MagicHttp(e,o);const t=(e,n,r,t,a)=>{i=e;c=n;u=r;l=t;d=a};function a(e){i=i||s.read("magic_qlurl");n=n||s.read("magic_qltoken");return e}function p(e){if(!i){i=s.read("magic_qlurl")}if(e.url.indexOf(i)<0){e.url=`${i}${e.url}`}return{...e,timeout:r}}function y(e){e.params={...e.params,t:Date.now()};return e}function m(e){n=n||s.read("magic_qltoken");if(n){e.headers["Authorization"]=`Bearer ${n}`}return e}function h(e){c=c||s.read("magic_qlclient");if(!!c){e.url=e.url.replace("/api/","/open/")}return e}async function b(e){try{const n=e.message||e.error||JSON.stringify(e);if((n.indexOf("NSURLErrorDomain")>=0&&n.indexOf("-1012")>=0||!!e.response&&e.response.status===401)&&!!e.config&&e.config.refreshToken!==true){o.warning(`Qinglong Panel token has expired.`);await v();e.config["refreshToken"]=true;return await f.request(e.config.method,e.config)}else{return Promise.reject(e)}}catch(e){return Promise.reject(e)}}f.interceptors.request.use(a,undefined);f.interceptors.request.use(p,undefined);f.interceptors.request.use(h,undefined,{runWhen:e=>{return e.url.indexOf("api/user/login")<0&&e.url.indexOf("open/auth/token")<0}});f.interceptors.request.use(m,undefined,{runWhen:e=>{return e.url.indexOf("api/user/login")<0&&e.url.indexOf("open/auth/token")<0}});f.interceptors.request.use(y,undefined,{runWhen:e=>{return e.url.indexOf("open/auth/token")<0&&e.url.indexOf("t=")<0}});f.interceptors.response.use(undefined,b);async function v(){c=c||s.read("magic_qlclient");u=u||s.read("magic_qlsecrt");l=l||s.read("magic_qlname");d=d||s.read("magic_qlpwd");if(i&&c&&u){await f.get({url:`/open/auth/token`,headers:{"Content-Type":"application/json"},params:{client_id:c,client_secret:u}}).then(e=>{o.info("Successfully logged in to Qinglong Panel");n=e.body.data.token;s.update("magic_qltoken",n);return n}).catch(e=>{o.error(`Error logging in to Qinglong Panel.\n${e.message}`)})}else if(i&&l&&d){await f.post({url:`/api/user/login`,headers:{"Content-Type":"application/json"},body:{username:l,password:d}}).then(e=>{o.info("Successfully logged in to Qinglong Panel");n=e.body.data.token;s.update("magic_qltoken",n);return n}).catch(e=>{o.error(`Error logging in to Qinglong Panel.\n${e.message}`)})}}async function E(n,r,t=null){i=i||s.read("magic_qlurl");if(t===null){let e=await w([{name:n,value:r}]);if(!!e&&e.length===1){return e[0]}}else{f.put({url:`/api/envs`,headers:{"Content-Type":"application/json"},body:{name:n,value:r,id:t}}).then(e=>{if(e.body.code===200){o.debug(`QINGLONG UPDATE ENV ${n} <${typeof r}> (${t})\n${JSON.stringify(r)}`);return true}else{o.error(`Error updating environment variable from Qinglong Panel.\n${JSON.stringify(e)}`)}}).catch(e=>{o.error(`Error updating environment variable from Qinglong Panel.\n${e.message}`);return false})}}async function w(e){let n=[];await f.post({url:`/api/envs`,headers:{"Content-Type":"application/json"},body:e}).then(e=>{if(e.body.code===200){e.body.data.forEach(e=>{o.debug(`QINGLONG ADD ENV ${e.name} <${typeof e.value}> (${e.id})\n${JSON.stringify(e)}`);n.push(e.id)})}else{o.error(`Error adding environment variable from Qinglong Panel.\n${JSON.stringify(e)}`)}}).catch(e=>{o.error(`Error adding environment variable from Qinglong Panel.\n${e.message}`)});return n}async function N(n){return await f.delete({url:`/api/envs`,headers:{Accept:"application/json","Accept-Language":"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",Connection:"keep-alive","Content-Type":"application/json;charset=UTF-8","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Edg/102.0.1245.30"},body:n}).then(e=>{if(e.body.code===200){o.debug(`QINGLONG DELETE ENV IDS: ${n}`);return true}else{o.error(`Error deleting environment variable from Qinglong Panel.\n${JSON.stringify(e)}`);return false}}).catch(e=>{o.error(`Error deleting environment variable from Qinglong Panel.\n${e.message}`)})}async function O(t=null,a="",i=0){if(i<=3){let r=[];await f.get({url:`/api/envs`,headers:{"Content-Type":"application/json"},params:{searchValue:a}}).then(e=>{if(e.body.code===200){const n=e.body.data;if(!!t){let e=[];for(const e of n){if(e.name===t){r.push(e)}}r=e}r=n}else{o.error(`Error reading environment variable from Qinglong Panel.\n${JSON.stringify(e)}`);b();i+=1;O(t,a,i)}}).catch(e=>{o.error(`Error reading environment variable from Qinglong Panel.\n${JSON.stringify(e)}`);b();i+=1;O(t,a,i)});return r}else{throw new Error("An error occurred while reading environment variable from Qinglong Panel.")}}async function S(e){let n=null;const r=await O();for(const t of r){if(t.id===e){n=t;break}}return n}async function $(n){let r=false;await f.put({url:`/api/envs/disable`,headers:{Accept:"application/json","Accept-Language":"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",Connection:"keep-alive","Content-Type":"application/json;charset=UTF-8","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Edg/102.0.1245.30"},body:n}).then(e=>{if(e.body.code===200){o.debug(`QINGLONG DISABLED ENV IDS: ${n}`);r=true}else{o.error(`Error disabling environment variable from Qinglong Panel.\n${JSON.stringify(e)}`)}}).catch(e=>{o.error(`Error disabling environment variable from Qinglong Panel.\n${e.message}`)});return r}async function T(n){let r=false;await f.put({url:`/api/envs/enable`,headers:{Accept:"application/json","Accept-Language":"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",Connection:"keep-alive","Content-Type":"application/json;charset=UTF-8","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36 Edg/102.0.1245.30"},body:n}).then(e=>{if(e.body.code===200){o.debug(`QINGLONG ENABLED ENV IDS: ${n}`);r=true}else{o.error(`Error enabling environment variable from Qilong panel.\n${JSON.stringify(e)}`)}}).catch(e=>{o.error(`Error enabling environment variable from Qilong panel.\n${e.message}`)});return r}async function Q(e,n="",r=""){let t=false;await f.post({url:`/api/scripts`,headers:{"Content-Type":"application/json"},body:{filename:e,path:n,content:r}}).then(e=>{if(e.body.code===200){t=true}else{o.error(`Error reading data from Qinglong Panel.\n${JSON.stringify(e)}`)}}).catch(e=>{o.error(`Error reading data from Qinglong Panel.\n${e.message}`)});return t}async function q(r,t="",a=0){if(a<=3){let n="";await f.get({url:`/api/scripts/${r}`,params:{path:t}}).then(e=>{if(e.body.code===200){n=e.body.data}else{o.error(`Error reading data from Qinglong Panel.\n${JSON.stringify(e)}`);b();a+=1;q(r,t,a)}}).catch(e=>{o.error(`Error reading data from Qinglong Panel.\n${e.message}`);b();a+=1;q(r,t,a)});return n}else{throw new Error("An error occurred while reading the data from Qinglong Panel.")}}async function P(e,n="",r=""){let t=false;await f.put({url:`/api/scripts`,headers:{"Content-Type":"application/json"},body:{filename:e,path:n,content:r}}).then(e=>{if(e.body.code===200){t=true}else{o.error(`Error reading data from Qinglong Panel.\n${JSON.stringify(e)}`)}}).catch(e=>{o.error(`Error reading data from Qinglong Panel.\n${e.message}`)});return t}async function j(e,n=""){let r=false;await f.delete({url:`/api/scripts`,headers:{"Content-Type":"application/json"},body:{filename:e,path:n}}).then(e=>{if(e.body.code===200){r=true}else{o.error(`Error reading data from Qinglong Panel.\n${JSON.stringify(e)}`)}}).catch(e=>{o.error(`Error reading data from Qinglong Panel.\n${e.message}`)});return r}async function k(e,n,r=""){let t=await q(g,"");let a=s.convertToObject(t);let i=s.write(e,n,r,a);t=JSON.stringify(a,null,4);let o=await P(g,"",t);return o&&i}async function C(...n){let e=await q(g,"");let r=s.convertToObject(e);for(let e of n){s.write(e[0],e[1],typeof e[2]!=="undefined"?e[2]:"",r)}e=JSON.stringify(r,null,4);return await P(g,"",e)}async function J(e,n,r,t=s.defaultValueComparator){let a=await q(g,"");let i=s.convertToObject(a);const o=s.update(e,n,r,t,i);let l=false;if(o===true){a=JSON.stringify(i,null,4);l=await P(g,"",a)}return o&&l}async function A(...n){let e=await q(g,"");let r=s.convertToObject(e);for(let e of n){s.update(e[0],e[1],typeof e[2]!=="undefined"?e[2]:"",typeof e[3]!=="undefined"?e["comparator"]:s.defaultValueComparator,r)}e=JSON.stringify(r,null,4);return await P(g,"",e)}async function G(e,n,r="",t=false){let a=await q(g,"");let i=s.convertToObject(a);return s.read(e,n,r,t,i)}async function L(...n){let e=await q(g,"");let r=s.convertToObject(e);let t=[];for(let e of n){const a=s.read(e[0],e[1],typeof e[2]!=="undefined"?e[2]:"",typeof e[3]==="boolean"?e[3]:false,r);t.push(a)}return t}async function _(e,n=""){let r=await q(g,"");let t=s.convertToObject(r);const a=s.del(e,n,t);r=JSON.stringify(t,null,4);const i=await P(g,"",r);return a&&i}async function x(...n){let e=await q(g,"");let r=s.convertToObject(e);for(let e of n){s.del(e[0],typeof e[1]!=="undefined"?e[1]:"",r)}e=JSON.stringify(r,null,4);return await P(g,"",e)}async function D(e){let n=await q(g,"");let r=s.convertToObject(n);return s.allSessionNames(e,r)}async function W(e){let n=await q(g,"");let r=s.convertToObject(n);return s.allSessions(e,r)}return{url:i||s.read("magic_qlurl"),init:t,getToken:v,setEnv:E,setEnvs:w,getEnv:S,getEnvs:O,delEnvs:N,disableEnvs:$,enableEnvs:T,addScript:Q,getScript:q,editScript:P,delScript:j,write:k,read:G,del:_,update:J,batchWrite:C,batchRead:L,batchUpdate:A,batchDel:x,allSessions:W,allSessionNames:D}} +// @formatter:on \ No newline at end of file diff --git a/script/testflight/testflight.lnplugin b/script/testflight/testflight.lnplugin new file mode 100644 index 000000000..5296786ad --- /dev/null +++ b/script/testflight/testflight.lnplugin @@ -0,0 +1,28 @@ +#!name= TestFlight +#!desc= 多账户共存与自动加入测试 +#!openUrl= https://github.com/blackmatrix7/ios_rule_script +#!author= blackmatrix7 +#!homepage= https://github.com/blackmatrix7/ios_rule_script +#!icon= https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/icon/testflight.png + +[General] +skip-proxy = iosapps.itunes.apple.com + +[Script] +# TestFlight_设置篡改 +http-request ^https:\/\/testflight\.apple\.com\/v\d\/accounts\/settings\/.*\/apps\/\d+ requires-body=1,script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js,tag=TestFlight_设置篡改 + +# TestFlight_应用篡改 +http-request ^https:\/\/testflight\.apple\.com\/v\d\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/apps\/\d+ requires-body=1,script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js,tag=TestFlight_应用篡改 + +# TestFlight_状态篡改 +http-request ^https:\/\/testflight\.apple\.com\/v\d\/apps\/\d+\/\d+\/install\/status$ requires-body=1,script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js,tag=TestFlight_状态篡改 + +# TestFlight_应用共存 +http-response ^https:\/\/testflight\.apple\.com\/v\d\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/apps$ requires-body=1,script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js,tag=TestFlight_应用共存 + +# TestFlight_自动入列 +http-response ^https:\/\/testflight\.apple\.com\/v3\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/ru\/([a-zA-Z0-9]{8})$ requires-body=1,script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js,tag=TestFlight_自动入列 + +[MITM] +hostname = testflight.apple.com diff --git a/script/testflight/testflight.sgmodule b/script/testflight/testflight.sgmodule new file mode 100644 index 000000000..fcb21e5c9 --- /dev/null +++ b/script/testflight/testflight.sgmodule @@ -0,0 +1,15 @@ +#!name=TestFlight +#!desc=多账户共存与自动加入测试 + +[General] +skip-proxy = %APPEND% iosapps.itunes.apple.com + +[Script] +TestFlight_设置篡改 = type=http-request,requires-body=1,max-size=0,pattern=^https:\/\/testflight\.apple\.com\/v\d\/accounts\/settings\/.*\/apps\/\d+,script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js +TestFlight_应用篡改 = type=http-request,requires-body=1,max-size=0,pattern=^https:\/\/testflight\.apple\.com\/v\d\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/apps\/\d+,script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js +TestFlight_状态篡改 = type=http-request,requires-body=1,max-size=0,pattern=^https:\/\/testflight\.apple\.com\/v\d\/apps\/\d+\/\d+\/install\/status$,script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js +TestFlight_应用共存 = type=http-response,requires-body=1,max-size=0,pattern=^https:\/\/testflight\.apple\.com\/v\d\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/apps$,script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js +TestFlight_自动入列 = type=http-response,requires-body=1,max-size=0,pattern=^https:\/\/testflight\.apple\.com\/v\d\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/ru\/([a-zA-Z0-9]{8})$,script-path=https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js + +[MITM] +hostname = %APPEND% testflight.apple.com \ No newline at end of file diff --git a/script/testflight/testflight.snippet b/script/testflight/testflight.snippet new file mode 100644 index 000000000..442965ec8 --- /dev/null +++ b/script/testflight/testflight.snippet @@ -0,0 +1,22 @@ +# TestFlight +# 多账户共存与自动加入测试 + +# TestFlight_自动入列 +^https:\/\/testflight\.apple\.com\/v\d+\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/ru\/([a-zA-Z0-9]{8})$ url script-response-body https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js + +# TestFlight_应用共存 +^https:\/\/testflight\.apple\.com\/v\d+\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/apps$ url script-response-body https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js + +# TestFlight_设置篡改 +^https:\/\/testflight\.apple\.com\/v\d+\/accounts\/settings\/.*\/apps\/\d+ url script-request-header https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js + +# TestFlight_更新篡改 +^https:\/\/testflight\.apple\.com\/v\d+\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/apps\/\d+\/.*install$ url script-request-body https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js + +# TestFlight_状态篡改 +^https:\/\/testflight\.apple\.com\/v\d\/apps\/\d+\/\d+\/install\/status$ url script-request-body https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js + +# TestFlight_应用篡改 +^https:\/\/testflight\.apple\.com\/v\d+\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/apps\/\d+\/(?!install).* url script-request-header https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js + +hostname = testflight.apple.com diff --git a/script/testflight/testflight.stoverride b/script/testflight/testflight.stoverride new file mode 100644 index 000000000..d73618960 --- /dev/null +++ b/script/testflight/testflight.stoverride @@ -0,0 +1,54 @@ +name: TestFlight +desc: 多账户共存与自动加入测试 + +http: + script: + + # TestFlight_设置篡改 + - match: ^https:\/\/testflight\.apple\.com\/v\d\/accounts\/settings\/.*\/apps\/\d+ + name: testflight.js + type: request + require-body: true + timeout: 60 + argument: '' + + # TestFlight_应用篡改 + - match: ^https:\/\/testflight\.apple\.com\/v\d\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/apps\/\d+ + name: testflight.js + type: request + require-body: true + timeout: 60 + argument: '' + + # TestFlight_状态篡改 + - match: ^https:\/\/testflight\.apple\.com\/v\d\/apps\/\d+\/\d+\/install\/status$ + name: testflight.js + type: request + require-body: true + timeout: 60 + argument: '' + + # TestFlight_获取信息 + - match: ^https:\/\/testflight\.apple\.com\/v\d\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/apps$ + name: testflight.js + type: response + require-body: true + timeout: 60 + argument: '' + + # TestFlight_自动入列 + - match: ^https:\/\/testflight\.apple\.com\/v3\/accounts\/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}\/ru\/([a-zA-Z0-9]{8})$ + name: testflight.js + type: response + require-body: true + timeout: 60 + argument: '' + + mitm: + - "testflight.apple.com" + +script-providers: + testflight.js: + url: https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/script/testflight/testflight.js + interval: 86400 +