Clash/script/testflight/testflight.js
2022-12-31 22:02:33 +08:00

710 lines
63 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 tfStorefrontId = "tf_storefront_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 headers["If-None-Match"];
} else {
newHeaders = $.http.convertHeadersToLowerCase(newHeaders);
delete headers["if-none-match"];
}
$.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 = $.data.read(tfStorefrontId, "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)
try {
if (syncQingLong === true) {
let qlSessionInfo = await $.qinglong.read(tfSessionInfoKey, {}, accountId);
if (qlSessionInfo && qlSessionInfo["x-request-id"] === sessionInfo["x-request-id"] && qlSessionInfo["valid"] === true) {
await $.qinglong.write(tfSessionInfoKey, sessionInfo, accountId);
$.notification.post(`已同步将青龙面板中AccountId ${accountId}设置为失效`);
}
}
} catch (err) {
$.logger.error(`将青龙面板中AccountId ${accountId}设置为失效时出现异常\n${err}`);
}
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) {
try {
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 || qlSessionInfo["x-session-id"] !== currentSessionInfo["x-session-id"] || qlSessionInfo["valid"] === false) {
$.logger.info(`符合同步条件已将当前Session同步至青龙面板`);
batchWriteData.push([tfSessionInfoKey, currentSessionInfo, currentAccountId]);
} else {
$.logger.info(`不符合同步条件无需同步当前Session至青龙面板`);
}
if (!qlSessionInfo || 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 => 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 同步到本地`);
}
}
}
} 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请检查青龙面板配置!`);
}
} catch (error) {
$.logger.error(`同步数据至青龙面板出现异常\n${error}`);
$.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(scriptName="MagicJS",logLevel="INFO"){const MagicEnvironment=()=>{const isLoon=typeof $loon!=="undefined";const isQuanX=typeof $task!=="undefined";const isNode=typeof module!=="undefined";const isSurge=typeof $httpClient!=="undefined"&&!isLoon;const isStorm=typeof $storm!=="undefined";const isStash=typeof $environment!=="undefined"&&typeof $environment["stash-build"]!=="undefined";const isSurgeLike=isSurge||isLoon||isStorm||isStash;const isScriptable=typeof importModule!=="undefined";return{isLoon:isLoon,isQuanX:isQuanX,isNode:isNode,isSurge:isSurge,isStorm:isStorm,isStash:isStash,isSurgeLike:isSurgeLike,isScriptable:isScriptable,get name(){if(isLoon){return"Loon"}else if(isQuanX){return"QuantumultX"}else if(isNode){return"NodeJS"}else if(isSurge){return"Surge"}else if(isScriptable){return"Scriptable"}else{return"unknown"}},get build(){if(isSurge){return $environment["surge-build"]}else if(isStash){return $environment["stash-build"]}else if(isStorm){return $storm.buildVersion}},get language(){if(isSurge||isStash){return $environment["language"]}},get version(){if(isSurge){return $environment["surge-version"]}else if(isStash){return $environment["stash-version"]}else if(isStorm){return $storm.appVersion}else if(isNode){return process.version}},get system(){if(isSurge){return $environment["system"]}else if(isNode){return process.platform}},get systemVersion(){if(isStorm){return $storm.systemVersion}},get deviceName(){if(isStorm){return $storm.deviceName}}}};const MagicLogger=(scriptName,logLevel="INFO")=>{let _level=logLevel;const logLevels={SNIFFER:6,DEBUG:5,INFO:4,NOTIFY:3,WARNING:2,ERROR:1,CRITICAL:0,NONE:-1};const logEmoji={SNIFFER:"",DEBUG:"",INFO:"",NOTIFY:"",WARNING:"❗ ",ERROR:"❌ ",CRITICAL:"❌ ",NONE:""};const _log=(msg,level="INFO")=>{if(!(logLevels[_level]<logLevels[level.toUpperCase()]))console.log(`[${level}] [${scriptName}]\n${logEmoji[level.toUpperCase()]}${msg}\n`)};const setLevel=logLevel=>{_level=logLevel};return{getLevel:()=>{return _level},setLevel:setLevel,sniffer:msg=>{_log(msg,"SNIFFER")},debug:msg=>{_log(msg,"DEBUG")},info:msg=>{_log(msg,"INFO")},notify:msg=>{_log(msg,"NOTIFY")},warning:msg=>{_log(msg,"WARNING")},error:msg=>{_log(msg,"ERROR")},retry:msg=>{_log(msg,"RETRY")}}};return new class{constructor(scriptName,logLevel){this._startTime=Date.now();this.version="3.0.0";this.scriptName=scriptName;this.env=MagicEnvironment();this.logger=MagicLogger(scriptName,logLevel);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,this.http):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 magicLoglevel=this.data.read("magic_loglevel");const barkUrl=this.data.read("magic_bark_url");if(magicLoglevel){this.logger.setLevel(magicLoglevel.toUpperCase())}if(barkUrl){this.notification.setBark(barkUrl)}}}get isRequest(){return typeof $request!=="undefined"&&typeof $response==="undefined"}get isResponse(){return typeof $response!=="undefined"}get isDebug(){return this.logger.level==="DEBUG"}get request(){return typeof $request!=="undefined"?$request:undefined}get response(){if(typeof $response!=="undefined"){if($response.hasOwnProperty("status"))$response["statusCode"]=$response["status"];if($response.hasOwnProperty("statusCode"))$response["status"]=$response["statusCode"];return $response}else{return undefined}}done=(value={})=>{this._endTime=Date.now();let span=(this._endTime-this._startTime)/1e3;this.logger.info(`SCRIPT COMPLETED: ${span} S.`);if(typeof $done!=="undefined"){$done(value)}}}(scriptName,logLevel)}
function MagicHttp(env,logger){const phoneUA="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 computerUA="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 axiosInstance;if(env.isNode){const axios=require("axios");axiosInstance=axios.create()}class InterceptorManager{constructor(isRequest=true){this.handlers=[];this.isRequest=isRequest}use(fulfilled,rejected,options){if(typeof fulfilled==="function"){logger.debug(`Register fulfilled ${fulfilled.name}`)}if(typeof rejected==="function"){logger.debug(`Register rejected ${rejected.name}`)}this.handlers.push({fulfilled:fulfilled,rejected:rejected,synchronous:options&&typeof options.synchronous==="boolean"?options.synchronous:false,runWhen:options?options.runWhen:null});return this.handlers.length-1}eject(id){if(this.handlers[id]){this.handlers[id]=null}}forEach(fn){this.handlers.forEach(element=>{if(element!==null){fn(element)}})}}function paramsToQueryString(config){let _config={...config};if(!!_config.params){if(!env.isNode){let qs=Object.keys(_config.params).map(key=>{const encodeKey=encodeURIComponent(key);_config.url=_config.url.replace(new RegExp(`${key}=[^&]*`,"ig"),"");_config.url=_config.url.replace(new RegExp(`${encodeKey}=[^&]*`,"ig"),"");return`${encodeKey}=${encodeURIComponent(_config.params[key])}`}).join("&");if(_config.url.indexOf("?")<0)_config.url+="?";if(!/(&|\?)$/g.test(_config.url)){_config.url+="&"}_config.url+=qs;delete _config.params;logger.debug(`Params to QueryString: ${_config.url}`)}}return _config}const mergeConfig=(method,configOrUrl)=>{let config=typeof configOrUrl==="object"?{headers:{},...configOrUrl}:{url:configOrUrl,headers:{}};if(!config.method){config["method"]=method}config=paramsToQueryString(config);if(config["rewrite"]===true){if(env.isSurge){config.headers["X-Surge-Skip-Scripting"]=false;delete config["rewrite"]}else if(env.isQuanX){config["hints"]=false;delete config["rewrite"]}}if(env.isSurgeLike){const contentType=config.headers["content-type"]||config.headers["Content-Type"];if(config["method"]!=="GET"&&contentType&&contentType.indexOf("application/json")>=0&&config.body instanceof Array){config.body=JSON.stringify(config.body);logger.debug(`Convert Array object to String: ${config.body}`)}}else if(env.isQuanX){if(config.hasOwnProperty("body")&&typeof config["body"]!=="string")config["body"]=JSON.stringify(config["body"]);config["method"]=method}else if(env.isNode){if(method==="POST"||method==="PUT"||method==="PATCH"||method==="DELETE"){config.data=config.data||config.body}else if(method==="GET"){config.params=config.params||config.body}delete config.body}return config};const modifyResponse=(resp,config=null)=>{if(resp){let _resp={...resp,config:resp.config||config,status:resp.statusCode||resp.status,body:resp.body||resp.data,headers:resp.headers||resp.header};if(typeof _resp.body==="string"){try{_resp.body=JSON.parse(_resp.body)}catch{}}delete _resp.data;return _resp}else{return resp}};const convertHeadersToLowerCase=headers=>{return Object.keys(headers).reduce((acc,key)=>{acc[key.toLowerCase()]=headers[key];return acc},{})};const convertHeadersToCamelCase=headers=>{return Object.keys(headers).reduce((acc,key)=>{const newKey=key.split("-").map(word=>word[0].toUpperCase()+word.slice(1)).join("-");acc[newKey]=headers[key];return acc},{})};const raiseExceptionByStatusCode=(resp,config=null)=>{if(!!resp&&resp.status>=400){logger.debug(`Raise exception when status code is ${resp.status}`);return{name:"RequestException",message:`Request failed with status code ${resp.status}`,config:config||resp.config,response:resp}}};const interceptors={request:new InterceptorManager,response:new InterceptorManager(false)};let requestInterceptorChain=[];let responseInterceptorChain=[];let synchronousRequestInterceptors=true;function interceptConfig(config){config=paramsToQueryString(config);logger.debug(`HTTP ${config["method"].toUpperCase()}:\n${JSON.stringify(config)}`);return config}function interceptResponse(resp){try{resp=!!resp?modifyResponse(resp):resp;logger.sniffer(`HTTP ${resp.config["method"].toUpperCase()}:\n${JSON.stringify(resp.config)}\nSTATUS CODE:\n${resp.status}\nRESPONSE:\n${typeof resp.body==="object"?JSON.stringify(resp.body):resp.body}`);const err=raiseExceptionByStatusCode(resp);if(!!err){return Promise.reject(err)}return resp}catch(err){logger.error(err);return resp}}const registerInterceptors=config=>{try{requestInterceptorChain=[];responseInterceptorChain=[];interceptors.request.forEach(interceptor=>{if(typeof interceptor.runWhen==="function"&&interceptor.runWhen(config)===false){return}synchronousRequestInterceptors=synchronousRequestInterceptors&&interceptor.synchronous;requestInterceptorChain.unshift(interceptor.fulfilled,interceptor.rejected)});interceptors.response.forEach(interceptor=>{responseInterceptorChain.push(interceptor.fulfilled,interceptor.rejected)})}catch(err){logger.error(`Failed to register interceptors: ${err}.`)}};const request=(method,config)=>{let dispatchRequest;const _method=method.toUpperCase();config=mergeConfig(_method,config);if(env.isNode){dispatchRequest=axiosInstance}else{if(env.isSurgeLike){dispatchRequest=config=>{return new Promise((resolve,reject)=>{$httpClient[method.toLowerCase()](config,(err,resp,body)=>{if(err){let newErr={name:err.name||err,message:err.message||err,stack:err.stack||err,config:config,response:modifyResponse(resp)};reject(newErr)}else{resp.config=config;resp.body=body;resolve(resp)}})})}}else{dispatchRequest=config=>{return new Promise((resolve,reject)=>{$task.fetch(config).then(resp=>{resp=modifyResponse(resp,config);const err=raiseExceptionByStatusCode(resp,config);if(err){return Promise.reject(err)}resolve(resp)}).catch(err=>{let newErr={name:err.message||err.error,message:err.message||err.error,stack:err.error,config:config,response:!!err.response?modifyResponse(err.response):null};reject(newErr)})})}}}let promise;registerInterceptors(config);const defaultRequestInterceptors=[interceptConfig,undefined];const defaultResponseInterceptors=[interceptResponse,undefined];if(!synchronousRequestInterceptors){logger.debug("Interceptors are executed in asynchronous mode");let chain=[dispatchRequest,undefined];Array.prototype.unshift.apply(chain,defaultRequestInterceptors);Array.prototype.unshift.apply(chain,requestInterceptorChain);chain=chain.concat(defaultResponseInterceptors);chain=chain.concat(responseInterceptorChain);promise=Promise.resolve(config);while(chain.length){try{let onFulfilled=chain.shift();let onRejected=chain.shift();if(!env.isNode&&config["timeout"]&&onFulfilled===dispatchRequest){onFulfilled=requestTimeout}if(typeof onFulfilled==="function"){logger.debug(`Executing request fulfilled ${onFulfilled.name}`)}if(typeof onRejected==="function"){logger.debug(`Executing request rejected ${onRejected.name}`)}promise=promise.then(onFulfilled,onRejected)}catch(err){logger.error(`request exception: ${err}`)}}return promise}else{logger.debug("Interceptors are executed in synchronous mode");Array.prototype.unshift.apply(requestInterceptorChain,defaultRequestInterceptors);requestInterceptorChain=requestInterceptorChain.concat([interceptConfig,undefined]);while(requestInterceptorChain.length){let onFulfilled=requestInterceptorChain.shift();let onRejected=requestInterceptorChain.shift();try{if(typeof onFulfilled==="function"){logger.debug(`Executing request fulfilled ${onFulfilled.name}`)}config=onFulfilled(config)}catch(error){if(typeof onRejected==="function"){logger.debug(`Executing request rejected ${onRejected.name}`)}onRejected(error);break}}try{if(!env.isNode&&config["timeout"]){promise=requestTimeout(config)}else{promise=dispatchRequest(config)}}catch(err){return Promise.reject(err)}Array.prototype.unshift.apply(responseInterceptorChain,defaultResponseInterceptors);while(responseInterceptorChain.length){promise=promise.then(responseInterceptorChain.shift(),responseInterceptorChain.shift())}return promise}function requestTimeout(config){try{const timer=new Promise((_,reject)=>{setTimeout(()=>{let err={message:`timeout of ${config["timeout"]}ms exceeded.`,config:config};reject(err)},config["timeout"])});return Promise.race([dispatchRequest(config),timer])}catch(err){logger.error(`Request Timeout exception: ${err}.`)}}};return{request:request,interceptors:interceptors,convertHeadersToLowerCase:convertHeadersToLowerCase,convertHeadersToCamelCase:convertHeadersToCamelCase,modifyResponse:modifyResponse,get:configOrUrl=>{return request("GET",configOrUrl)},post:configOrUrl=>{return request("POST",configOrUrl)},put:configOrUrl=>{return request("PUT",configOrUrl)},patch:configOrUrl=>{return request("PATCH",configOrUrl)},delete:configOrUrl=>{return request("DELETE",configOrUrl)},head:configOrUrl=>{return request("HEAD",configOrUrl)},options:configOrUrl=>{return request("OPTIONS",configOrUrl)}}}
function MagicData(env,logger){let node={fs:undefined,data:{}};if(env.isNode){node.fs=require("fs");try{node.fs.accessSync("./magic.json",node.fs.constants.R_OK|node.fs.constants.W_OK)}catch(err){node.fs.writeFileSync("./magic.json","{}",{encoding:"utf8"})}node.data=require("./magic.json")}const defaultValueComparator=(oldVal,newVal)=>{if(typeof newVal==="object"){return false}else{return oldVal===newVal}};const _typeConvertor=val=>{if(val==="true"){return true}else if(val==="false"){return false}else if(typeof val==="undefined"){return null}else{return val}};const _valConvertor=(val,default_,session,read_no_session)=>{if(session){try{if(typeof val==="string")val=JSON.parse(val);if(val["magic_session"]===true){val=val[session]}else{val=null}}catch{val=null}}if(typeof val==="string"&&val!=="null"){try{val=JSON.parse(val)}catch{}}if(read_no_session===false&&!!val&&val["magic_session"]===true){val=null}if((val===null||typeof val==="undefined")&&default_!==null&&typeof default_!=="undefined"){val=default_}val=_typeConvertor(val);return val};const convertToObject=obj=>{if(typeof obj==="string"){let data={};try{data=JSON.parse(obj);const type=typeof data;if(type!=="object"||data instanceof Array||type==="bool"||data===null){data={}}}catch{}return data}else if(obj instanceof Array||obj===null||typeof obj==="undefined"||obj!==obj||typeof obj==="boolean"){return{}}else{return obj}};const readForNode=(key,default_=null,session="",read_no_session=false,externalData=null)=>{let data=externalData||node.data;if(!!data&&typeof data[key]!=="undefined"&&data[key]!==null){val=data[key]}else{val=!!session?{}:null}val=_valConvertor(val,default_,session,read_no_session);return val};const read=(key,default_=null,session="",read_no_session=false,externalData=null)=>{let val="";if(externalData||env.isNode){val=readForNode(key,default_,session,read_no_session,externalData)}else{if(env.isSurgeLike){val=$persistentStore.read(key)}else if(env.isQuanX){val=$prefs.valueForKey(key)}val=_valConvertor(val,default_,session,read_no_session)}logger.debug(`READ DATA [${key}]${!!session?`[${session}]`:""} <${typeof val}>\n${JSON.stringify(val)}`);return val};const writeForNode=(key,val,session="",externalData=null)=>{let data=externalData||node.data;data=convertToObject(data);if(!!session){let obj=convertToObject(data[key]);obj["magic_session"]=true;obj[session]=val;data[key]=obj}else{data[key]=val}if(externalData!==null){externalData=data}return data};const write=(key,val,session="",externalData=null)=>{if(typeof val==="undefined"||val!==val){return false}if(!env.isNode&&(typeof val==="boolean"||typeof val==="number")){val=String(val)}let data="";if(externalData||env.isNode){data=writeForNode(key,val,session,externalData)}else{if(!session){data=val}else{if(env.isSurgeLike){data=!!$persistentStore.read(key)?$persistentStore.read(key):data}else if(env.isQuanX){data=!!$prefs.valueForKey(key)?$prefs.valueForKey(key):data}data=convertToObject(data);data["magic_session"]=true;data[session]=val}}if(!!data&&typeof data==="object"){data=JSON.stringify(data,null,4)}logger.debug(`WRITE DATA [${key}]${session?`[${session}]`:""} <${typeof val}>\n${JSON.stringify(val)}`);if(!externalData){if(env.isSurgeLike){return $persistentStore.write(data,key)}else if(env.isQuanX){return $prefs.setValueForKey(data,key)}else if(env.isNode){try{node.fs.writeFileSync("./magic.json",data);return true}catch(err){logger.error(err);return false}}}return true};const update=(key,val,session,comparator=defaultValueComparator,externalData=null)=>{val=_typeConvertor(val);const oldValue=read(key,null,session,false,externalData);if(comparator(oldValue,val)===true){return false}else{const result=write(key,val,session,externalData);let newVal=read(key,null,session,false,externalData);if(comparator===defaultValueComparator&&typeof newVal==="object"){return result}return comparator(val,newVal)}};const delForNode=(key,session,externalData)=>{let data=externalData||node.data;data=convertToObject(data);if(!!session){obj=convertToObject(data[key]);delete obj[session];data[key]=obj}else{delete data[key]}if(!!externalData){externalData=data}return data};const del=(key,session="",externalData=null)=>{let data={};if(externalData||env.isNode){data=delForNode(key,session,externalData);if(!externalData){node.fs.writeFileSync("./magic.json",JSON.stringify(data,null,4))}else{externalData=data}}else{if(!session){if(env.isStorm){return $persistentStore.remove(key)}else if(env.isSurgeLike){return $persistentStore.write(null,key)}else if(env.isQuanX){return $prefs.removeValueForKey(key)}}else{if(env.isSurgeLike){data=$persistentStore.read(key)}else if(env.isQuanX){data=$prefs.valueForKey(key)}data=convertToObject(data);delete data[session];const json=JSON.stringify(data,null,4);write(key,json)}}logger.debug(`DELETE KEY [${key}]${!!session?`[${session}]`:""}`)};const allSessionNames=(key,externalData=null)=>{let _sessions=[];let data=read(key,null,null,true,externalData);data=convertToObject(data);if(data["magic_session"]!==true){_sessions=[]}else{_sessions=Object.keys(data).filter(key=>key!=="magic_session")}logger.debug(`READ ALL SESSIONS [${key}] <${typeof _sessions}>\n${JSON.stringify(_sessions,null,4)}`);return _sessions};const allSessions=(key,externalData=null)=>{let _sessions={};let data=read(key,null,null,true,externalData);data=convertToObject(data);if(data["magic_session"]===true){_sessions={...data};delete _sessions["magic_session"]}logger.debug(`READ ALL SESSIONS [${key}] <${typeof _sessions}>\n${JSON.stringify(_sessions,null,4)}`);return _sessions};return{read:read,write:write,del:del,update:update,allSessions:allSessions,allSessionNames:allSessionNames,defaultValueComparator:defaultValueComparator,convertToObject:convertToObject}}
function MagicNotification(scriptName,env,logger,http){let _barkUrl=null;let _barkKey=null;const setBark=url=>{try{let _url=url.replace(/\/+$/g,"");_barkUrl=`${/^https?:\/\/([^/]*)/.exec(_url)[0]}/push`;_barkKey=/\/([^\/]+)\/?$/.exec(_url)[1]}catch(ex){logger.error(`Bark url error: ${ex}.`)}};function post(title=scriptName,subTitle="",body="",opts=""){const _adaptOpts=_opts=>{try{let newOpts={};if(typeof _opts==="string"){if(env.isLoon)newOpts={openUrl:_opts};else if(env.isQuanX)newOpts={"open-url":_opts};else if(env.isSurge)newOpts={url:_opts}}else if(typeof _opts==="object"){if(env.isLoon){newOpts["openUrl"]=!!_opts["open-url"]?_opts["open-url"]:"";newOpts["mediaUrl"]=!!_opts["media-url"]?_opts["media-url"]:""}else if(env.isQuanX){newOpts=!!_opts["open-url"]||!!_opts["media-url"]?_opts:{}}else if(env.isSurge){let openUrl=_opts["open-url"]||_opts["openUrl"];newOpts=openUrl?{url:openUrl}:{}}}return newOpts}catch(err){logger.error(`通知选项转换失败${err}`)}return _opts};opts=_adaptOpts(opts);if(arguments.length===1){title=scriptName;subTitle="",body=arguments[0]}logger.notify(`title:${title}\nsubTitle:${subTitle}\nbody:${body}\noptions:${typeof opts==="object"?JSON.stringify(opts):opts}`);if(env.isSurge){$notification.post(title,subTitle,body,opts)}else if(env.isLoon){if(!!opts)$notification.post(title,subTitle,body,opts);else $notification.post(title,subTitle,body)}else if(env.isQuanX){$notify(title,subTitle,body,opts)}if(_barkUrl&&_barkKey){bark(title,subTitle,body)}}function debug(title=scriptName,subTitle="",body="",opts=""){if(logger.getLevel()==="DEBUG"){if(arguments.length===1){title=scriptName;subTitle="";body=arguments[0]}this.post(title,subTitle,body,opts)}}function bark(title=scriptName,subTitle="",body="",opts=""){if(typeof http==="undefined"||typeof http.post==="undefined"){throw"Bark notification needs to import MagicHttp module."}let options={url:_barkUrl,headers:{"content-type":"application/json; charset=utf-8"},body:{title:title,body:subTitle?`${subTitle}\n${body}`:body,device_key:_barkKey}};http.post(options).catch(ex=>{logger.error(`Bark notify error: ${ex}`)})}return{post:post,debug:debug,bark:bark,setBark:setBark}}
function MagicUtils(env,logger){const retry=(fn,retries=5,interval=0,callback=null)=>{return(...args)=>{return new Promise((resolve,reject)=>{function _retry(...args){Promise.resolve().then(()=>fn.apply(this,args)).then(result=>{if(typeof callback==="function"){Promise.resolve().then(()=>callback(result)).then(()=>{resolve(result)}).catch(ex=>{if(retries>=1){if(interval>0)setTimeout(()=>_retry.apply(this,args),interval);else _retry.apply(this,args)}else{reject(ex)}retries--})}else{resolve(result)}}).catch(ex=>{logger.error(ex);if(retries>=1&&interval>0){setTimeout(()=>_retry.apply(this,args),interval)}else if(retries>=1){_retry.apply(this,args)}else{reject(ex)}retries--})}_retry.apply(this,args)})}};const formatTime=(time,fmt="yyyy-MM-dd hh:mm:ss")=>{let o={"M+":time.getMonth()+1,"d+":time.getDate(),"h+":time.getHours(),"m+":time.getMinutes(),"s+":time.getSeconds(),"q+":Math.floor((time.getMonth()+3)/3),S:time.getMilliseconds()};if(/(y+)/.test(fmt))fmt=fmt.replace(RegExp.$1,(time.getFullYear()+"").substr(4-RegExp.$1.length));for(let k in o)if(new RegExp("("+k+")").test(fmt))fmt=fmt.replace(RegExp.$1,RegExp.$1.length===1?o[k]:("00"+o[k]).substr((""+o[k]).length));return fmt};const now=()=>{return formatTime(new Date,"yyyy-MM-dd hh:mm:ss")};const today=()=>{return formatTime(new Date,"yyyy-MM-dd")};const sleep=time=>{return new Promise(resolve=>setTimeout(resolve,time))};const assert=(val,msg=null)=>{if(env.isNode){const _assert=require("assert");if(msg)_assert(val,msg);else _assert(val)}else{if(val!==true){let err=`AssertionError: ${msg||"The expression evaluated to a falsy value."}`;logger.error(err)}}};return{retry:retry,formatTime:formatTime,now:now,today:today,sleep:sleep,assert:assert}}
function MagicQingLong(env,data,logger){let qlUrl="";let qlName="";let qlClient="";let qlSecret="";let qlPwd="";let qlToken="";const magicJsonFileName="magic.json";const timeout=3e3;const http=(()=>MagicHttp(env,logger))();const init=(url,clientId,clientSecret,username,password)=>{qlUrl=url;qlClient=clientId;qlSecret=clientSecret;qlName=username;qlPwd=password};function readQingLongConfig(config){qlUrl=qlUrl||data.read("magic_qlurl");qlToken=qlToken||data.read("magic_qltoken");logger.debug(`QingLong url: ${qlUrl}\nQingLong token: ${qlToken}`);return config}function setBaseUrlAndTimeout(config){if(!qlUrl){qlUrl=data.read("magic_qlurl")}if(config.url.indexOf(qlUrl)<0){config.url=`${qlUrl}${config.url}`}return{...config,timeout:timeout}}function setTimestamp(config){config.params={...config.params,t:Date.now()};return config}async function setAuthorization(config){qlToken=qlToken||data.read("magic_qltoken","");if(!qlToken){await getToken()}config.headers["authorization"]=`Bearer ${qlToken}`;return config}function switchClientMode(config){qlClient=qlClient||data.read("magic_qlclient");if(!!qlClient){config.url=config.url.replace("/api/","/open/")}return config}async function refreshToken(error){try{const message=error.message||error.error||JSON.stringify(error);if((message.indexOf("NSURLErrorDomain")>=0&&message.indexOf("-1012")>=0||!!error.response&&error.response.status===401)&&!!error.config&&error.config.refreshToken!==true){logger.warning(`QingLong Panel token has expired`);logger.info("Refreshing the QingLong Panel token");await getToken();error.config["refreshToken"]=true;logger.info("Call the previous method again");return await http.request(error.config.method,error.config)}else{return Promise.reject(error)}}catch(ex){return Promise.reject(ex)}}http.interceptors.request.use(setBaseUrlAndTimeout,undefined);http.interceptors.request.use(switchClientMode,undefined,{runWhen:config=>{return config.url.indexOf("api/user/login")<0&&config.url.indexOf("open/auth/token")<0}});http.interceptors.request.use(setAuthorization,undefined,{runWhen:config=>{return config.url.indexOf("api/user/login")<0&&config.url.indexOf("open/auth/token")<0}});http.interceptors.request.use(setTimestamp,undefined,{runWhen:config=>{return config.url.indexOf("open/auth/token")<0}});http.interceptors.request.use(readQingLongConfig,undefined);http.interceptors.response.use(undefined,refreshToken);async function getToken(){qlClient=qlClient||data.read("magic_qlclient");qlSecret=qlSecret||data.read("magic_qlsecrt");qlName=qlName||data.read("magic_qlname");qlPwd=qlPwd||data.read("magic_qlpwd");if(qlUrl&&qlClient&&qlSecret){logger.info("Get token from QingLong Panel");await http.get({url:`/open/auth/token`,headers:{"content-type":"application/json"},params:{client_id:qlClient,client_secret:qlSecret}}).then(resp=>{if(Object.keys(resp.body).length>0&&resp.body.data&&resp.body.data.token){logger.info("Successfully logged in to QingLong Panel");qlToken=resp.body.data.token;data.write("magic_qltoken",qlToken)}else{throw new Error("Get QingLong Panel token failed.")}}).catch(err=>{logger.error(`Error logging in to QingLong Panel.\n${err.message||err}`)})}else if(qlUrl&&qlName&&qlPwd){await http.post({url:`/api/user/login`,headers:{"content-type":"application/json"},body:{username:qlName,password:qlPwd}}).then(resp=>{logger.info("Successfully logged in to QingLong Panel");qlToken=resp.body.data.token;data.write("magic_qltoken",qlToken)}).catch(err=>{logger.error(`Error logging in to QingLong Panel.\n${err.message||err}`)})}return qlToken}async function setEnv(name,value,id=null){qlUrl=qlUrl||data.read("magic_qlurl");if(id===null){let envIds=await setEnvs([{name:name,value:value}]);if(!!envIds&&envIds.length===1){return envIds[0]}}else{await http.put({url:`/api/envs`,headers:{"content-type":"application/json"},body:{name:name,value:value,id:id}}).then(resp=>{if(resp.body.code===200){logger.debug(`QINGLONG UPDATE ENV ${name} <${typeof value}> (${id})\n${JSON.stringify(value)}`);return true}else{logger.error(`Error adding environment variable from QingLong Panel.\n${JSON.stringify(resp)}`)}}).catch(err=>{logger.error(`Error adding environment variable from QingLong Panel.\n${err.message||err}`);return false})}}async function setEnvs(envs){let envIds=[];await http.post({url:`/api/envs`,headers:{"content-type":"application/json"},body:envs}).then(resp=>{if(resp.body.code===200){resp.body.data.forEach(element=>{logger.debug(`QINGLONG ADD ENV ${element.name} <${typeof element.value}> (${element.id})\n${JSON.stringify(element)}`);envIds.push(element.id)})}else{logger.error(`Error adding environments variable from QingLong Panel.\n${JSON.stringify(resp)}`)}}).catch(err=>{logger.error(`Error adding environments variable from QingLong Panel.\n${err.message||err}`)});return envIds}async function delEnvs(ids){return await http.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:ids}).then(resp=>{if(resp.body.code===200){logger.debug(`QINGLONG DELETE ENV IDS: ${ids}`);return true}else{logger.error(`Error deleting environments variable from QingLong Panel.\n${JSON.stringify(resp)}`);return false}}).catch(err=>{logger.error(`Error deleting environments variable from QingLong Panel.\n${err.message||err}`)})}async function getEnvs(name=null,searchValue="",retired=0){let envs=[];await http.get({url:`/api/envs`,headers:{"content-type":"application/json"},params:{searchValue:searchValue}}).then(resp=>{if(resp.body.code===200){const allEnvs=resp.body.data;if(!!name){let _envs=[];for(const env of allEnvs){if(env.name===name){envs.push(env)}}envs=_envs}envs=allEnvs}else{throw new Error(`Error reading environment variable from QingLong Panel.\n${JSON.stringify(resp)}`)}}).catch(err=>{throw new Error(`Error reading environments variable from QingLong Panel.\n${err.message||err}`)});return envs}async function getEnv(id){let env=null;const allEnvs=await getEnvs();for(const _env of allEnvs){if(_env.id===id){env=_env;break}}return env}async function disableEnvs(ids){let result=false;await http.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:ids}).then(resp=>{if(resp.body.code===200){logger.debug(`QINGLONG DISABLED ENV IDS: ${ids}`);result=true}else{logger.error(`Error disabling environments variable from QingLong Panel.\n${JSON.stringify(resp)}`)}}).catch(err=>{logger.error(`Error disabling environments variable from QingLong Panel.\n${err.message||err}`)});return result}async function enableEnvs(ids){let result=false;await http.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:ids}).then(resp=>{if(resp.body.code===200){logger.debug(`QINGLONG ENABLED ENV IDS: ${ids}`);result=true}else{logger.error(`Error enabling environments variable from Qilong panel.\n${JSON.stringify(resp)}`)}}).catch(err=>{logger.error(`Error enabling environments variable from Qilong panel.\n${err.message||err}`)});return result}async function addScript(name,path="",content=""){let result=false;await http.post({url:`/api/scripts`,headers:{"content-type":"application/json"},body:{filename:name,path:path,content:content}}).then(resp=>{if(resp.body.code===200){result=true}else{logger.error(`Error reading data from QingLong Panel.\n${JSON.stringify(resp)}`)}}).catch(err=>{logger.error(`Error reading data from QingLong Panel.\n${err.message||err}`)});return result}async function getScript(name,path=""){let content="";await http.get({url:`/api/scripts/${name}`,params:{path:path}}).then(resp=>{if(resp.body.code===200){content=resp.body.data}else{throw new Error(`Error reading data from QingLong Panel.\n${JSON.stringify(resp)}`)}}).catch(err=>{throw new Error(`Error reading data from QingLong Panel.\n${err.message||err}`)});return content}async function editScript(name,path="",content=""){let result=false;await http.put({url:`/api/scripts`,headers:{"content-type":"application/json"},body:{filename:name,path:path,content:content}}).then(resp=>{if(resp.body.code===200){result=true}else{logger.error(`Error reading data from QingLong Panel.\n${JSON.stringify(resp)}`)}}).catch(err=>{logger.error(`Error reading data from QingLong Panel.\n${err.message||err}`)});return result}async function delScript(name,path=""){let result=false;await http.delete({url:`/api/scripts`,headers:{"content-type":"application/json"},body:{filename:name,path:path}}).then(resp=>{if(resp.body.code===200){result=true}else{logger.error(`Error reading data from QingLong Panel.\n${JSON.stringify(resp)}`)}}).catch(err=>{logger.error(`Error reading data from QingLong Panel.\n${err.message||err}`)});return result}async function write(key,val,session=""){let qlContent=await getScript(magicJsonFileName,"");let qlData=data.convertToObject(qlContent);let writeResult=data.write(key,val,session,qlData);qlContent=JSON.stringify(qlData,null,4);let editResult=await editScript(magicJsonFileName,"",qlContent);return editResult&&writeResult}async function batchWrite(...args){let qlContent=await getScript(magicJsonFileName,"");let qlData=data.convertToObject(qlContent);for(let arg of args){data.write(arg[0],arg[1],typeof arg[2]!=="undefined"?arg[2]:"",qlData)}qlContent=JSON.stringify(qlData,null,4);return await editScript(magicJsonFileName,"",qlContent)}async function update(key,val,session,comparator=data.defaultValueComparator){let qlContent=await getScript(magicJsonFileName,"");let qlData=data.convertToObject(qlContent);const updateResult=data.update(key,val,session,comparator,qlData);let editScriptResult=false;if(updateResult===true){qlContent=JSON.stringify(qlData,null,4);editScriptResult=await editScript(magicJsonFileName,"",qlContent)}return updateResult&&editScriptResult}async function batchUpdate(...args){let qlContent=await getScript(magicJsonFileName,"");let qlData=data.convertToObject(qlContent);for(let arg of args){data.update(arg[0],arg[1],typeof arg[2]!=="undefined"?arg[2]:"",typeof arg[3]!=="undefined"?arg["comparator"]:data.defaultValueComparator,qlData)}qlContent=JSON.stringify(qlData,null,4);return await editScript(magicJsonFileName,"",qlContent)}async function read(key,val,session="",read_no_session=false){let qlContent=await getScript(magicJsonFileName,"");let qlData=data.convertToObject(qlContent);return data.read(key,val,session,read_no_session,qlData)}async function batchRead(...args){let qlContent=await getScript(magicJsonFileName,"");let qlData=data.convertToObject(qlContent);let results=[];for(let arg of args){const result=data.read(arg[0],arg[1],typeof arg[2]!=="undefined"?arg[2]:"",typeof arg[3]==="boolean"?arg[3]:false,qlData);results.push(result)}return results}async function del(key,session=""){let qlContent=await getScript(magicJsonFileName,"");let qlData=data.convertToObject(qlContent);const delResult=data.del(key,session,qlData);qlContent=JSON.stringify(qlData,null,4);const editResult=await editScript(magicJsonFileName,"",qlContent);return delResult&&editResult}async function batchDel(...args){let qlContent=await getScript(magicJsonFileName,"");let qlData=data.convertToObject(qlContent);for(let arg of args){data.del(arg[0],typeof arg[1]!=="undefined"?arg[1]:"",qlData)}qlContent=JSON.stringify(qlData,null,4);return await editScript(magicJsonFileName,"",qlContent)}async function allSessionNames(key){let qlContent=await getScript(magicJsonFileName,"");let qlData=data.convertToObject(qlContent);return data.allSessionNames(key,qlData)}async function allSessions(key){let qlContent=await getScript(magicJsonFileName,"");let qlData=data.convertToObject(qlContent);return data.allSessions(key,qlData)}return{url:qlUrl||data.read("magic_qlurl"),init:init,getToken:getToken,setEnv:setEnv,setEnvs:setEnvs,getEnv:getEnv,getEnvs:getEnvs,delEnvs:delEnvs,disableEnvs:disableEnvs,enableEnvs:enableEnvs,addScript:addScript,getScript:getScript,editScript:editScript,delScript:delScript,write:write,read:read,del:del,update:update,batchWrite:batchWrite,batchRead:batchRead,batchUpdate:batchUpdate,batchDel:batchDel,allSessions:allSessions,allSessionNames:allSessionNames}}
// @formatter:on