mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-10-12 21:41:29 +08:00
432 lines
14 KiB
JavaScript
432 lines
14 KiB
JavaScript
/* eslint-disable no-undef */
|
||
const isQX = typeof $task !== 'undefined';
|
||
const isLoon = typeof $loon !== 'undefined';
|
||
const isSurge = typeof $httpClient !== 'undefined' && !isLoon;
|
||
const isNode = eval(`typeof process !== "undefined"`); // eval is needed in order to avoid browserify processing
|
||
const isStash =
|
||
'undefined' !== typeof $environment && $environment['stash-version'];
|
||
const isShadowRocket = 'undefined' !== typeof $rocket;
|
||
const isEgern = 'object' == typeof egern;
|
||
const isLanceX = 'undefined' != typeof $native;
|
||
const isGUIforCores = typeof $Plugins !== 'undefined';
|
||
|
||
export class OpenAPI {
|
||
constructor(name = 'untitled', debug = false) {
|
||
this.name = name;
|
||
this.debug = debug;
|
||
|
||
this.http = HTTP();
|
||
this.env = ENV();
|
||
|
||
this.node = (() => {
|
||
if (isNode) {
|
||
const fs = eval("require('fs')");
|
||
|
||
return {
|
||
fs,
|
||
};
|
||
} else {
|
||
return null;
|
||
}
|
||
})();
|
||
this.initCache();
|
||
|
||
const delay = (t, v) =>
|
||
new Promise(function (resolve) {
|
||
setTimeout(resolve.bind(null, v), t);
|
||
});
|
||
|
||
Promise.prototype.delay = async function (t) {
|
||
const v = await this;
|
||
return await delay(t, v);
|
||
};
|
||
}
|
||
|
||
// persistence
|
||
// initialize cache
|
||
initCache() {
|
||
if (isQX)
|
||
this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}');
|
||
if (isLoon || isSurge)
|
||
this.cache = JSON.parse($persistentStore.read(this.name) || '{}');
|
||
if (isGUIforCores)
|
||
this.cache = JSON.parse(Plugins.$memory.get(this.name) || '{}');
|
||
if (isNode) {
|
||
// create a json for root cache
|
||
const basePath =
|
||
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
||
let rootPath = `${basePath}/root.json`;
|
||
|
||
this.log(`Root path: ${rootPath}`);
|
||
if (!this.node.fs.existsSync(rootPath)) {
|
||
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
|
||
flag: 'wx',
|
||
});
|
||
this.root = {};
|
||
} else {
|
||
this.root = JSON.parse(
|
||
this.node.fs.readFileSync(`${rootPath}`),
|
||
);
|
||
}
|
||
|
||
// create a json file with the given name if not exists
|
||
let fpath = `${basePath}/${this.name}.json`;
|
||
this.log(`Data path: ${fpath}`);
|
||
if (!this.node.fs.existsSync(fpath)) {
|
||
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
|
||
flag: 'wx',
|
||
});
|
||
this.cache = {};
|
||
} else {
|
||
this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
|
||
}
|
||
}
|
||
}
|
||
|
||
// store cache
|
||
persistCache() {
|
||
const data = JSON.stringify(this.cache, null, 2);
|
||
if (isQX) $prefs.setValueForKey(data, this.name);
|
||
if (isLoon || isSurge) $persistentStore.write(data, this.name);
|
||
if (isGUIforCores) Plugins.$memory.set(this.name, data);
|
||
if (isNode) {
|
||
const basePath =
|
||
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
||
|
||
this.node.fs.writeFileSync(
|
||
`${basePath}/${this.name}.json`,
|
||
data,
|
||
{ flag: 'w' },
|
||
(err) => console.log(err),
|
||
);
|
||
this.node.fs.writeFileSync(
|
||
`${basePath}/root.json`,
|
||
JSON.stringify(this.root, null, 2),
|
||
{ flag: 'w' },
|
||
(err) => console.log(err),
|
||
);
|
||
}
|
||
}
|
||
|
||
write(data, key) {
|
||
this.log(`SET ${key}`);
|
||
if (key.indexOf('#') !== -1) {
|
||
key = key.substr(1);
|
||
if (isSurge || isLoon) {
|
||
return $persistentStore.write(data, key);
|
||
}
|
||
if (isQX) {
|
||
return $prefs.setValueForKey(data, key);
|
||
}
|
||
if (isNode) {
|
||
this.root[key] = data;
|
||
}
|
||
if (isGUIforCores) {
|
||
return Plugins.$memory.set(key, data);
|
||
}
|
||
} else {
|
||
this.cache[key] = data;
|
||
}
|
||
this.persistCache();
|
||
}
|
||
|
||
read(key) {
|
||
this.log(`READ ${key}`);
|
||
if (key.indexOf('#') !== -1) {
|
||
key = key.substr(1);
|
||
if (isSurge || isLoon) {
|
||
return $persistentStore.read(key);
|
||
}
|
||
if (isQX) {
|
||
return $prefs.valueForKey(key);
|
||
}
|
||
if (isNode) {
|
||
return this.root[key];
|
||
}
|
||
if (isGUIforCores) {
|
||
return Plugins.$memory.get(key);
|
||
}
|
||
} else {
|
||
return this.cache[key];
|
||
}
|
||
}
|
||
|
||
delete(key) {
|
||
this.log(`DELETE ${key}`);
|
||
if (key.indexOf('#') !== -1) {
|
||
key = key.substr(1);
|
||
if (isSurge || isLoon) {
|
||
return $persistentStore.write(null, key);
|
||
}
|
||
if (isQX) {
|
||
return $prefs.removeValueForKey(key);
|
||
}
|
||
if (isNode) {
|
||
delete this.root[key];
|
||
}
|
||
if (isGUIforCores) {
|
||
return Plugins.$memory.remove(key);
|
||
}
|
||
} else {
|
||
delete this.cache[key];
|
||
}
|
||
this.persistCache();
|
||
}
|
||
|
||
// notification
|
||
notify(title, subtitle = '', content = '', options = {}) {
|
||
const openURL = options['open-url'];
|
||
const mediaURL = options['media-url'];
|
||
|
||
if (isQX) $notify(title, subtitle, content, options);
|
||
if (isSurge) {
|
||
$notification.post(
|
||
title,
|
||
subtitle,
|
||
content + `${mediaURL ? '\n多媒体:' + mediaURL : ''}`,
|
||
{
|
||
url: openURL,
|
||
},
|
||
);
|
||
}
|
||
if (isLoon) {
|
||
let opts = {};
|
||
if (openURL) opts['openUrl'] = openURL;
|
||
if (mediaURL) opts['mediaUrl'] = mediaURL;
|
||
if (JSON.stringify(opts) === '{}') {
|
||
$notification.post(title, subtitle, content);
|
||
} else {
|
||
$notification.post(title, subtitle, content, opts);
|
||
}
|
||
}
|
||
if (isNode) {
|
||
const content_ =
|
||
content +
|
||
(openURL ? `\n点击跳转: ${openURL}` : '') +
|
||
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
|
||
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
|
||
|
||
let push = eval('process.env.SUB_STORE_PUSH_SERVICE');
|
||
if (push) {
|
||
const url = push
|
||
.replace(
|
||
'[推送标题]',
|
||
encodeURIComponent(title || 'Sub-Store'),
|
||
)
|
||
.replace(
|
||
'[推送内容]',
|
||
encodeURIComponent(
|
||
[subtitle, content_].map((i) => i).join('\n'),
|
||
),
|
||
);
|
||
const $http = HTTP();
|
||
$http
|
||
.get({ url })
|
||
.then((resp) => {
|
||
console.log(
|
||
`[Push Service] URL: ${url}\nRES: ${resp.statusCode} ${resp.body}`,
|
||
);
|
||
})
|
||
.catch((e) => {
|
||
console.log(`[Push Service] URL: ${url}\nERROR: ${e}`);
|
||
});
|
||
}
|
||
}
|
||
if (isGUIforCores) {
|
||
$Plugins.Notify(title, subtitle + '\n' + content);
|
||
}
|
||
}
|
||
|
||
// other helper functions
|
||
log(msg) {
|
||
if (this.debug) console.log(`[${this.name}] LOG: ${msg}`);
|
||
}
|
||
|
||
info(msg) {
|
||
console.log(`[${this.name}] INFO: ${msg}`);
|
||
}
|
||
|
||
error(msg) {
|
||
console.log(`[${this.name}] ERROR: ${msg}`);
|
||
}
|
||
|
||
wait(millisec) {
|
||
return new Promise((resolve) => setTimeout(resolve, millisec));
|
||
}
|
||
|
||
done(value = {}) {
|
||
if (isQX || isLoon || isSurge || isGUIforCores) {
|
||
$done(value);
|
||
} else if (isNode) {
|
||
if (typeof $context !== 'undefined') {
|
||
$context.headers = value.headers;
|
||
$context.statusCode = value.statusCode;
|
||
$context.body = value.body;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
export function ENV() {
|
||
return {
|
||
isQX,
|
||
isLoon,
|
||
isSurge,
|
||
isNode,
|
||
isStash,
|
||
isShadowRocket,
|
||
isEgern,
|
||
isLanceX,
|
||
isGUIforCores,
|
||
};
|
||
}
|
||
|
||
export function HTTP(defaultOptions = { baseURL: '' }) {
|
||
const { isQX, isLoon, isSurge, isNode, isGUIforCores } = ENV();
|
||
const methods = [
|
||
'GET',
|
||
'POST',
|
||
'PUT',
|
||
'DELETE',
|
||
'HEAD',
|
||
'OPTIONS',
|
||
'PATCH',
|
||
];
|
||
const URL_REGEX =
|
||
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
||
|
||
function send(method, options) {
|
||
options = typeof options === 'string' ? { url: options } : options;
|
||
const baseURL = defaultOptions.baseURL;
|
||
if (baseURL && !URL_REGEX.test(options.url || '')) {
|
||
options.url = baseURL ? baseURL + options.url : options.url;
|
||
}
|
||
options = { ...defaultOptions, ...options };
|
||
const timeout = options.timeout;
|
||
const events = {
|
||
...{
|
||
onRequest: () => {},
|
||
onResponse: (resp) => resp,
|
||
onTimeout: () => {},
|
||
},
|
||
...options.events,
|
||
};
|
||
|
||
events.onRequest(method, options);
|
||
|
||
if (options.node) {
|
||
// Surge & Loon allow connecting to a server using a specified proxy node
|
||
if (isSurge) {
|
||
const build = $environment['surge-build'];
|
||
if (build && parseInt(build) >= 2407) {
|
||
options['policy-descriptor'] = options.node;
|
||
delete options.node;
|
||
}
|
||
}
|
||
}
|
||
|
||
let worker;
|
||
if (isQX) {
|
||
worker = $task.fetch({
|
||
method,
|
||
url: options.url,
|
||
headers: options.headers,
|
||
body: options.body,
|
||
opts: options.opts,
|
||
});
|
||
} else if (isLoon || isSurge || isNode) {
|
||
worker = new Promise((resolve, reject) => {
|
||
const request = isNode
|
||
? eval("require('request')")
|
||
: $httpClient;
|
||
const opts = JSON.parse(JSON.stringify(options));
|
||
if (!isNode && opts.timeout) {
|
||
opts.timeout++;
|
||
let unit = 'ms';
|
||
// 这些客户端单位为 s
|
||
if (isSurge || isStash || isShadowRocket) {
|
||
opts.timeout = Math.ceil(opts.timeout / 1000);
|
||
unit = 's';
|
||
}
|
||
// Loon 为 ms
|
||
// console.log(`[httpClient timeout] ${opts.timeout}${unit}`);
|
||
}
|
||
request[method.toLowerCase()](opts, (err, response, body) => {
|
||
// if (err) {
|
||
// console.log(err);
|
||
// } else {
|
||
// console.log({
|
||
// statusCode:
|
||
// response.status || response.statusCode,
|
||
// headers: response.headers,
|
||
// body,
|
||
// });
|
||
// }
|
||
|
||
if (err) reject(err);
|
||
else
|
||
resolve({
|
||
statusCode: response.status || response.statusCode,
|
||
headers: response.headers,
|
||
body,
|
||
});
|
||
});
|
||
});
|
||
} else if (isGUIforCores) {
|
||
worker = new Promise(async (resolve, reject) => {
|
||
const requestHandler = {
|
||
get: $Plugins.HttpGet,
|
||
post: $Plugins.HttpPost,
|
||
put: $Plugins.HttpPut,
|
||
delete: $Plugins.HttpDelete,
|
||
};
|
||
const request = requestHandler[method.toLowerCase()];
|
||
if (!request) reject('GUI.for.Cores未实现当前方法:' + method);
|
||
try {
|
||
const { url, headers } = options;
|
||
const response = await request(url, headers, options.body);
|
||
resolve({
|
||
statusCode: response.status ?? 200,
|
||
headers: response.header,
|
||
body: response.body,
|
||
});
|
||
} catch (error) {
|
||
reject(error);
|
||
}
|
||
});
|
||
}
|
||
|
||
let timeoutid;
|
||
|
||
const timer = timeout
|
||
? new Promise((_, reject) => {
|
||
// console.log(`[request timeout] ${timeout}ms`);
|
||
timeoutid = setTimeout(() => {
|
||
events.onTimeout();
|
||
return reject(
|
||
`${method} URL: ${options.url} exceeds the timeout ${timeout} ms`,
|
||
);
|
||
}, timeout);
|
||
})
|
||
: null;
|
||
|
||
return (
|
||
timer
|
||
? Promise.race([timer, worker]).then((res) => {
|
||
if (typeof clearTimeout !== 'undefined') {
|
||
clearTimeout(timeoutid);
|
||
}
|
||
return res;
|
||
})
|
||
: worker
|
||
).then((resp) => events.onResponse(resp));
|
||
}
|
||
|
||
const http = {};
|
||
methods.forEach(
|
||
(method) =>
|
||
(http[method.toLowerCase()] = (options) => send(method, options)),
|
||
);
|
||
return http;
|
||
}
|