Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions NobyDa_BoxJs.json
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,23 @@
"val": false,
"type": "boolean",
"desc": "用于调试脚本, 一般用户请勿开启."
}, {
"id": "@TESTFLIGHT-ACCOUNT.EnableCache",
"name": "启用缓存",
"val": false,
"type": "boolean",
"desc": "用于缓存APP列表, 改善列表页面加载过慢,需与\"请求超时\"配合使用。开启缓存并刷新列表后,可适当调小超时"
}, {
"id": "@TESTFLIGHT-ACCOUNT.Timeout",
"name": "请求超时",
"val":"",
"type": "number",
"placeholder": "30",
"desc": "默认为30, 单位: 秒"
}],
"scripts": [{
"name": "清除缓存",
"script": "https://gist.githubusercontent.com/NobyDa/d025c53d3922657f921b983ce129fc1d/raw/TestFlightAccountRemoveCache.js"
}],
"author": "@NobyDa",
"repo": "https://github.com/NobyDa/Script/blob/master/TestFlight/TestFlightAccount.js",
Expand Down
4 changes: 3 additions & 1 deletion Surge/Module/TestFlightAccount.sgmodule
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#!name=TestFlight账户管理
#!desc=自动存储/合并多个TestFlight账户列表, 并可导出/分享TestFlight APP.
#!arguments=请求超时:30,列表缓存:0
#!arguments-desc=请求超时:单位秒\n列表缓存:1/0,分别为开启/关闭

[General]
skip-proxy = %APPEND% iosapps.itunes.apple.com

[Script]
TestFlight账户管理 = type=http-request,pattern=^https:\/\/testflight\.apple\.com\/v\d\/(app|account|invite)s\/,requires-body=1,timeout=120,script-path=https://raw.githubusercontent.com/NobyDa/Script/master/TestFlight/TestFlightAccount.js
TestFlight账户管理 = type=http-request,pattern=^https:\/\/testflight\.apple\.com\/v\d\/(app|account|invite)s\/,requires-body=1,timeout=180,script-path=https://raw.githubusercontent.com/NobyDa/Script/master/TestFlight/TestFlightAccount.js, argument="timeout={{{请求超时}}}&enableCache={{{列表缓存}}}"

[MITM]
hostname = %APPEND% testflight.apple.com
46 changes: 34 additions & 12 deletions TestFlight/TestFlightAccount.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ TestFlight账户管理脚本

脚本作者: @NobyDa
脚本兼容: Surge4、QuantumultX、Loon(2.1.20 413+)
更新时间: 2024/02/23
更新时间: 2024/03/23
主要功能:
1. 自动存储多个TestFlight账户,并自动合并APP列表,避免切换账户。

Expand Down Expand Up @@ -47,11 +47,13 @@ https://raw.githubusercontent.com/NobyDa/Script/master/Loon/Loon_TF_Account.plug
*********************************/

const $ = API("TESTFLIGHT-ACCOUNT");
const args = formatArgument(typeof $argument == "string" && $argument || '');
$.env.isNode ? $request = $.read('Request') : null;
const [arr, obj, req, rsp] = [[], new Map(), $request, {}];
const [k1, k2, k3] = ['x-session-id', 'x-request-id', 'x-session-digest'];
const [list, appList] = [$.read('AccountList') || {}, $.read('AppList') || {}];
$.debug = $.read('Debug') === 'true';
$.EnableCache = Number(args.enableCache) || ($.read('EnableCache') === 'true');

runs()
.catch(e => $.error(e.error || e.message || e))
Expand Down Expand Up @@ -133,9 +135,13 @@ function formatHeaders(h) {
return Object.keys(h).reduce((t, i) => (t[i.toLowerCase()] = h[i], t), {})
}

function formatArgument(s) {
return Object.fromEntries(s.split('&').map(item => item.split('=')))
}

function ChangeHeaders(id) {
const re = JSON.parse(JSON.stringify(req)); //easy deep copy
re.timeout = 30;
re.timeout = Number(args.timeout || $.read('Timeout')) || 30;
re.insecure = true;
if (id) {
$.log(`Request header replaced, using "${id}"`);
Expand Down Expand Up @@ -168,22 +174,26 @@ function QueryFallback(o) {
}

function QueryAppList(o) {
const resp = {};
return $.http[req.method.toLowerCase()](ChangeHeaders(o))
.then(r => {
const m = req.url.includes(o);
$.log(`Received response: status=${r.statusCode}, body=${Boolean(r.body)}, account=${o}, main=${m}`);
if (m) {
resp.data = r.body;
resp.main = req.url.includes(o);
$.log(`Received response: status=${r.statusCode}, body=${Boolean(r.body)}, account=${o}, main=${resp.main}`);
if (resp.main) {
[rsp.status, rsp.headers, rsp.body] = [r.statusCode, r.headers, r.body];
}
if (r.statusCode == 401) {
throw new Error('Key expires');
}
const res = JSON.parse(r.body || '{}');
$.log(`Account "${o}" app list: ${$.stringify((res.data || []).map(i => i.name))}`);
return (res.data || []).filter(i => (i.aid = o, !list[o].only || list[o].only.includes(String(i.appAdamId))))
.map(p => arr[m ? 'unshift' : 'push'](p))
}).catch(e => { //surge cannot get 401 in apple domain
if (/Key expires|NSURLErrorDomain.+?-1012/.test(e.error || e.message || e)) {
if ($.EnableCache && r.body.startsWith('{')) {
const cacheList = JSON.parse($.read('#TESTFLIGHT-ACCOUNT-CACHE') || '{}');
cacheList[o] = r.body;
$.write($.stringify(cacheList), '#TESTFLIGHT-ACCOUNT-CACHE');
$.log(`Account "${o}" write app list to cache`)
}
}).catch(e => {
if ((e.error || e.message || e).includes('Key expires')) {
if (list[o].InvalidKey >= 2) { //prevent misjudgment
delete list[o];
} else {
Expand All @@ -194,6 +204,18 @@ function QueryAppList(o) {
$.notify('TestFlight Account', '', `Account ID "${o}" ${e}`);
};
$.error(`Account "${o}" response failed: ${e.error || e.message || e}`);
if ($.EnableCache && !(e.error || e.message || e).includes('Key expires')) {
resp.data = JSON.parse($.read('#TESTFLIGHT-ACCOUNT-CACHE') || '{}')[o];
$.error(`Account "${o}" Try using cache app list. Status: ${Boolean(resp.data)}`);
}
}).finally(() => {
if (resp.data) {
const res = JSON.parse(resp.data);
$.log(`Account "${o}" app list: ${$.stringify((res.data || []).map(i => i.name))}`);
return (res.data || [])
.filter(i => (i.aid = o, !list[o].only || list[o].only.includes(String(i.appAdamId))))
.map(p => arr[resp.main ? 'unshift' : 'push'](p))
}
})
}

Expand Down Expand Up @@ -290,4 +312,4 @@ function letterDecode(e) {
}

// https://github.com/Peng-YM/QuanX/tree/master/Tools/OpenAPI
function ENV() { const e = "function" == typeof require && "undefined" != typeof $jsbox; return { isQX: "undefined" != typeof $task, isLoon: "undefined" != typeof $loon, isSurge: "undefined" != typeof $httpClient && "undefined" == typeof $loon, isShadowrocket: "undefined" != typeof $Shadowrocket, isBrowser: "undefined" != typeof document, isNode: "function" == typeof require && !e, isJSBox: e, isRequest: "undefined" != typeof $request, isScriptable: "undefined" != typeof importModule } } function HTTP() { const { isQX: t, isLoon: s, isSurge: o, isScriptable: n, isNode: i, isBrowser: r } = ENV(), u = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; const a = {}; return ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"].forEach(h => a[h.toLowerCase()] = (a => (function (a, h) { h = "string" == typeof h ? { url: h } : h; h.timeout && s && (h.timeout = h.timeout * 1000); let f, p; if (t) f = $task.fetch({ method: a, ...h }); else if (s || o || i) f = new Promise((e, t) => { (i ? require("request") : $httpClient)[a.toLowerCase()](h, (s, o, n) => { s ? t(s) : e({ statusCode: o.status || o.statusCode, headers: o.headers, body: n }) }) }); else if (n) { const e = new Request(h.url); e.method = a, e.headers = h.headers, e.body = h.body, f = new Promise((t, s) => { e.loadString().then(s => { t({ statusCode: e.response.statusCode, headers: e.response.headers, body: s }) }).catch(e => s(e)) }) } else r && (f = new Promise((e, t) => { fetch(h.url, { method: a, headers: h.headers, body: h.body }).then(e => e.json()).then(t => e({ statusCode: t.status, headers: t.headers, body: t.data })).catch(t) })); return f })(h, a))), a } function API(e = "untitled", t = !1) { const { isQX: s, isLoon: o, isSurge: n, isNode: i, isJSBox: r, isScriptable: u } = ENV(); return new class { constructor(e, t) { this.name = e, this.debug = t, this.http = HTTP(), this.env = ENV(), this.node = (() => { if (i) { return { fs: require("fs") } } return null })(), this.initCache(); Promise.prototype.delay = function (e) { return this.then(function (t) { return ((e, t) => new Promise(function (s) { setTimeout(s.bind(null, t), e) }))(e, t) }) } } initCache() { if (s && (this.cache = JSON.parse($prefs.valueForKey(this.name) || "{}")), (o || n) && (this.cache = JSON.parse($persistentStore.read(this.name) || "{}")), i) { let e = "root.json"; this.node.fs.existsSync(e) || this.node.fs.writeFileSync(e, JSON.stringify({}), { flag: "wx" }, e => console.log(e)), this.root = {}, e = `${this.name}.json`, this.node.fs.existsSync(e) ? this.cache = JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)) : (this.node.fs.writeFileSync(e, JSON.stringify({}), { flag: "wx" }, e => console.log(e)), this.cache = {}) } } persistCache() { const e = JSON.stringify(this.cache, null, 2); s && $prefs.setValueForKey(e, this.name), (o || n) && $persistentStore.write(e, this.name), i && (this.node.fs.writeFileSync(`${this.name}.json`, e, { flag: "w" }, e => console.log(e)), this.node.fs.writeFileSync("root.json", JSON.stringify(this.root, null, 2), { flag: "w" }, e => console.log(e))) } write(e, t) { if (this.log(`SET ${t}`), -1 !== t.indexOf("#")) { if (t = t.substr(1), n || o) return $persistentStore.write(e, t); if (s) return $prefs.setValueForKey(e, t); i && (this.root[t] = e) } else this.cache[t] = e; this.persistCache() } read(e) { return this.log(`READ ${e}`), -1 === e.indexOf("#") ? this.cache[e] : (e = e.substr(1), n || o ? $persistentStore.read(e) : s ? $prefs.valueForKey(e) : i ? this.root[e] : void 0) } delete(e) { if (this.log(`DELETE ${e}`), -1 !== e.indexOf("#")) { if (e = e.substr(1), n || o) return $persistentStore.write(null, e); if (s) return $prefs.removeValueForKey(e); i && delete this.root[e] } else delete this.cache[e]; this.persistCache() } notify(e, t = "", a = "", h = {}) { const d = h["open-url"], l = h["media-url"]; if (s && $notify(e, t, a, h), n && $notification.post(e, t, a + `${l ? "\n多媒体:" + l : ""}`, { url: d }), o) { let s = {}; d && (s.openUrl = d), l && (s.mediaUrl = l), "{}" === JSON.stringify(s) ? $notification.post(e, t, a) : $notification.post(e, t, a, s) } if (i || u) { const s = a + (d ? `\n点击跳转: ${d}` : "") + (l ? `\n多媒体: ${l}` : ""); if (r) { require("push").schedule({ title: e, body: (t ? t + "\n" : "") + s }) } else console.log(`${e}\n${t}\n${s}\n\n`) } } log(e) { this.debug && console.log(`[${this.name}] LOG: ${this.stringify(e)}`) } info(e) { console.log(`[${this.name}] INFO: ${this.stringify(e)}`) } error(e) { console.log(`[${this.name}] ERROR: ${this.stringify(e)}`) } wait(e) { return new Promise(t => setTimeout(t, e)) } done(e = {}) { s || o || n ? $done(e) : i && !r && "undefined" != typeof $context && ($context.headers = e.headers, $context.statusCode = e.statusCode, $context.body = e.body) } stringify(e) { if ("string" == typeof e || e instanceof String) return e; try { return JSON.stringify(e, null, 2) } catch (e) { return "[object Object]" } } }(e, t) }
function ENV() { const e = "function" == typeof require && "undefined" != typeof $jsbox; return { isQX: "undefined" != typeof $task, isLoon: "undefined" != typeof $loon, isSurge: "undefined" != typeof $httpClient && "undefined" == typeof $loon, isShadowrocket: "undefined" != typeof $Shadowrocket, isBrowser: "undefined" != typeof document, isNode: "function" == typeof require && !e, isJSBox: e, isRequest: "undefined" != typeof $request, isScriptable: "undefined" != typeof importModule, } } function HTTP(e = { baseURL: "" }) { const { isQX: t, isLoon: s, isSurge: o, isScriptable: n, isNode: i, isBrowser: r, } = ENV(), u = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/; const a = {}; return (["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"].forEach((h) => (a[h.toLowerCase()] = (a) => (function (a, h) { h = "string" == typeof h ? { url: h } : h; const d = e.baseURL; d && !u.test(h.url || "") && (h.url = d ? d + h.url : h.url), h.body && h.headers; h.timeout && s && (h.timeout = h.timeout * 1000); const l = (h = { ...e, ...h }).timeout, c = { onRequest: () => { }, onResponse: (e) => e, onTimeout: () => { }, ...h.events, }; let f, p; if ((c.onRequest(a, h), t)) f = $task.fetch({ method: a, ...h }); else if (s || o || i) f = new Promise((e, t) => { (i ? require("request") : $httpClient)[a.toLowerCase()](h, (s, o, n) => { s ? t(s) : e({ statusCode: o.status || o.statusCode, headers: o.headers, body: n, }) },) }); else if (n) { const e = new Request(h.url); (e.method = a), (e.headers = h.headers), (e.body = h.body), (f = new Promise((t, s) => { e.loadString().then((s) => { t({ statusCode: e.response.statusCode, headers: e.response.headers, body: s, }) }).catch((e) => s(e)) })) } else r && (f = new Promise((e, t) => { fetch(h.url, { method: a, headers: h.headers, body: h.body }).then((e) => e.json()).then((t) => e({ statusCode: t.status, headers: t.headers, body: t.data, }),).catch(t) })); const y = l ? new Promise((e, t) => { p = setTimeout(() => (c.onTimeout(), t(`timeout`)), !s ? l * 1000 : l,) }) : null; return (y ? Promise.race([y, f]).then((e) => ((typeof clearTimeout !== 'undefined') && clearTimeout(p), e)) : f).then((e) => c.onResponse(e)) })(h, a)),), a) } function API(e = "untitled", t = !1) { const { isQX: s, isLoon: o, isSurge: n, isNode: i, isJSBox: r, isScriptable: u, } = ENV(); return new (class { constructor(e, t) { (this.name = e), (this.debug = t), (this.http = HTTP()), (this.env = ENV()), (this.node = (() => { if (i) { return { fs: require("fs") } } return null })()), this.initCache(); Promise.prototype.delay = function (e) { return this.then(function (t) { return ((e, t) => new Promise(function (s) { setTimeout(s.bind(null, t), e) }))(e, t) }) } } initCache() { if ((s && (this.cache = JSON.parse($prefs.valueForKey(this.name) || "{}")), (o || n) && (this.cache = JSON.parse($persistentStore.read(this.name) || "{}")), i)) { let e = "root.json"; this.node.fs.existsSync(e) || this.node.fs.writeFileSync(e, JSON.stringify({}), { flag: "wx" }, (e) => console.log(e),), (this.root = {}), (e = `${this.name}.json`), this.node.fs.existsSync(e) ? (this.cache = JSON.parse(this.node.fs.readFileSync(`${this.name}.json`),)) : (this.node.fs.writeFileSync(e, JSON.stringify({}), { flag: "wx" }, (e) => console.log(e),), (this.cache = {})) } } persistCache() { const e = JSON.stringify(this.cache, null, 2); s && $prefs.setValueForKey(e, this.name), (o || n) && $persistentStore.write(e, this.name), i && (this.node.fs.writeFileSync(`${this.name}.json`, e, { flag: "w" }, (e) => console.log(e),), this.node.fs.writeFileSync("root.json", JSON.stringify(this.root, null, 2), { flag: "w" }, (e) => console.log(e),)) } write(e, t) { if ((this.log(`SET ${t}`), -1 !== t.indexOf("#"))) { if (((t = t.substr(1)), n || o)) return $persistentStore.write(e, t); if (s) return $prefs.setValueForKey(e, t); i && (this.root[t] = e) } else this.cache[t] = e; this.persistCache() } read(e) { return (this.log(`READ ${e}`), -1 === e.indexOf("#") ? this.cache[e] : ((e = e.substr(1)), n || o ? $persistentStore.read(e) : s ? $prefs.valueForKey(e) : i ? this.root[e] : void 0)) } delete(e) { if ((this.log(`DELETE ${e}`), -1 !== e.indexOf("#"))) { if (((e = e.substr(1)), n || o)) return $persistentStore.write(null, e); if (s) return $prefs.removeValueForKey(e); i && delete this.root[e] } else delete this.cache[e]; this.persistCache() } notify(e, t = "", a = "", h = {}) { const d = h["open-url"], l = h["media-url"]; if ((s && $notify(e, t, a, h), n && $notification.post(e, t, a + `${l ? "\n多媒体:" + l : ""}`, { url: d, }), o)) { let s = {}; d && (s.openUrl = d), l && (s.mediaUrl = l), "{}" === JSON.stringify(s) ? $notification.post(e, t, a) : $notification.post(e, t, a, s) } if (i || u) { const s = a + (d ? `\n点击跳转:${d}` : "") + (l ? `\n多媒体:${l}` : ""); if (r) { require("push").schedule({ title: e, body: (t ? t + "\n" : "") + s }) } else console.log(`${e}\n${t}\n${s}\n\n`) } } log(e) { this.debug && console.log(`[${this.name}]LOG:${this.stringify(e)}`) } info(e) { console.log(`[${this.name}]INFO:${this.stringify(e)}`) } error(e) { console.log(`[${this.name}]ERROR:${this.stringify(e)}`) } wait(e) { return new Promise((t) => setTimeout(t, e)) } done(e = {}) { s || o || n ? $done(e) : i && !r && "undefined" != typeof $context && (($context.headers = e.headers), ($context.statusCode = e.statusCode), ($context.body = e.body)) } stringify(e) { if ("string" == typeof e || e instanceof String) return e; try { return JSON.stringify(e, null, 2) } catch (e) { return "[object Object]" } } })(e, t) }