Bump to ES6

This commit is contained in:
Peng-YM 2022-05-24 21:20:26 +08:00
parent 46e37df110
commit e228416718
23 changed files with 14866 additions and 3442 deletions

7
backend/.babelrc Normal file
View File

@ -0,0 +1,7 @@
{
"presets": [
[
"@babel/preset-env"
]
]
}

6
backend/.prettierrc.json Normal file
View File

@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 4,
"bracketSpacing": true
}

View File

@ -5,8 +5,9 @@
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
* Advanced Subscription Manager for QX, Loon, Surge and Clash.
* @version: 1.5
* Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket!
* @updated: <%= updated %>
* @version: <%= pkg.version %>
* @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46

48
backend/gulpfile.babel.js Normal file
View File

@ -0,0 +1,48 @@
import fs from 'fs';
import browserify from 'browserify';
import gulp from 'gulp';
import prettier from 'gulp-prettier';
import header from 'gulp-header';
const DEST_FILE = 'sub-store.min.js';
export function styles() {
return gulp
.src('src/**/*.js')
.pipe(
prettier({
singleQuote: true,
trailingComma: 'all',
tabWidth: 4,
bracketSpacing: true
})
)
.pipe(gulp.dest((file) => file.base));
}
export function scripts() {
return browserify('src/main.js')
.transform('babelify', {
presets: [
[
'@babel/preset-env'
]
]
})
.plugin('tinyify')
.bundle()
.pipe(fs.createWriteStream(DEST_FILE));
}
export function banner() {
const pkg = require('./package.json');
return gulp
.src(DEST_FILE)
.pipe(header(fs.readFileSync('./banner', 'utf-8'), { pkg, updated: new Date().toLocaleString() }))
.pipe(gulp.dest((file) => file.base));
}
const build = gulp.series(styles, scripts, banner);
export default build;

10843
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
{
"name": "sub-store",
"version": "1.5",
"version": "1.5.1",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"serve": "node src/main.js",
"build": "browserify -p tinyify src/main.js > bundle && cat banner bundle > sub-store.min.js && rm bundle"
"build": "gulp"
},
"author": "Peng-YM",
"license": "GPL",
@ -18,8 +18,20 @@
"static-js-yaml": "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.18.0",
"@babel/preset-env": "^7.18.0",
"@babel/register": "^7.17.7",
"@types/gulp": "^4.0.9",
"axios": "^0.20.0",
"babelify": "^10.0.0",
"browser-pack-flat": "^3.4.2",
"browserify": "^17.0.0",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
"gulp-header": "^2.0.9",
"gulp-prettier": "^4.0.0",
"prettier": "2.6.2",
"prettier-plugin-sort-imports": "^1.6.1",
"tinyify": "^3.0.0"
}
}

View File

@ -1,4 +1,4 @@
const { API } = require('../utils/open-api');
import { API } from '../utils/open-api';
const $ = API('sub-store');
module.exports = $;
export default $;

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
const $ = require("./app");
import $ from './app';
const RULE_TYPES_MAPPING = [
[/^(DOMAIN|host|HOST)$/, 'DOMAIN'],
@ -11,7 +11,7 @@ const RULE_TYPES_MAPPING = [
[/^(IN|SRC)-PORT$/, 'IN-PORT'],
[/^PROTOCOL$/, 'PROTOCOL'],
[/^IP-CIDR$/i, 'IP-CIDR'],
[ /^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/ ]
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/],
];
const RULE_PREPROCESSORS = (function () {
@ -57,17 +57,23 @@ const RULE_PARSERS = (function() {
matched = true;
const rule = {
type: item[1],
content: params[1]
content: params[1],
};
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
if (
rule.type === 'IP-CIDR' ||
rule.type === 'IP-CIDR6'
) {
rule.options = params.slice(2);
}
result.push(rule);
}
}
if (!matched) throw new Error('Invalid rule type: ' + rawType);
if (!matched)
throw new Error('Invalid rule type: ' + rawType);
} catch (e) {
console.error(`Failed to parse line: ${line}\n Reason: ${e}`);
console.error(
`Failed to parse line: ${line}\n Reason: ${e}`,
);
}
}
return result;
@ -89,7 +95,7 @@ const RULE_PROCESSORS = (function() {
});
return keep ? selected : !selected;
});
}
},
};
}
@ -98,7 +104,7 @@ const RULE_PROCESSORS = (function() {
name: 'Type Filter',
func: (rules) => {
return rules.map((rule) => types.some((t) => rule.type === t));
}
},
};
}
@ -111,14 +117,16 @@ const RULE_PROCESSORS = (function() {
rules.forEach((rule) => {
const options = rule.options || [];
options.sort();
const key = `${rule.type},${rule.content},${JSON.stringify(options)}`;
const key = `${rule.type},${rule.content},${JSON.stringify(
options,
)}`;
if (!seen.has(key)) {
result.push(rule);
seen.add(key);
}
});
return result;
}
},
};
}
@ -129,11 +137,13 @@ const RULE_PROCESSORS = (function() {
func: (rules) => {
return rules.map((rule) => {
for (const { expr, now } of regex) {
rule.content = rule.content.replace(new RegExp(expr, 'g'), now).trim();
rule.content = rule.content
.replace(new RegExp(expr, 'g'), now)
.trim();
}
return rule;
});
}
},
};
}
@ -142,7 +152,7 @@ const RULE_PROCESSORS = (function() {
'Remove Duplicate Filter': RemoveDuplicateFilter,
'Type Filter': TypeFilter,
'Regex Replace Operator': RegexReplaceOperator
'Regex Replace Operator': RegexReplaceOperator,
};
})();
const RULE_PRODUCERS = (function () {
@ -150,18 +160,26 @@ const RULE_PRODUCERS = (function() {
const type = 'SINGLE';
const func = (rule) => {
// skip unsupported rules
const UNSUPPORTED = [ 'URL-REGEX', 'DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL' ];
const UNSUPPORTED = [
'URL-REGEX',
'DEST-PORT',
'SRC-IP',
'IN-PORT',
'PROTOCOL',
];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
const TRANSFORM = {
'DOMAIN-KEYWORD': 'HOST-KEYWORD',
'DOMAIN-SUFFIX': 'HOST-SUFFIX',
DOMAIN: 'HOST',
'IP-CIDR6': 'IP6-CIDR'
'IP-CIDR6': 'IP6-CIDR',
};
// QX does not support the no-resolve option
return `${TRANSFORM[rule.type] || rule.type},${rule.content},SUB-STORE`;
return `${TRANSFORM[rule.type] || rule.type},${
rule.content
},SUB-STORE`;
};
return { type, func };
}
@ -195,16 +213,18 @@ const RULE_PRODUCERS = (function() {
const TRANSFORM = {
'DEST-PORT': 'DST-PORT',
'SRC-IP': 'SRC-IP-CIDR',
'IN-PORT': 'SRC-PORT'
'IN-PORT': 'SRC-PORT',
};
const conf = {
payload: rules.map((rule) => {
let output = `${TRANSFORM[rule.type] || rule.type},${rule.content}`;
let output = `${TRANSFORM[rule.type] || rule.type},${
rule.content
}`;
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
output += rule.options ? `,${rule.options[0]}` : '';
}
return output;
})
}),
};
return YAML.stringify(conf);
};
@ -215,11 +235,11 @@ const RULE_PRODUCERS = (function() {
QX: QXFilter(),
Surge: SurgeRuleSet(),
Loon: LoonRules(),
Clash: ClashRuleProvider()
Clash: ClashRuleProvider(),
};
})();
const RuleUtils = (function() {
export const RuleUtils = (function () {
function preprocess(raw) {
for (const processor of RULE_PREPROCESSORS) {
try {
@ -257,7 +277,11 @@ const RuleUtils = (function() {
continue;
}
const processor = RULE_PROCESSORS[item.type](item.args);
$.info(`Applying "${item.type}" with arguments: \n >>> ${JSON.stringify(item.args) || 'None'}`);
$.info(
`Applying "${item.type}" with arguments: \n >>> ${
JSON.stringify(item.args) || 'None'
}`,
);
rules = ApplyProcessor(processor, rules);
}
return rules;
@ -266,15 +290,24 @@ const RuleUtils = (function() {
function produce(rules, targetPlatform) {
const producer = RULE_PRODUCERS[targetPlatform];
if (!producer) {
throw new Error(`Target platform: ${targetPlatform} is not supported!`);
throw new Error(
`Target platform: ${targetPlatform} is not supported!`,
);
}
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
if (
typeof producer.type === 'undefined' ||
producer.type === 'SINGLE'
) {
return rules
.map((rule) => {
try {
return producer.func(rule);
} catch (err) {
console.log(`ERROR: cannot produce rule: ${JSON.stringify(rule)}\nReason: ${err}`);
console.log(
`ERROR: cannot produce rule: ${JSON.stringify(
rule,
)}\nReason: ${err}`,
);
return '';
}
})
@ -287,7 +320,3 @@ const RuleUtils = (function() {
return { parse, process, produce };
})();
module.exports = {
RuleUtils
};

View File

@ -1,11 +1,17 @@
const $ = require('../core/app');
import { ProxyUtils } from '../core/proxy-utils';
import { RuleUtils } from '../core/rule-utils';
import download from '../utils/download';
import Gist from '../utils/gist';
import $ from '../core/app';
const download = require('../utils/download');
const Gist = require('../utils/gist');
const { ProxyUtils } = require('../core/proxy-utils');
const { RuleUtils } = require('../core/rule-utils');
const { SUBS_KEY, ARTIFACTS_KEY, ARTIFACT_REPOSITORY_KEY, COLLECTIONS_KEY, RULES_KEY, SETTINGS_KEY } = require('./constants');
import {
SUBS_KEY,
ARTIFACTS_KEY,
ARTIFACT_REPOSITORY_KEY,
COLLECTIONS_KEY,
RULES_KEY,
SETTINGS_KEY,
} from './constants';
function register($app) {
// Initialization
@ -14,7 +20,10 @@ function register($app) {
// RESTful APIs
$app.route('/api/artifacts').get(getAllArtifacts).post(createArtifact);
$app.route('/api/artifact/:name').get(getArtifact).patch(updateArtifact).delete(deleteArtifact);
$app.route('/api/artifact/:name')
.get(getArtifact)
.patch(updateArtifact)
.delete(deleteArtifact);
// sync all artifacts
$app.get('/api/cron/sync-artifacts', cronSyncArtifacts);
@ -43,7 +52,7 @@ async function getArtifact(req, res) {
const output = await produceArtifact({
type: artifact.type,
item,
platform: artifact.platform
platform: artifact.platform,
});
if (action === 'preview') {
res.send(output);
@ -52,32 +61,35 @@ async function getArtifact(req, res) {
console.log(JSON.stringify(artifact, null, 2));
try {
const resp = await syncArtifact({
[artifact.name]: { content: output }
[artifact.name]: { content: output },
});
artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body);
artifact.url = body.files[artifact.name].raw_url.replace(/\/raw\/[^\/]*\/(.*)/, '/raw/$1');
artifact.url = body.files[artifact.name].raw_url.replace(
/\/raw\/[^\/]*\/(.*)/,
'/raw/$1',
);
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: 'success'
status: 'success',
});
} catch (err) {
res.status(500).json({
status: 'failed',
message: err
message: err,
});
}
}
} else {
res.json({
status: 'success',
data: artifact
data: artifact,
});
}
} else {
res.status(404).json({
status: 'failed',
message: '未找到对应的配置!'
message: '未找到对应的配置!',
});
}
}
@ -89,7 +101,7 @@ function createArtifact(req, res) {
if (allArtifacts[artifact.name]) {
res.status(500).json({
status: 'failed',
message: `远程配置${artifact.name}已存在!`
message: `远程配置${artifact.name}已存在!`,
});
} else {
if (/^[\w-_.]*$/.test(artifact.name)) {
@ -97,12 +109,12 @@ function createArtifact(req, res) {
$.write(allArtifacts, ARTIFACTS_KEY);
res.status(201).json({
status: 'success',
data: artifact
data: artifact,
});
} else {
res.status(500).json({
status: 'failed',
message: `远程配置名称 ${artifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
message: `远程配置名称 ${artifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
}
@ -115,28 +127,31 @@ function updateArtifact(req, res) {
if (artifact) {
$.info(`正在更新远程配置:${artifact.name}`);
const newArtifact = req.body;
if (typeof newArtifact.name !== 'undefined' && !/^[\w-_.]*$/.test(newArtifact.name)) {
if (
typeof newArtifact.name !== 'undefined' &&
!/^[\w-_.]*$/.test(newArtifact.name)
) {
res.status(500).json({
status: 'failed',
message: `远程配置名称 ${newArtifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
message: `远程配置名称 ${newArtifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
} else {
const merged = {
...artifact,
...newArtifact
...newArtifact,
};
allArtifacts[merged.name] = merged;
if (merged.name !== oldName) delete allArtifacts[oldName];
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: 'success',
data: merged
data: merged,
});
}
} else {
res.status(404).json({
status: 'failed',
message: '未找到对应的远程配置!'
message: '未找到对应的远程配置!',
});
}
}
@ -166,14 +181,14 @@ async function cronSyncArtifacts(_, res) {
const output = await produceArtifact({
type: artifact.type,
item,
platform: artifact.platform
platform: artifact.platform,
});
files[artifact.name] = {
content: output
content: output,
};
}
})
}),
);
const resp = await syncArtifact(files);
@ -182,7 +197,10 @@ async function cronSyncArtifacts(_, res) {
for (const artifact of Object.values(allArtifacts)) {
artifact.updated = new Date().getTime();
// extract real url from gist
artifact.url = body.files[artifact.name].raw_url.replace(/\/raw\/[^\/]*\/(.*)/, '/raw/$1');
artifact.url = body.files[artifact.name].raw_url.replace(
/\/raw\/[^\/]*\/(.*)/,
'/raw/$1',
);
}
$.write(allArtifacts, ARTIFACTS_KEY);
@ -190,7 +208,7 @@ async function cronSyncArtifacts(_, res) {
res.status(200).end();
} catch (err) {
res.status(500).json({
error: err
error: err,
});
$.info(`同步订阅失败,原因:${err}`);
}
@ -207,14 +225,14 @@ async function deleteArtifact(req, res) {
// delete gist
await syncArtifact({
filename: name,
content: ''
content: '',
});
}
// delete local cache
delete allArtifacts[name];
$.write(allArtifacts, ARTIFACTS_KEY);
res.json({
status: 'success'
status: 'success',
});
} catch (err) {
// delete local cache
@ -222,7 +240,7 @@ async function deleteArtifact(req, res) {
$.write(allArtifacts, ARTIFACTS_KEY);
res.status(500).json({
status: 'failed',
message: `无法删除远程配置:${name}, 原因:${err}`
message: `无法删除远程配置:${name}, 原因:${err}`,
});
}
}
@ -231,7 +249,7 @@ function getAllArtifacts(req, res) {
const allArtifacts = $.read(ARTIFACTS_KEY);
res.json({
status: 'success',
data: allArtifacts
data: allArtifacts,
});
}
@ -242,7 +260,7 @@ async function syncArtifact(files) {
}
const manager = new Gist({
token: gistToken,
key: ARTIFACT_REPOSITORY_KEY
key: ARTIFACT_REPOSITORY_KEY,
});
return manager.upload(files);
}
@ -250,8 +268,8 @@ async function syncArtifact(files) {
async function produceArtifact(
{ type, item, platform, noProcessor } = {
platform: 'JSON',
noProcessor: false
}
noProcessor: false,
},
) {
if (type === 'subscription') {
const sub = item;
@ -260,16 +278,25 @@ async function produceArtifact(
let proxies = ProxyUtils.parse(raw);
if (!noProcessor) {
// apply processors
proxies = await ProxyUtils.process(proxies, sub.process || [], platform);
proxies = await ProxyUtils.process(
proxies,
sub.process || [],
platform,
);
}
// check duplicate
const exist = {};
for (const proxy of proxies) {
if (exist[proxy.name]) {
$.notify('🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』', '⚠️ 订阅包含重复节点!', '请仔细检测配置!', {
$.notify(
'🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』',
'⚠️ 订阅包含重复节点!',
'请仔细检测配置!',
{
'media-url':
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png'
});
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
},
);
break;
}
exist[proxy.name] = true;
@ -293,27 +320,45 @@ async function produceArtifact(
let currentProxies = ProxyUtils.parse(raw);
if (!noProcessor) {
// apply processors
currentProxies = await ProxyUtils.process(currentProxies, sub.process || [], platform);
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
platform,
);
}
results[name] = currentProxies;
processed++;
$.info(`✅ 子订阅:${sub.name}加载成功,进度--${100 * (processed / subs.length).toFixed(1)}% `);
$.info(
`✅ 子订阅:${sub.name}加载成功,进度--${
100 * (processed / subs.length).toFixed(1)
}% `,
);
} catch (err) {
processed++;
$.error(
`❌ 处理组合订阅中的子订阅: ${sub.name}时出现错误:${err},该订阅已被跳过!进度--${100 *
(processed / subs.length).toFixed(1)}%`
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误${err}该订阅已被跳过进度--${
100 * (processed / subs.length).toFixed(1)
}%`,
);
}
})
}),
);
// merge proxies with the original order
let proxies = Array.prototype.concat.apply([], subs.map(name => results[name]));
let proxies = Array.prototype.concat.apply(
[],
subs.map((name) => results[name]),
);
if (!noProcessor) {
// apply own processors
proxies = await ProxyUtils.process(proxies, collection.process || [], platform);
proxies = await ProxyUtils.process(
proxies,
collection.process || [],
platform,
);
}
if (proxies.length === 0) {
throw new Error(`组合订阅中不含有效节点!`);
@ -322,10 +367,15 @@ async function produceArtifact(
const exist = {};
for (const proxy of proxies) {
if (exist[proxy.name]) {
$.notify('🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』', '⚠️ 订阅包含重复节点!', '请仔细检测配置!', {
$.notify(
'🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』',
'⚠️ 订阅包含重复节点!',
'请仔细检测配置!',
{
'media-url':
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png'
});
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
},
);
break;
}
exist[proxy.name] = true;
@ -336,20 +386,28 @@ async function produceArtifact(
let rules = [];
for (let i = 0; i < rule.urls.length; i++) {
const url = rule.urls[i];
$.info(`正在处理URL${url},进度--${100 * ((i + 1) / rule.urls.length).toFixed(1)}% `);
$.info(
`正在处理URL${url},进度--${
100 * ((i + 1) / rule.urls.length).toFixed(1)
}% `,
);
try {
const { body } = await download(url);
const currentRules = RuleUtils.parse(body);
rules = rules.concat(currentRules);
} catch (err) {
$.error(`处理分流订阅中的URL: ${url}时出现错误:${err}! 该订阅已被跳过。`);
$.error(
`处理分流订阅中的URL: ${url}时出现错误:${err}! 该订阅已被跳过。`,
);
}
}
// remove duplicates
rules = await RuleUtils.process(rules, [ { type: 'Remove Duplicate Filter' } ]);
rules = await RuleUtils.process(rules, [
{ type: 'Remove Duplicate Filter' },
]);
// produce output
return RuleUtils.produce(rules, platform);
}
}
module.exports = { register, produceArtifact };
export { register, produceArtifact };

View File

@ -1,23 +1,29 @@
const $ = require('../core/app');
const { SUBS_KEY, COLLECTIONS_KEY } = require('./constants');
const { getPlatformFromHeaders, getFlowHeaders } = require('./subscriptions');
const { produceArtifact } = require('./artifacts');
import { getPlatformFromHeaders, getFlowHeaders } from './subscriptions';
import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
import { produceArtifact } from './artifacts';
import $ from '../core/app';
function register($app) {
export function register($app) {
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
$app.get("/download/collection/:name", downloadCollection);
$app.get('/download/collection/:name', downloadCollection);
$app.route('/api/collection/:name').get(getCollection).patch(updateCollection).delete(deleteCollection);
$app.route('/api/collection/:name')
.get(getCollection)
.patch(updateCollection)
.delete(deleteCollection);
$app.route('/api/collections').get(getAllCollections).post(createCollection);
$app.route('/api/collections')
.get(getAllCollections)
.post(createCollection);
}
// collection API
async function downloadCollection(req, res) {
const { name } = req.params;
const { raw } = req.query || 'false';
const platform = req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const allCollections = $.read(COLLECTIONS_KEY);
const collection = allCollections[name];
@ -41,24 +47,33 @@ async function downloadCollection(req, res) {
type: 'collection',
item: collection,
platform,
noProcessor: raw
noProcessor: raw,
});
if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send(output);
res.set('Content-Type', 'application/json;charset=utf-8').send(
output,
);
} else {
res.send(output);
}
} catch (err) {
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`, `❌ 下载组合订阅错误:${name}`, `🤔 原因:${err}`);
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
`❌ 下载组合订阅错误:${name}`,
`🤔 原因:${err}`,
);
res.status(500).json({
status: 'failed',
message: err
message: err,
});
}
} else {
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`, `❌ 未找到组合订阅:${name}`);
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
`❌ 未找到组合订阅:${name}`,
);
res.status(404).json({
status: 'failed'
status: 'failed',
});
}
}
@ -70,7 +85,7 @@ function createCollection(req, res) {
if (allCol[collection.name]) {
res.status(500).json({
status: 'failed',
message: `订阅集${collection.name}已存在!`
message: `订阅集${collection.name}已存在!`,
});
}
// validate name
@ -79,12 +94,12 @@ function createCollection(req, res) {
$.write(allCol, COLLECTIONS_KEY);
res.status(201).json({
status: 'success',
data: collection
data: collection,
});
} else {
res.status(500).json({
status: 'failed',
message: `订阅集名称 ${collection.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
message: `订阅集名称 ${collection.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
}
@ -95,12 +110,12 @@ function getCollection(req, res) {
if (collection) {
res.json({
status: 'success',
data: collection
data: collection,
});
} else {
res.status(404).json({
status: 'failed',
message: `未找到订阅集:${name}!`
message: `未找到订阅集:${name}!`,
});
}
}
@ -112,7 +127,7 @@ function updateCollection(req, res) {
if (allCol[name]) {
const newCol = {
...allCol[name],
...collection
...collection,
};
$.info(`正在更新组合订阅:${name}...`);
// allow users to update collection name
@ -121,12 +136,12 @@ function updateCollection(req, res) {
$.write(allCol, COLLECTIONS_KEY);
res.json({
status: 'success',
data: newCol
data: newCol,
});
} else {
res.status(500).json({
status: 'failed',
message: `订阅集${name}不存在,无法更新!`
message: `订阅集${name}不存在,无法更新!`,
});
}
}
@ -138,7 +153,7 @@ function deleteCollection(req, res) {
delete allCol[name];
$.write(allCol, COLLECTIONS_KEY);
res.json({
status: 'success'
status: 'success',
});
}
@ -146,10 +161,6 @@ function getAllCollections(req, res) {
const allCols = $.read(COLLECTIONS_KEY);
res.json({
status: 'success',
data: allCols
data: allCols,
});
}
module.exports = {
register
};

View File

@ -1,11 +1,7 @@
module.exports = {
SETTINGS_KEY: 'settings',
SUBS_KEY: 'subs',
COLLECTIONS_KEY: 'collections',
RULES_KEY: 'rules',
BUILT_IN_KEY: 'builtin',
ARTIFACTS_KEY: 'artifacts',
GIST_BACKUP_KEY: 'Auto Generated Sub-Store Backup',
GIST_BACKUP_FILE_NAME: 'Sub-Store',
ARTIFACT_REPOSITORY_KEY: 'Sub-Store Artifacts Repository'
};
export const SETTINGS_KEY = 'settings';
export const SUBS_KEY = 'subs';
export const COLLECTIONS_KEY = 'collections';
export const ARTIFACTS_KEY = 'artifacts';
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';

View File

@ -1,11 +1,15 @@
const $ = require('../core/app');
const { ENV } = require('../utils/open-api');
const { IP_API } = require('../utils/geo');
const Gist = require('../utils/gist');
const { SETTINGS_KEY, GIST_BACKUP_KEY, GIST_BACKUP_FILE_NAME } = require('./constants');
import {
SETTINGS_KEY,
GIST_BACKUP_KEY,
GIST_BACKUP_FILE_NAME,
} from './constants';
import { ENV } from '../utils/open-api';
import express from '../utils/express';
import { IP_API } from '../utils/geo';
import Gist from '../utils/gist';
import $ from '../core/app';
function serve() {
const express = require('../utils/express');
export default function serve() {
const $app = express();
// register routes
@ -54,7 +58,7 @@ function getEnv(req, res) {
if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge';
res.json({
backend
backend,
});
}
@ -65,12 +69,12 @@ async function gistBackup(req, res) {
if (!gistToken) {
res.status(500).json({
status: 'failed',
message: '未找到Gist备份Token!'
message: '未找到Gist备份Token!',
});
} else {
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY
key: GIST_BACKUP_KEY,
});
try {
let content;
@ -81,7 +85,8 @@ async function gistBackup(req, res) {
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
if ($.env.isNode)
content = JSON.stringify($.cache, null, ` `);
$.info(`上传备份中...`);
await gist.upload({ [GIST_BACKUP_FILE_NAME]: { content } });
break;
@ -99,17 +104,17 @@ async function gistBackup(req, res) {
break;
}
res.json({
status: 'success'
status: 'success',
});
} catch (err) {
const msg = `${action === 'upload' ? '上传' : '下载'}备份失败!${err}`;
const msg = `${
action === 'upload' ? '上传' : '下载'
}备份失败${err}`;
$.error(msg);
res.status(500).json({
status: 'failed',
message: msg
message: msg,
});
}
}
}
module.exports = serve;

View File

@ -1,9 +1,8 @@
const $ = require('../core/app');
const { SETTINGS_KEY } = require('./constants');
import { SETTINGS_KEY } from './constants';
import $ from '../core/app';
function register($app) {
export function register($app) {
if (!$.read(SETTINGS_KEY)) $.write({}, SETTINGS_KEY);
$app.route('/api/settings').get(getSettings).patch(updateSettings);
}
@ -18,15 +17,11 @@ function updateSettings(req, res) {
$.write(
{
...settings,
...data
...data,
},
SETTINGS_KEY
SETTINGS_KEY,
);
res.json({
status: 'success'
status: 'success',
});
}
module.exports = {
register
};

View File

@ -1,13 +1,16 @@
const $ = require("../core/app");
const { produceArtifact } = require('./artifacts');
const { SUBS_KEY, COLLECTIONS_KEY } = require('./constants');
import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
import { produceArtifact } from './artifacts';
import $ from '../core/app';
function register($app) {
export function register($app) {
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
$app.get('/download/:name', downloadSubscription);
$app.route('/api/sub/:name').get(getSubscription).patch(updateSubscription).delete(deleteSubscription);
$app.route('/api/sub/:name')
.get(getSubscription)
.patch(updateSubscription)
.delete(deleteSubscription);
$app.route('/api/subs').get(getAllSubscriptions).post(createSubscription);
}
@ -16,7 +19,8 @@ function register($app) {
async function downloadSubscription(req, res) {
const { name } = req.params;
const { raw } = req.query || 'false';
const platform = req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
const platform =
req.query.target || getPlatformFromHeaders(req.headers) || 'JSON';
$.info(`正在下载订阅:${name}`);
@ -28,7 +32,7 @@ async function downloadSubscription(req, res) {
type: 'subscription',
item: sub,
platform,
noProcessor: raw
noProcessor: raw,
});
// forward flow headers
@ -38,22 +42,28 @@ async function downloadSubscription(req, res) {
}
if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send(output);
res.set('Content-Type', 'application/json;charset=utf-8').send(
output,
);
} else {
res.send(output);
}
} catch (err) {
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 无法下载订阅:${name}`, `🤔 原因:${JSON.stringify(err)}`);
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`,
`❌ 无法下载订阅:${name}`,
`🤔 原因:${JSON.stringify(err)}`,
);
$.error(JSON.stringify(err));
res.status(500).json({
status: 'failed',
message: err
message: err,
});
}
} else {
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 未找到订阅:${name}`);
res.status(404).json({
status: 'failed'
status: 'failed',
});
}
}
@ -65,7 +75,7 @@ function createSubscription(req, res) {
if (allSubs[sub.name]) {
res.status(500).json({
status: 'failed',
message: `订阅${sub.name}已存在!`
message: `订阅${sub.name}已存在!`,
});
}
// validate name
@ -74,12 +84,12 @@ function createSubscription(req, res) {
$.write(allSubs, SUBS_KEY);
res.status(201).json({
status: 'success',
data: sub
data: sub,
});
} else {
res.status(500).json({
status: 'failed',
message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
});
}
}
@ -90,12 +100,12 @@ function getSubscription(req, res) {
if (sub) {
res.json({
status: 'success',
data: sub
data: sub,
});
} else {
res.status(404).json({
status: 'failed',
message: `未找到订阅:${name}!`
message: `未找到订阅:${name}!`,
});
}
}
@ -107,7 +117,7 @@ function updateSubscription(req, res) {
if (allSubs[name]) {
const newSub = {
...allSubs[name],
...sub
...sub,
};
$.info(`正在更新订阅: ${name}`);
// allow users to update the subscription name
@ -129,12 +139,12 @@ function updateSubscription(req, res) {
$.write(allSubs, SUBS_KEY);
res.json({
status: 'success',
data: newSub
data: newSub,
});
} else {
res.status(500).json({
status: 'failed',
message: `订阅${name}不存在,无法更新!`
message: `订阅${name}不存在,无法更新!`,
});
}
}
@ -149,11 +159,13 @@ function deleteSubscription(req, res) {
// delete from collections
let allCols = $.read(COLLECTIONS_KEY);
for (const k of Object.keys(allCols)) {
allCols[k].subscriptions = allCols[k].subscriptions.filter((s) => s !== name);
allCols[k].subscriptions = allCols[k].subscriptions.filter(
(s) => s !== name,
);
}
$.write(allCols, COLLECTIONS_KEY);
res.json({
status: 'success'
status: 'success',
});
}
@ -161,22 +173,24 @@ function getAllSubscriptions(req, res) {
const allSubs = $.read(SUBS_KEY);
res.json({
status: 'success',
data: allSubs
data: allSubs,
});
}
async function getFlowHeaders(url) {
export async function getFlowHeaders(url) {
const { headers } = await $.http.get({
url,
headers: {
'User-Agent': 'Quantumult/1.0.13 (iPhone10,3; iOS 14.0)'
}
'User-Agent': 'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)',
},
});
const subkey = Object.keys(headers).filter((k) => /SUBSCRIPTION-USERINFO/i.test(k))[0];
const subkey = Object.keys(headers).filter((k) =>
/SUBSCRIPTION-USERINFO/i.test(k),
)[0];
return headers[subkey];
}
function getPlatformFromHeaders(headers) {
export function getPlatformFromHeaders(headers) {
const keys = Object.keys(headers);
let UA = '';
for (let k of keys) {
@ -191,15 +205,12 @@ function getPlatformFromHeaders(headers) {
return 'Surge';
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
return 'Loon';
} else if (UA.indexOf('Stash') !== -1 || UA.indexOf('Shadowrocket') !== -1) {
} else if (
UA.indexOf('Stash') !== -1 ||
UA.indexOf('Shadowrocket') !== -1
) {
return 'Clash';
} else {
return null;
}
}
module.exports = {
register,
getPlatformFromHeaders,
getFlowHeaders
};

View File

@ -16,8 +16,9 @@ console.log(
𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 © 𝑷𝒆𝒏𝒈-𝒀𝑴
`
`,
);
const serve = require('./facade');
import serve from './facade';
serve();

View File

@ -1,8 +1,8 @@
const { HTTP } = require('./open-api');
import { HTTP } from './open-api';
const cache = new Map();
async function download(url, ua) {
export default async function download(url, ua) {
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
const id = ua + url;
if (cache.has(id)) {
@ -11,20 +11,19 @@ async function download(url, ua) {
const $http = HTTP({
headers: {
'User-Agent': ua
}
'User-Agent': ua,
},
});
const result = new Promise((resolve, reject) => {
$http.get(url).then((resp) => {
const body = resp.body;
if (body.replace(/\s/g, '').length === 0) reject(new Error('订阅内容为空!'));
if (body.replace(/\s/g, '').length === 0)
reject(new Error('订阅内容为空!'));
else resolve(body);
});
});
cache[id] = result;
cache.set(id, result);
return result;
}
module.exports = download;

View File

@ -1,13 +1,14 @@
const $ = require('../core/app');
const { ENV } = require('./open-api');
import { ENV } from './open-api';
import $ from '../core/app';
function express({ port } = { port: 3000 }) {
export default function express({ port } = { port: 3000 }) {
const { isNode } = ENV();
const DEFAULT_HEADERS = {
'Content-Type': 'text/plain;charset=UTF-8',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept'
'Access-Control-Allow-Headers':
'Origin, X-Requested-With, Content-Type, Accept',
};
// node support
@ -16,7 +17,9 @@ function express({ port } = { port: 3000 }) {
const bodyParser = eval(`require("body-parser")`);
const app = express_();
app.use(bodyParser.json({ verify: rawBodySaver }));
app.use(bodyParser.urlencoded({ verify: rawBodySaver, extended: true }));
app.use(
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
);
app.use(bodyParser.raw({ verify: rawBodySaver, type: '*/*' }));
app.use((_, res, next) => {
res.set(DEFAULT_HEADERS);
@ -36,7 +39,16 @@ function express({ port } = { port: 3000 }) {
const handlers = [];
// http methods
const METHODS_NAMES = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', "HEAD'", 'ALL' ];
const METHODS_NAMES = [
'GET',
'POST',
'PUT',
'DELETE',
'PATCH',
'OPTIONS',
"HEAD'",
'ALL',
];
// dispatch url to route
const dispatch = (request, start = 0) => {
@ -75,7 +87,7 @@ function express({ port } = { port: 3000 }) {
query,
params: extractPathParams(handler.pattern, path),
headers,
body
body,
};
const res = Response();
const cb = handler.callback;
@ -83,7 +95,7 @@ function express({ port } = { port: 3000 }) {
const errFunc = (err) => {
res.status(500).json({
status: 'failed',
message: `Internal Server Error: ${err}`
message: `Internal Server Error: ${err}`,
});
};
@ -101,7 +113,7 @@ function express({ port } = { port: 3000 }) {
const res = Response();
res.status(404).json({
status: 'failed',
message: 'ERROR: 404 not found'
message: 'ERROR: 404 not found',
});
}
};
@ -156,9 +168,9 @@ function express({ port } = { port: 3000 }) {
307: 'HTTP/1.1 307 Temporary Redirect',
308: 'HTTP/1.1 308 Permanent Redirect',
404: 'HTTP/1.1 404 Not Found',
500: 'HTTP/1.1 500 Internal Server Error'
500: 'HTTP/1.1 500 Internal Server Error',
};
return new class {
return new (class {
status(code) {
statusCode = code;
return this;
@ -168,13 +180,13 @@ function express({ port } = { port: 3000 }) {
const response = {
status: isQX ? STATUS_CODE_MAP[statusCode] : statusCode,
body,
headers
headers,
};
if (isQX) {
$done(response);
} else if (isLoon || isSurge) {
$done({
response
response,
});
}
}
@ -197,7 +209,7 @@ function express({ port } = { port: 3000 }) {
headers[key] = val;
return this;
}
}();
})();
}
function patternMatched(pattern, path) {
@ -235,13 +247,13 @@ function express({ port } = { port: 3000 }) {
if (split !== -1) {
let hashes = url.slice(url.indexOf('?') + 1).split('&');
for (let i = 0; i < hashes.length; i++) {
hash = hashes[i].split('=');
const hash = hashes[i].split('=');
query[hash[0]] = hash[1];
}
}
return {
path,
query
query,
};
}
@ -271,5 +283,3 @@ function express({ port } = { port: 3000 }) {
}
}
}
module.exports = express;

View File

@ -1,12 +1,26 @@
const { HTTP } = require('./open-api');
import { HTTP } from './open-api';
// get proxy flag according to its name
function getFlag(name) {
export function getFlag(name) {
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
const flags = {
'🇦🇿': ['阿塞拜疆'],
'🇦🇹': ['奥地利', '奧地利', 'Austria', '维也纳'],
'🇦🇺': [ 'AU', 'Australia', 'Sydney', '澳大利亚', '澳洲', '墨尔本', '悉尼', '土澳', '京澳', '廣澳', '滬澳', '沪澳', '广澳' ],
'🇦🇺': [
'AU',
'Australia',
'Sydney',
'澳大利亚',
'澳洲',
'墨尔本',
'悉尼',
'土澳',
'京澳',
'廣澳',
'滬澳',
'沪澳',
'广澳',
],
'🇧🇪': ['BE', '比利時', '比利时'],
'🇧🇬': ['保加利亚', '保加利亞', 'Bulgaria'],
'🇧🇭': ['BH', '巴林'],
@ -15,20 +29,55 @@ function getFlag(name) {
'🇰🇭': ['柬埔寨'],
'🇺🇦': ['烏克蘭', '乌克兰'],
'🇭🇷': ['克罗地亚', 'HR', '克羅地亞'],
'🇨🇦': [ 'Canada', 'CANADA', 'CAN', 'Waterloo', '加拿大', '蒙特利尔', '温哥华', '楓葉', '枫叶', '滑铁卢', '多伦多', 'CA' ],
'🇨🇦': [
'Canada',
'CANADA',
'CAN',
'Waterloo',
'加拿大',
'蒙特利尔',
'温哥华',
'楓葉',
'枫叶',
'滑铁卢',
'多伦多',
'CA',
],
'🇨🇭': ['瑞士', '苏黎世', 'Switzerland', 'Zurich'],
'🇳🇬': ['尼日利亚', 'NG', '尼日利亞'],
'🇨🇿': ['Czechia', '捷克'],
'🇸🇰': ['斯洛伐克', 'SK'],
'🇷🇸': ['RS', '塞尔维亚'],
'🇲🇩': ['摩爾多瓦', 'MD', '摩尔多瓦'],
'🇩🇪': [ 'DE', 'German', 'GERMAN', '德国', '德國', '法兰克福', '京德', '滬德', '廣德', '沪德', '广德', 'Frankfurt' ],
'🇩🇪': [
'DE',
'German',
'GERMAN',
'德国',
'德國',
'法兰克福',
'京德',
'滬德',
'廣德',
'沪德',
'广德',
'Frankfurt',
],
'🇩🇰': ['DK', 'DNK', '丹麦', '丹麥'],
'🇪🇸': ['ES', '西班牙', 'Spain'],
'🇪🇺': ['EU', '欧盟', '欧罗巴'],
'🇫🇮': ['Finland', '芬兰', '芬蘭', '赫尔辛基'],
'🇫🇷': ['FR', 'France', '法国', '法國', '巴黎'],
'🇬🇧': [ 'UK', 'GB', 'England', 'United Kingdom', '英国', '伦敦', '英', 'London' ],
'🇬🇧': [
'UK',
'GB',
'England',
'United Kingdom',
'英国',
'伦敦',
'英',
'London',
],
'🇲🇴': ['MO', 'Macao', '澳门', '澳門', 'CTM'],
'🇰🇿': ['哈萨克斯坦', '哈萨克'],
'🇭🇺': ['匈牙利', 'Hungary'],
@ -49,7 +98,7 @@ function getFlag(name) {
'CMI',
'穗港',
'京港',
'港'
'港',
],
'🇮🇩': ['Indonesia', '印尼', '印度尼西亚', '雅加达'],
'🇮🇪': ['Ireland', 'IRELAND', '爱尔兰', '愛爾蘭', '都柏林'],
@ -57,11 +106,30 @@ function getFlag(name) {
'🇮🇳': ['India', 'IND', 'INDIA', '印度', '孟买', 'MFumbai'],
'🇮🇸': ['IS', 'ISL', '冰岛', '冰島'],
'🇰🇵': ['KP', '朝鲜'],
'🇰🇷': [ 'KR', 'Korea', 'KOR', '韩国', '首尔', '韩', '韓', '春川', 'Chuncheon', 'Seoul' ],
'🇰🇷': [
'KR',
'Korea',
'KOR',
'韩国',
'首尔',
'韩',
'韓',
'春川',
'Chuncheon',
'Seoul',
],
'🇱🇺': ['卢森堡'],
'🇱🇻': ['Latvia', 'Latvija', '拉脱维亚'],
'🇲🇽': ['MEX', 'MX', '墨西哥'],
'🇲🇾': [ 'MY', 'Malaysia', 'MALAYSIA', '马来西亚', '大馬', '馬來西亞', '吉隆坡' ],
'🇲🇾': [
'MY',
'Malaysia',
'MALAYSIA',
'马来西亚',
'大馬',
'馬來西亞',
'吉隆坡',
],
'🇳🇱': ['NL', 'Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
'🇳🇵': ['尼泊尔'],
'🇵🇭': ['PH', 'Philippines', '菲律宾', '菲律賓'],
@ -84,16 +152,44 @@ function getFlag(name) {
'滬俄',
'广俄',
'沪俄',
'Moscow'
'Moscow',
],
'🇸🇦': ['沙特'],
'🇸🇪': ['SE', 'Sweden', '瑞典'],
'🇲🇹': ['马耳他'],
'🇲🇦': ['MA', '摩洛哥'],
'🇸🇬': [ 'SG', 'Singapore', 'SINGAPORE', '新加坡', '狮城', '沪新', '京新', '泉新', '穗新', '深新', '杭新', '广新', '廣新', '滬新' ],
'🇸🇬': [
'SG',
'Singapore',
'SINGAPORE',
'新加坡',
'狮城',
'沪新',
'京新',
'泉新',
'穗新',
'深新',
'杭新',
'广新',
'廣新',
'滬新',
],
'🇹🇭': ['TH', 'Thailand', '泰国', '泰國', '曼谷'],
'🇹🇷': ['TR', 'Turkey', '土耳其', '伊斯坦布尔'],
'🇹🇼': [ 'TW', 'Taiwan', 'TAIWAN', '台湾', '台北', '台中', '新北', '彰化', 'CHT', '台', 'HINET', 'Taipei' ],
'🇹🇼': [
'TW',
'Taiwan',
'TAIWAN',
'台湾',
'台北',
'台中',
'新北',
'彰化',
'CHT',
'台',
'HINET',
'Taipei',
],
'🇺🇸': [
'US',
'USA',
@ -121,7 +217,7 @@ function getFlag(name) {
'Los Angeles',
'San Jose',
'Sillicon Valley',
'Michigan'
'Michigan',
],
'🇻🇳': ['VN', '越南', '胡志明市'],
'🇻🇪': ['VE', '委内瑞拉'],
@ -148,11 +244,28 @@ function getFlag(name) {
'广日',
'大坂',
'Osaka',
'Tokyo'
'Tokyo',
],
'🇦🇷': ['AR', '阿根廷'],
'🇳🇴': ['Norway', '挪威', 'NO'],
'🇨🇳': [ 'CN', 'China', '回国', '中国', '中國', '江苏', '北京', '上海', '广州', '深圳', '杭州', '徐州', '青岛', '宁波', '镇江', 'back' ],
'🇨🇳': [
'CN',
'China',
'回国',
'中国',
'中國',
'江苏',
'北京',
'上海',
'广州',
'深圳',
'杭州',
'徐州',
'青岛',
'宁波',
'镇江',
'back',
],
'🇵🇱': ['PL', 'POL', '波兰', '波蘭'],
'🇨🇱': ['智利'],
'🇳🇿': ['新西蘭', '新西兰'],
@ -181,7 +294,7 @@ function getFlag(name) {
'🇧🇦': ['波黑共和国', '波黑'],
'🇬🇪': ['格魯吉亞', '格鲁吉亚'],
'🇦🇱': ['阿爾巴尼亞', '阿尔巴尼亚'],
'🏳️‍🌈': [ '流量', '时间', '应急', '过期', 'Bandwidth', 'expire' ]
'🏳️‍🌈': ['流量', '时间', '应急', '过期', 'Bandwidth', 'expire'],
};
for (let k of Object.keys(flags)) {
if (flags[k].some((item) => name.indexOf(item) !== -1)) {
@ -189,12 +302,14 @@ function getFlag(name) {
}
}
// no flag found
const oldFlag = (name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/) || [])[0];
const oldFlag = (name.match(
/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/,
) || [])[0];
return oldFlag || '🏴‍☠️';
}
// util API
async function IP_API(req, res) {
export async function IP_API(req, res) {
const server = decodeURIComponent(req.params.server);
const $http = HTTP();
const result = await $http
@ -202,8 +317,3 @@ async function IP_API(req, res) {
.then((resp) => JSON.parse(resp.body));
res.json(result);
}
module.exports = {
getFlag,
IP_API
};

View File

@ -1,25 +1,27 @@
const { HTTP } = require('./open-api');
import { HTTP } from './open-api';
/**
* Gist backup
*/
function Gist({ token, key }) {
export default function Gist({ token, key }) {
const http = HTTP({
baseURL: 'https://api.github.com',
headers: {
Authorization: `token ${token}`,
'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36'
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.141 Safari/537.36',
},
events: {
onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) {
return Promise.reject(`ERROR: ${JSON.parse(resp.body).message}`);
return Promise.reject(
`ERROR: ${JSON.parse(resp.body).message}`,
);
} else {
return resp;
}
}
}
},
},
});
async function locate() {
@ -44,14 +46,14 @@ function Gist({ token, key }) {
body: JSON.stringify({
description: key,
public: false,
files
})
files,
}),
});
} else {
// update an existing gist
return http.patch({
url: `/gists/${id}`,
body: JSON.stringify({ files })
body: JSON.stringify({ files }),
});
}
};
@ -62,7 +64,9 @@ function Gist({ token, key }) {
return Promise.reject('未找到Gist备份');
} else {
try {
const { files } = await http.get(`/gists/${id}`).then((resp) => JSON.parse(resp.body));
const { files } = await http
.get(`/gists/${id}`)
.then((resp) => JSON.parse(resp.body));
const url = files[filename].raw_url;
return await http.get(url).then((resp) => resp.body);
} catch (err) {
@ -71,5 +75,3 @@ function Gist({ token, key }) {
}
};
}
module.exports = Gist;

View File

@ -14,9 +14,4 @@ function FULL(length, bool) {
return [...Array(length).keys()].map(() => bool);
}
module.exports = {
AND,
OR,
NOT,
FULL
}
export { AND, OR, NOT, FULL };

View File

@ -1,4 +1,4 @@
function ENV() {
export function ENV() {
const isQX = typeof $task !== 'undefined';
const isLoon = typeof $loon !== 'undefined';
const isSurge = typeof $httpClient !== 'undefined' && !isLoon;
@ -6,10 +6,19 @@ function ENV() {
return { isQX, isLoon, isSurge, isNode };
}
function HTTP(defaultOptions = { baseURL: '' }) {
export function HTTP(defaultOptions = { baseURL: '' }) {
const { isQX, isLoon, isSurge, isNode } = 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()@:%_\+.~#?&//=]*)/;
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;
@ -23,9 +32,9 @@ function HTTP(defaultOptions = { baseURL: '' }) {
...{
onRequest: () => {},
onResponse: (resp) => resp,
onTimeout: () => {}
onTimeout: () => {},
},
...options.events
...options.events,
};
events.onRequest(method, options);
@ -36,20 +45,26 @@ function HTTP(defaultOptions = { baseURL: '' }) {
method,
url: options.url,
headers: options.headers,
body: options.body
body: options.body,
});
} else if (isLoon || isSurge || isNode) {
worker = new Promise((resolve, reject) => {
const request = isNode ? eval("require('request')") : $httpClient;
request[method.toLowerCase()](options, (err, response, body) => {
const request = isNode
? eval("require('request')")
: $httpClient;
request[method.toLowerCase()](
options,
(err, response, body) => {
if (err) reject(err);
else
resolve({
statusCode: response.status || response.statusCode,
statusCode:
response.status || response.statusCode,
headers: response.headers,
body
});
body,
});
},
);
});
}
@ -58,27 +73,34 @@ function HTTP(defaultOptions = { baseURL: '' }) {
? new Promise((_, reject) => {
timeoutid = setTimeout(() => {
events.onTimeout();
return reject(`${method} URL: ${options.url} exceeds the timeout ${timeout} ms`);
return reject(
`${method} URL: ${options.url} exceeds the timeout ${timeout} ms`,
);
}, timeout);
})
: null;
return (timer
return (
timer
? Promise.race([timer, worker]).then((res) => {
clearTimeout(timeoutid);
return res;
})
: worker).then((resp) => events.onResponse(resp));
: worker
).then((resp) => events.onResponse(resp));
}
const http = {};
methods.forEach((method) => (http[method.toLowerCase()] = (options) => send(method, options)));
methods.forEach(
(method) =>
(http[method.toLowerCase()] = (options) => send(method, options)),
);
return http;
}
function API(name = 'untitled', debug = false) {
export function API(name = 'untitled', debug = false) {
const { isQX, isLoon, isSurge, isNode } = ENV();
return new class {
return new (class {
constructor(name, debug) {
this.name = name;
this.debug = debug;
@ -91,7 +113,7 @@ function API(name = 'untitled', debug = false) {
const fs = eval("require('fs')");
return {
fs
fs,
};
} else {
return null;
@ -114,24 +136,40 @@ function API(name = 'untitled', debug = false) {
// 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 (isQX)
this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}');
if (isLoon || isSurge)
this.cache = JSON.parse(
$persistentStore.read(this.name) || '{}',
);
if (isNode) {
// create a json for root cache
let fpath = 'root.json';
if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { flag: 'wx' }, (err) => console.log(err));
this.node.fs.writeFileSync(
fpath,
JSON.stringify({}),
{ flag: 'wx' },
(err) => console.log(err),
);
}
this.root = {};
// create a json file with the given name if not exists
fpath = `${this.name}.json`;
if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { flag: 'wx' }, (err) => console.log(err));
this.node.fs.writeFileSync(
fpath,
JSON.stringify({}),
{ flag: 'wx' },
(err) => console.log(err),
);
this.cache = {};
} else {
this.cache = JSON.parse(this.node.fs.readFileSync(`${this.name}.json`));
this.cache = JSON.parse(
this.node.fs.readFileSync(`${this.name}.json`),
);
}
}
}
@ -142,9 +180,17 @@ function API(name = 'untitled', debug = false) {
if (isQX) $prefs.setValueForKey(data, this.name);
if (isLoon || isSurge) $persistentStore.write(data, this.name);
if (isNode) {
this.node.fs.writeFileSync(`${this.name}.json`, data, { flag: 'w' }, (err) => console.log(err));
this.node.fs.writeFileSync('root.json', JSON.stringify(this.root, null, 2), { flag: 'w' }, (err) =>
console.log(err)
this.node.fs.writeFileSync(
`${this.name}.json`,
data,
{ flag: 'w' },
(err) => console.log(err),
);
this.node.fs.writeFileSync(
'root.json',
JSON.stringify(this.root, null, 2),
{ flag: 'w' },
(err) => console.log(err),
);
}
}
@ -212,9 +258,14 @@ function API(name = 'untitled', debug = false) {
if (isQX) $notify(title, subtitle, content, options);
if (isSurge) {
$notification.post(title, subtitle, content + `${mediaURL ? '\n多媒体:' + mediaURL : ''}`, {
url: openURL
});
$notification.post(
title,
subtitle,
content + `${mediaURL ? '\n多媒体:' + mediaURL : ''}`,
{
url: openURL,
},
);
}
if (isLoon) {
let opts = {};
@ -228,7 +279,9 @@ function API(name = 'untitled', debug = false) {
}
if (isNode) {
const content_ =
content + (openURL ? `\n点击跳转: ${openURL}` : '') + (mediaURL ? `\n多媒体: ${mediaURL}` : '');
content +
(openURL ? `\n点击跳转: ${openURL}` : '') +
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
}
}
@ -261,11 +314,5 @@ function API(name = 'untitled', debug = false) {
}
}
}
}(name, debug);
})(name, debug);
}
module.exports = {
HTTP,
ENV,
API
};

File diff suppressed because one or more lines are too long