mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-07-06 07:15:12 +08:00
Bump to ES6
This commit is contained in:
parent
46e37df110
commit
e228416718
7
backend/.babelrc
Normal file
7
backend/.babelrc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"@babel/preset-env"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
6
backend/.prettierrc.json
Normal file
6
backend/.prettierrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"bracketSpacing": true
|
||||||
|
}
|
@ -5,8 +5,9 @@
|
|||||||
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
|
* ╚════██║██║ ██║██╔══██╗╚════╝╚════██║ ██║ ██║ ██║██╔══██╗██╔══╝
|
||||||
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
|
* ███████║╚██████╔╝██████╔╝ ███████║ ██║ ╚██████╔╝██║ ██║███████╗
|
||||||
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
|
||||||
* Advanced Subscription Manager for QX, Loon, Surge and Clash.
|
* Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket!
|
||||||
* @version: 1.5
|
* @updated: <%= updated %>
|
||||||
|
* @version: <%= pkg.version %>
|
||||||
* @author: Peng-YM
|
* @author: Peng-YM
|
||||||
* @github: https://github.com/Peng-YM/Sub-Store
|
* @github: https://github.com/Peng-YM/Sub-Store
|
||||||
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
|
||||||
|
48
backend/gulpfile.babel.js
Normal file
48
backend/gulpfile.babel.js
Normal 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
10843
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sub-store",
|
"name": "sub-store",
|
||||||
"version": "1.5",
|
"version": "1.5.1",
|
||||||
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"serve": "node src/main.js",
|
"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",
|
"author": "Peng-YM",
|
||||||
"license": "GPL",
|
"license": "GPL",
|
||||||
@ -18,8 +18,20 @@
|
|||||||
"static-js-yaml": "^1.0.0"
|
"static-js-yaml": "^1.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"axios": "^0.20.0",
|
||||||
|
"babelify": "^10.0.0",
|
||||||
|
"browser-pack-flat": "^3.4.2",
|
||||||
"browserify": "^17.0.0",
|
"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"
|
"tinyify": "^3.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { API } = require('../utils/open-api');
|
import { API } from '../utils/open-api';
|
||||||
const $ = API('sub-store');
|
const $ = API('sub-store');
|
||||||
|
|
||||||
module.exports = $;
|
export default $;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,293 +1,322 @@
|
|||||||
const $ = require("./app");
|
import $ from './app';
|
||||||
|
|
||||||
const RULE_TYPES_MAPPING = [
|
const RULE_TYPES_MAPPING = [
|
||||||
[ /^(DOMAIN|host|HOST)$/, 'DOMAIN' ],
|
[/^(DOMAIN|host|HOST)$/, 'DOMAIN'],
|
||||||
[ /^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, 'DOMAIN-KEYWORD' ],
|
[/^(DOMAIN-KEYWORD|host-keyword|HOST-KEYWORD)$/, 'DOMAIN-KEYWORD'],
|
||||||
[ /^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, 'DOMAIN-SUFFIX' ],
|
[/^(DOMAIN-SUFFIX|host-suffix|HOST-SUFFIX)$/, 'DOMAIN-SUFFIX'],
|
||||||
[ /^USER-AGENT$/i, 'USER-AGENT' ],
|
[/^USER-AGENT$/i, 'USER-AGENT'],
|
||||||
[ /^PROCESS-NAME$/, 'PROCESS-NAME' ],
|
[/^PROCESS-NAME$/, 'PROCESS-NAME'],
|
||||||
[ /^(DEST-PORT|DST-PORT)$/, 'DST-PORT' ],
|
[/^(DEST-PORT|DST-PORT)$/, 'DST-PORT'],
|
||||||
[ /^SRC-IP(-CIDR)?$/, 'SRC-IP' ],
|
[/^SRC-IP(-CIDR)?$/, 'SRC-IP'],
|
||||||
[ /^(IN|SRC)-PORT$/, 'IN-PORT' ],
|
[/^(IN|SRC)-PORT$/, 'IN-PORT'],
|
||||||
[ /^PROTOCOL$/, 'PROTOCOL' ],
|
[/^PROTOCOL$/, 'PROTOCOL'],
|
||||||
[ /^IP-CIDR$/i, 'IP-CIDR' ],
|
[/^IP-CIDR$/i, 'IP-CIDR'],
|
||||||
[ /^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/ ]
|
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/],
|
||||||
];
|
];
|
||||||
|
|
||||||
const RULE_PREPROCESSORS = (function() {
|
const RULE_PREPROCESSORS = (function () {
|
||||||
function HTML() {
|
function HTML() {
|
||||||
const name = 'HTML';
|
const name = 'HTML';
|
||||||
const test = (raw) => /^<!DOCTYPE html>/.test(raw);
|
const test = (raw) => /^<!DOCTYPE html>/.test(raw);
|
||||||
// simply discard HTML
|
// simply discard HTML
|
||||||
const parse = (_) => '';
|
const parse = (_) => '';
|
||||||
return { name, test, parse };
|
return { name, test, parse };
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClashProvider() {
|
function ClashProvider() {
|
||||||
const name = 'Clash Provider';
|
const name = 'Clash Provider';
|
||||||
const test = (raw) => raw.indexOf('payload:') === 0;
|
const test = (raw) => raw.indexOf('payload:') === 0;
|
||||||
const parse = (raw) => {
|
const parse = (raw) => {
|
||||||
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
|
return raw.replace('payload:', '').replace(/^\s*-\s*/gm, '');
|
||||||
};
|
};
|
||||||
return { name, test, parse };
|
return { name, test, parse };
|
||||||
}
|
}
|
||||||
|
|
||||||
return [ HTML(), ClashProvider() ];
|
return [HTML(), ClashProvider()];
|
||||||
})();
|
})();
|
||||||
const RULE_PARSERS = (function() {
|
const RULE_PARSERS = (function () {
|
||||||
function AllRuleParser() {
|
function AllRuleParser() {
|
||||||
const name = 'Universal Rule Parser';
|
const name = 'Universal Rule Parser';
|
||||||
const test = () => true;
|
const test = () => true;
|
||||||
const parse = (raw) => {
|
const parse = (raw) => {
|
||||||
const lines = raw.split('\n');
|
const lines = raw.split('\n');
|
||||||
const result = [];
|
const result = [];
|
||||||
for (let line of lines) {
|
for (let line of lines) {
|
||||||
line = line.trim();
|
line = line.trim();
|
||||||
// skip empty line
|
// skip empty line
|
||||||
if (line.length === 0) continue;
|
if (line.length === 0) continue;
|
||||||
// skip comments
|
// skip comments
|
||||||
if (/\s*#/.test(line)) continue;
|
if (/\s*#/.test(line)) continue;
|
||||||
try {
|
try {
|
||||||
const params = line.split(',').map((w) => w.trim());
|
const params = line.split(',').map((w) => w.trim());
|
||||||
let rawType = params[0];
|
let rawType = params[0];
|
||||||
let matched = false;
|
let matched = false;
|
||||||
for (const item of RULE_TYPES_MAPPING) {
|
for (const item of RULE_TYPES_MAPPING) {
|
||||||
const regex = item[0];
|
const regex = item[0];
|
||||||
if (regex.test(rawType)) {
|
if (regex.test(rawType)) {
|
||||||
matched = true;
|
matched = true;
|
||||||
const rule = {
|
const rule = {
|
||||||
type: item[1],
|
type: item[1],
|
||||||
content: params[1]
|
content: params[1],
|
||||||
};
|
};
|
||||||
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
|
if (
|
||||||
rule.options = params.slice(2);
|
rule.type === 'IP-CIDR' ||
|
||||||
}
|
rule.type === 'IP-CIDR6'
|
||||||
result.push(rule);
|
) {
|
||||||
}
|
rule.options = params.slice(2);
|
||||||
}
|
}
|
||||||
if (!matched) throw new Error('Invalid rule type: ' + rawType);
|
result.push(rule);
|
||||||
} catch (e) {
|
}
|
||||||
console.error(`Failed to parse line: ${line}\n Reason: ${e}`);
|
}
|
||||||
}
|
if (!matched)
|
||||||
}
|
throw new Error('Invalid rule type: ' + rawType);
|
||||||
return result;
|
} catch (e) {
|
||||||
};
|
console.error(
|
||||||
return { name, test, parse };
|
`Failed to parse line: ${line}\n Reason: ${e}`,
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
return { name, test, parse };
|
||||||
|
}
|
||||||
|
|
||||||
return [ AllRuleParser() ];
|
return [AllRuleParser()];
|
||||||
})();
|
})();
|
||||||
const RULE_PROCESSORS = (function() {
|
const RULE_PROCESSORS = (function () {
|
||||||
function RegexFilter({ regex = [], keep = true }) {
|
function RegexFilter({ regex = [], keep = true }) {
|
||||||
return {
|
return {
|
||||||
name: 'Regex Filter',
|
name: 'Regex Filter',
|
||||||
func: (rules) => {
|
func: (rules) => {
|
||||||
return rules.map((rule) => {
|
return rules.map((rule) => {
|
||||||
const selected = regex.some((r) => {
|
const selected = regex.some((r) => {
|
||||||
r = new RegExp(r);
|
r = new RegExp(r);
|
||||||
return r.test(rule);
|
return r.test(rule);
|
||||||
});
|
});
|
||||||
return keep ? selected : !selected;
|
return keep ? selected : !selected;
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function TypeFilter(types) {
|
function TypeFilter(types) {
|
||||||
return {
|
return {
|
||||||
name: 'Type Filter',
|
name: 'Type Filter',
|
||||||
func: (rules) => {
|
func: (rules) => {
|
||||||
return rules.map((rule) => types.some((t) => rule.type === t));
|
return rules.map((rule) => types.some((t) => rule.type === t));
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function RemoveDuplicateFilter() {
|
function RemoveDuplicateFilter() {
|
||||||
return {
|
return {
|
||||||
name: 'Remove Duplicate Filter',
|
name: 'Remove Duplicate Filter',
|
||||||
func: (rules) => {
|
func: (rules) => {
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
const result = [];
|
const result = [];
|
||||||
rules.forEach((rule) => {
|
rules.forEach((rule) => {
|
||||||
const options = rule.options || [];
|
const options = rule.options || [];
|
||||||
options.sort();
|
options.sort();
|
||||||
const key = `${rule.type},${rule.content},${JSON.stringify(options)}`;
|
const key = `${rule.type},${rule.content},${JSON.stringify(
|
||||||
if (!seen.has(key)) {
|
options,
|
||||||
result.push(rule);
|
)}`;
|
||||||
seen.add(key);
|
if (!seen.has(key)) {
|
||||||
}
|
result.push(rule);
|
||||||
});
|
seen.add(key);
|
||||||
return result;
|
}
|
||||||
}
|
});
|
||||||
};
|
return result;
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// regex: [{expr: "string format regex", now: "now"}]
|
// regex: [{expr: "string format regex", now: "now"}]
|
||||||
function RegexReplaceOperator(regex) {
|
function RegexReplaceOperator(regex) {
|
||||||
return {
|
return {
|
||||||
name: 'Regex Rename Operator',
|
name: 'Regex Rename Operator',
|
||||||
func: (rules) => {
|
func: (rules) => {
|
||||||
return rules.map((rule) => {
|
return rules.map((rule) => {
|
||||||
for (const { expr, now } of regex) {
|
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)
|
||||||
return rule;
|
.trim();
|
||||||
});
|
}
|
||||||
}
|
return rule;
|
||||||
};
|
});
|
||||||
}
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'Regex Filter': RegexFilter,
|
'Regex Filter': RegexFilter,
|
||||||
'Remove Duplicate Filter': RemoveDuplicateFilter,
|
'Remove Duplicate Filter': RemoveDuplicateFilter,
|
||||||
'Type Filter': TypeFilter,
|
'Type Filter': TypeFilter,
|
||||||
|
|
||||||
'Regex Replace Operator': RegexReplaceOperator
|
'Regex Replace Operator': RegexReplaceOperator,
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
const RULE_PRODUCERS = (function() {
|
const RULE_PRODUCERS = (function () {
|
||||||
function QXFilter() {
|
function QXFilter() {
|
||||||
const type = 'SINGLE';
|
const type = 'SINGLE';
|
||||||
const func = (rule) => {
|
const func = (rule) => {
|
||||||
// skip unsupported rules
|
// skip unsupported rules
|
||||||
const UNSUPPORTED = [ 'URL-REGEX', 'DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL' ];
|
const UNSUPPORTED = [
|
||||||
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
'URL-REGEX',
|
||||||
|
'DEST-PORT',
|
||||||
|
'SRC-IP',
|
||||||
|
'IN-PORT',
|
||||||
|
'PROTOCOL',
|
||||||
|
];
|
||||||
|
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
||||||
|
|
||||||
const TRANSFORM = {
|
const TRANSFORM = {
|
||||||
'DOMAIN-KEYWORD': 'HOST-KEYWORD',
|
'DOMAIN-KEYWORD': 'HOST-KEYWORD',
|
||||||
'DOMAIN-SUFFIX': 'HOST-SUFFIX',
|
'DOMAIN-SUFFIX': 'HOST-SUFFIX',
|
||||||
DOMAIN: 'HOST',
|
DOMAIN: 'HOST',
|
||||||
'IP-CIDR6': 'IP6-CIDR'
|
'IP-CIDR6': 'IP6-CIDR',
|
||||||
};
|
};
|
||||||
|
|
||||||
// QX does not support the no-resolve option
|
// 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
|
||||||
return { type, func };
|
},SUB-STORE`;
|
||||||
}
|
};
|
||||||
|
return { type, func };
|
||||||
|
}
|
||||||
|
|
||||||
function SurgeRuleSet() {
|
function SurgeRuleSet() {
|
||||||
const type = 'SINGLE';
|
const type = 'SINGLE';
|
||||||
const func = (rule) => {
|
const func = (rule) => {
|
||||||
let output = `${rule.type},${rule.content}`;
|
let output = `${rule.type},${rule.content}`;
|
||||||
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
|
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
|
||||||
output += rule.options ? `,${rule.options[0]}` : '';
|
output += rule.options ? `,${rule.options[0]}` : '';
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
};
|
};
|
||||||
return { type, func };
|
return { type, func };
|
||||||
}
|
}
|
||||||
|
|
||||||
function LoonRules() {
|
function LoonRules() {
|
||||||
const type = 'SINGLE';
|
const type = 'SINGLE';
|
||||||
const func = (rule) => {
|
const func = (rule) => {
|
||||||
// skip unsupported rules
|
// skip unsupported rules
|
||||||
const UNSUPPORTED = [ 'DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL' ];
|
const UNSUPPORTED = ['DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL'];
|
||||||
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
|
||||||
return SurgeRuleSet().func(rule);
|
return SurgeRuleSet().func(rule);
|
||||||
};
|
};
|
||||||
return { type, func };
|
return { type, func };
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClashRuleProvider() {
|
function ClashRuleProvider() {
|
||||||
const type = 'ALL';
|
const type = 'ALL';
|
||||||
const func = (rules) => {
|
const func = (rules) => {
|
||||||
const TRANSFORM = {
|
const TRANSFORM = {
|
||||||
'DEST-PORT': 'DST-PORT',
|
'DEST-PORT': 'DST-PORT',
|
||||||
'SRC-IP': 'SRC-IP-CIDR',
|
'SRC-IP': 'SRC-IP-CIDR',
|
||||||
'IN-PORT': 'SRC-PORT'
|
'IN-PORT': 'SRC-PORT',
|
||||||
};
|
};
|
||||||
const conf = {
|
const conf = {
|
||||||
payload: rules.map((rule) => {
|
payload: rules.map((rule) => {
|
||||||
let output = `${TRANSFORM[rule.type] || rule.type},${rule.content}`;
|
let output = `${TRANSFORM[rule.type] || rule.type},${
|
||||||
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
|
rule.content
|
||||||
output += rule.options ? `,${rule.options[0]}` : '';
|
}`;
|
||||||
}
|
if (rule.type === 'IP-CIDR' || rule.type === 'IP-CIDR6') {
|
||||||
return output;
|
output += rule.options ? `,${rule.options[0]}` : '';
|
||||||
})
|
}
|
||||||
};
|
return output;
|
||||||
return YAML.stringify(conf);
|
}),
|
||||||
};
|
};
|
||||||
return { type, func };
|
return YAML.stringify(conf);
|
||||||
}
|
};
|
||||||
|
return { type, func };
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
QX: QXFilter(),
|
QX: QXFilter(),
|
||||||
Surge: SurgeRuleSet(),
|
Surge: SurgeRuleSet(),
|
||||||
Loon: LoonRules(),
|
Loon: LoonRules(),
|
||||||
Clash: ClashRuleProvider()
|
Clash: ClashRuleProvider(),
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const RuleUtils = (function() {
|
export const RuleUtils = (function () {
|
||||||
function preprocess(raw) {
|
function preprocess(raw) {
|
||||||
for (const processor of RULE_PREPROCESSORS) {
|
for (const processor of RULE_PREPROCESSORS) {
|
||||||
try {
|
try {
|
||||||
if (processor.test(raw)) {
|
if (processor.test(raw)) {
|
||||||
$.info(`Pre-processor [${processor.name}] activated`);
|
$.info(`Pre-processor [${processor.name}] activated`);
|
||||||
return processor.parse(raw);
|
return processor.parse(raw);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
$.error(`Parser [${processor.name}] failed\n Reason: ${e}`);
|
$.error(`Parser [${processor.name}] failed\n Reason: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parse(raw) {
|
function parse(raw) {
|
||||||
raw = preprocess(raw);
|
raw = preprocess(raw);
|
||||||
for (const parser of RULE_PARSERS) {
|
for (const parser of RULE_PARSERS) {
|
||||||
let matched;
|
let matched;
|
||||||
try {
|
try {
|
||||||
matched = parser.test(raw);
|
matched = parser.test(raw);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
matched = false;
|
matched = false;
|
||||||
}
|
}
|
||||||
if (matched) {
|
if (matched) {
|
||||||
$.info(`Rule parser [${parser.name}] is activated!`);
|
$.info(`Rule parser [${parser.name}] is activated!`);
|
||||||
return parser.parse(raw);
|
return parser.parse(raw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function process(rules, operators) {
|
async function process(rules, operators) {
|
||||||
for (const item of operators) {
|
for (const item of operators) {
|
||||||
if (!RULE_PROCESSORS[item.type]) {
|
if (!RULE_PROCESSORS[item.type]) {
|
||||||
console.error(`Unknown operator: ${item.type}!`);
|
console.error(`Unknown operator: ${item.type}!`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const processor = RULE_PROCESSORS[item.type](item.args);
|
const processor = RULE_PROCESSORS[item.type](item.args);
|
||||||
$.info(`Applying "${item.type}" with arguments: \n >>> ${JSON.stringify(item.args) || 'None'}`);
|
$.info(
|
||||||
rules = ApplyProcessor(processor, rules);
|
`Applying "${item.type}" with arguments: \n >>> ${
|
||||||
}
|
JSON.stringify(item.args) || 'None'
|
||||||
return rules;
|
}`,
|
||||||
}
|
);
|
||||||
|
rules = ApplyProcessor(processor, rules);
|
||||||
|
}
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
function produce(rules, targetPlatform) {
|
function produce(rules, targetPlatform) {
|
||||||
const producer = RULE_PRODUCERS[targetPlatform];
|
const producer = RULE_PRODUCERS[targetPlatform];
|
||||||
if (!producer) {
|
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') {
|
);
|
||||||
return rules
|
}
|
||||||
.map((rule) => {
|
if (
|
||||||
try {
|
typeof producer.type === 'undefined' ||
|
||||||
return producer.func(rule);
|
producer.type === 'SINGLE'
|
||||||
} catch (err) {
|
) {
|
||||||
console.log(`ERROR: cannot produce rule: ${JSON.stringify(rule)}\nReason: ${err}`);
|
return rules
|
||||||
return '';
|
.map((rule) => {
|
||||||
}
|
try {
|
||||||
})
|
return producer.func(rule);
|
||||||
.filter((line) => line.length > 0)
|
} catch (err) {
|
||||||
.join('\n');
|
console.log(
|
||||||
} else if (producer.type === 'ALL') {
|
`ERROR: cannot produce rule: ${JSON.stringify(
|
||||||
return producer.func(rules);
|
rule,
|
||||||
}
|
)}\nReason: ${err}`,
|
||||||
}
|
);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((line) => line.length > 0)
|
||||||
|
.join('\n');
|
||||||
|
} else if (producer.type === 'ALL') {
|
||||||
|
return producer.func(rules);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { parse, process, produce };
|
return { parse, process, produce };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
RuleUtils
|
|
||||||
};
|
|
||||||
|
@ -1,355 +1,413 @@
|
|||||||
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');
|
import {
|
||||||
const Gist = require('../utils/gist');
|
SUBS_KEY,
|
||||||
|
ARTIFACTS_KEY,
|
||||||
const { ProxyUtils } = require('../core/proxy-utils');
|
ARTIFACT_REPOSITORY_KEY,
|
||||||
const { RuleUtils } = require('../core/rule-utils');
|
COLLECTIONS_KEY,
|
||||||
const { SUBS_KEY, ARTIFACTS_KEY, ARTIFACT_REPOSITORY_KEY, COLLECTIONS_KEY, RULES_KEY, SETTINGS_KEY } = require('./constants');
|
RULES_KEY,
|
||||||
|
SETTINGS_KEY,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
function register($app) {
|
function register($app) {
|
||||||
// Initialization
|
// Initialization
|
||||||
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
|
if (!$.read(ARTIFACTS_KEY)) $.write({}, ARTIFACTS_KEY);
|
||||||
|
|
||||||
// RESTful APIs
|
// RESTful APIs
|
||||||
$app.route('/api/artifacts').get(getAllArtifacts).post(createArtifact);
|
$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
|
// sync all artifacts
|
||||||
$app.get('/api/cron/sync-artifacts', cronSyncArtifacts);
|
$app.get('/api/cron/sync-artifacts', cronSyncArtifacts);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getArtifact(req, res) {
|
async function getArtifact(req, res) {
|
||||||
const name = req.params.name;
|
const name = req.params.name;
|
||||||
const action = req.query.action;
|
const action = req.query.action;
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
const artifact = allArtifacts[name];
|
const artifact = allArtifacts[name];
|
||||||
|
|
||||||
if (artifact) {
|
if (artifact) {
|
||||||
if (action) {
|
if (action) {
|
||||||
let item;
|
let item;
|
||||||
switch (artifact.type) {
|
switch (artifact.type) {
|
||||||
case 'subscription':
|
case 'subscription':
|
||||||
item = $.read(SUBS_KEY)[artifact.source];
|
item = $.read(SUBS_KEY)[artifact.source];
|
||||||
break;
|
break;
|
||||||
case 'collection':
|
case 'collection':
|
||||||
item = $.read(COLLECTIONS_KEY)[artifact.source];
|
item = $.read(COLLECTIONS_KEY)[artifact.source];
|
||||||
break;
|
break;
|
||||||
case 'rule':
|
case 'rule':
|
||||||
item = $.read(RULES_KEY)[artifact.source];
|
item = $.read(RULES_KEY)[artifact.source];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const output = await produceArtifact({
|
const output = await produceArtifact({
|
||||||
type: artifact.type,
|
type: artifact.type,
|
||||||
item,
|
item,
|
||||||
platform: artifact.platform
|
platform: artifact.platform,
|
||||||
});
|
});
|
||||||
if (action === 'preview') {
|
if (action === 'preview') {
|
||||||
res.send(output);
|
res.send(output);
|
||||||
} else if (action === 'sync') {
|
} else if (action === 'sync') {
|
||||||
$.info(`正在上传配置:${artifact.name}\n>>>`);
|
$.info(`正在上传配置:${artifact.name}\n>>>`);
|
||||||
console.log(JSON.stringify(artifact, null, 2));
|
console.log(JSON.stringify(artifact, null, 2));
|
||||||
try {
|
try {
|
||||||
const resp = await syncArtifact({
|
const resp = await syncArtifact({
|
||||||
[artifact.name]: { content: output }
|
[artifact.name]: { content: output },
|
||||||
});
|
});
|
||||||
artifact.updated = new Date().getTime();
|
artifact.updated = new Date().getTime();
|
||||||
const body = JSON.parse(resp.body);
|
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(
|
||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
/\/raw\/[^\/]*\/(.*)/,
|
||||||
res.json({
|
'/raw/$1',
|
||||||
status: 'success'
|
);
|
||||||
});
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
} catch (err) {
|
res.json({
|
||||||
res.status(500).json({
|
status: 'success',
|
||||||
status: 'failed',
|
});
|
||||||
message: err
|
} catch (err) {
|
||||||
});
|
res.status(500).json({
|
||||||
}
|
status: 'failed',
|
||||||
}
|
message: err,
|
||||||
} else {
|
});
|
||||||
res.json({
|
}
|
||||||
status: 'success',
|
}
|
||||||
data: artifact
|
} else {
|
||||||
});
|
res.json({
|
||||||
}
|
status: 'success',
|
||||||
} else {
|
data: artifact,
|
||||||
res.status(404).json({
|
});
|
||||||
status: 'failed',
|
}
|
||||||
message: '未找到对应的配置!'
|
} else {
|
||||||
});
|
res.status(404).json({
|
||||||
}
|
status: 'failed',
|
||||||
|
message: '未找到对应的配置!',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createArtifact(req, res) {
|
function createArtifact(req, res) {
|
||||||
const artifact = req.body;
|
const artifact = req.body;
|
||||||
$.info(`正在创建远程配置:${artifact.name}`);
|
$.info(`正在创建远程配置:${artifact.name}`);
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
if (allArtifacts[artifact.name]) {
|
if (allArtifacts[artifact.name]) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `远程配置${artifact.name}已存在!`
|
message: `远程配置${artifact.name}已存在!`,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (/^[\w-_.]*$/.test(artifact.name)) {
|
if (/^[\w-_.]*$/.test(artifact.name)) {
|
||||||
allArtifacts[artifact.name] = artifact;
|
allArtifacts[artifact.name] = artifact;
|
||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: artifact
|
data: artifact,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `远程配置名称 ${artifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
|
message: `远程配置名称 ${artifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateArtifact(req, res) {
|
function updateArtifact(req, res) {
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
const oldName = req.params.name;
|
const oldName = req.params.name;
|
||||||
const artifact = allArtifacts[oldName];
|
const artifact = allArtifacts[oldName];
|
||||||
if (artifact) {
|
if (artifact) {
|
||||||
$.info(`正在更新远程配置:${artifact.name}`);
|
$.info(`正在更新远程配置:${artifact.name}`);
|
||||||
const newArtifact = req.body;
|
const newArtifact = req.body;
|
||||||
if (typeof newArtifact.name !== 'undefined' && !/^[\w-_.]*$/.test(newArtifact.name)) {
|
if (
|
||||||
res.status(500).json({
|
typeof newArtifact.name !== 'undefined' &&
|
||||||
status: 'failed',
|
!/^[\w-_.]*$/.test(newArtifact.name)
|
||||||
message: `远程配置名称 ${newArtifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
|
) {
|
||||||
});
|
res.status(500).json({
|
||||||
} else {
|
status: 'failed',
|
||||||
const merged = {
|
message: `远程配置名称 ${newArtifact.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
|
||||||
...artifact,
|
});
|
||||||
...newArtifact
|
} else {
|
||||||
};
|
const merged = {
|
||||||
allArtifacts[merged.name] = merged;
|
...artifact,
|
||||||
if (merged.name !== oldName) delete allArtifacts[oldName];
|
...newArtifact,
|
||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
};
|
||||||
res.json({
|
allArtifacts[merged.name] = merged;
|
||||||
status: 'success',
|
if (merged.name !== oldName) delete allArtifacts[oldName];
|
||||||
data: merged
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
});
|
res.json({
|
||||||
}
|
status: 'success',
|
||||||
} else {
|
data: merged,
|
||||||
res.status(404).json({
|
});
|
||||||
status: 'failed',
|
}
|
||||||
message: '未找到对应的远程配置!'
|
} else {
|
||||||
});
|
res.status(404).json({
|
||||||
}
|
status: 'failed',
|
||||||
|
message: '未找到对应的远程配置!',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cronSyncArtifacts(_, res) {
|
async function cronSyncArtifacts(_, res) {
|
||||||
$.info('开始同步所有远程配置...');
|
$.info('开始同步所有远程配置...');
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
const files = {};
|
const files = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.values(allArtifacts).map(async (artifact) => {
|
Object.values(allArtifacts).map(async (artifact) => {
|
||||||
if (artifact.sync) {
|
if (artifact.sync) {
|
||||||
$.info(`正在同步云配置:${artifact.name}...`);
|
$.info(`正在同步云配置:${artifact.name}...`);
|
||||||
let item;
|
let item;
|
||||||
switch (artifact.type) {
|
switch (artifact.type) {
|
||||||
case 'subscription':
|
case 'subscription':
|
||||||
item = $.read(SUBS_KEY)[artifact.source];
|
item = $.read(SUBS_KEY)[artifact.source];
|
||||||
break;
|
break;
|
||||||
case 'collection':
|
case 'collection':
|
||||||
item = $.read(COLLECTIONS_KEY)[artifact.source];
|
item = $.read(COLLECTIONS_KEY)[artifact.source];
|
||||||
break;
|
break;
|
||||||
case 'rule':
|
case 'rule':
|
||||||
item = $.read(RULES_KEY)[artifact.source];
|
item = $.read(RULES_KEY)[artifact.source];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const output = await produceArtifact({
|
const output = await produceArtifact({
|
||||||
type: artifact.type,
|
type: artifact.type,
|
||||||
item,
|
item,
|
||||||
platform: artifact.platform
|
platform: artifact.platform,
|
||||||
});
|
});
|
||||||
|
|
||||||
files[artifact.name] = {
|
files[artifact.name] = {
|
||||||
content: output
|
content: output,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const resp = await syncArtifact(files);
|
const resp = await syncArtifact(files);
|
||||||
const body = JSON.parse(resp.body);
|
const body = JSON.parse(resp.body);
|
||||||
|
|
||||||
for (const artifact of Object.values(allArtifacts)) {
|
for (const artifact of Object.values(allArtifacts)) {
|
||||||
artifact.updated = new Date().getTime();
|
artifact.updated = new Date().getTime();
|
||||||
// extract real url from gist
|
// 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);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
$.info('全部订阅同步成功!');
|
$.info('全部订阅同步成功!');
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
error: err
|
error: err,
|
||||||
});
|
});
|
||||||
$.info(`同步订阅失败,原因:${err}`);
|
$.info(`同步订阅失败,原因:${err}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteArtifact(req, res) {
|
async function deleteArtifact(req, res) {
|
||||||
const name = req.params.name;
|
const name = req.params.name;
|
||||||
$.info(`正在删除远程配置:${name}`);
|
$.info(`正在删除远程配置:${name}`);
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
try {
|
try {
|
||||||
const artifact = allArtifacts[name];
|
const artifact = allArtifacts[name];
|
||||||
if (!artifact) throw new Error(`远程配置:${name}不存在!`);
|
if (!artifact) throw new Error(`远程配置:${name}不存在!`);
|
||||||
if (artifact.updated) {
|
if (artifact.updated) {
|
||||||
// delete gist
|
// delete gist
|
||||||
await syncArtifact({
|
await syncArtifact({
|
||||||
filename: name,
|
filename: name,
|
||||||
content: ''
|
content: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// delete local cache
|
// delete local cache
|
||||||
delete allArtifacts[name];
|
delete allArtifacts[name];
|
||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success'
|
status: 'success',
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// delete local cache
|
// delete local cache
|
||||||
delete allArtifacts[name];
|
delete allArtifacts[name];
|
||||||
$.write(allArtifacts, ARTIFACTS_KEY);
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `无法删除远程配置:${name}, 原因:${err}`
|
message: `无法删除远程配置:${name}, 原因:${err}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllArtifacts(req, res) {
|
function getAllArtifacts(req, res) {
|
||||||
const allArtifacts = $.read(ARTIFACTS_KEY);
|
const allArtifacts = $.read(ARTIFACTS_KEY);
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: allArtifacts
|
data: allArtifacts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function syncArtifact(files) {
|
async function syncArtifact(files) {
|
||||||
const { gistToken } = $.read(SETTINGS_KEY);
|
const { gistToken } = $.read(SETTINGS_KEY);
|
||||||
if (!gistToken) {
|
if (!gistToken) {
|
||||||
return Promise.reject('未设置Gist Token!');
|
return Promise.reject('未设置Gist Token!');
|
||||||
}
|
}
|
||||||
const manager = new Gist({
|
const manager = new Gist({
|
||||||
token: gistToken,
|
token: gistToken,
|
||||||
key: ARTIFACT_REPOSITORY_KEY
|
key: ARTIFACT_REPOSITORY_KEY,
|
||||||
});
|
});
|
||||||
return manager.upload(files);
|
return manager.upload(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function produceArtifact(
|
async function produceArtifact(
|
||||||
{ type, item, platform, noProcessor } = {
|
{ type, item, platform, noProcessor } = {
|
||||||
platform: 'JSON',
|
platform: 'JSON',
|
||||||
noProcessor: false
|
noProcessor: false,
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
if (type === 'subscription') {
|
if (type === 'subscription') {
|
||||||
const sub = item;
|
const sub = item;
|
||||||
const raw = await download(sub.url, sub.ua);
|
const raw = await download(sub.url, sub.ua);
|
||||||
// parse proxies
|
// parse proxies
|
||||||
let proxies = ProxyUtils.parse(raw);
|
let proxies = ProxyUtils.parse(raw);
|
||||||
if (!noProcessor) {
|
if (!noProcessor) {
|
||||||
// apply processors
|
// apply processors
|
||||||
proxies = await ProxyUtils.process(proxies, sub.process || [], platform);
|
proxies = await ProxyUtils.process(
|
||||||
}
|
proxies,
|
||||||
// check duplicate
|
sub.process || [],
|
||||||
const exist = {};
|
platform,
|
||||||
for (const proxy of proxies) {
|
);
|
||||||
if (exist[proxy.name]) {
|
}
|
||||||
$.notify('🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』', '⚠️ 订阅包含重复节点!', '请仔细检测配置!', {
|
// check duplicate
|
||||||
'media-url':
|
const exist = {};
|
||||||
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png'
|
for (const proxy of proxies) {
|
||||||
});
|
if (exist[proxy.name]) {
|
||||||
break;
|
$.notify(
|
||||||
}
|
'🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』',
|
||||||
exist[proxy.name] = true;
|
'⚠️ 订阅包含重复节点!',
|
||||||
}
|
'请仔细检测配置!',
|
||||||
// produce
|
{
|
||||||
return ProxyUtils.produce(proxies, platform);
|
'media-url':
|
||||||
} else if (type === 'collection') {
|
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
|
||||||
const allSubs = $.read(SUBS_KEY);
|
},
|
||||||
const collection = item;
|
);
|
||||||
const subs = collection['subscriptions'];
|
break;
|
||||||
const results = {};
|
}
|
||||||
let processed = 0;
|
exist[proxy.name] = true;
|
||||||
|
}
|
||||||
|
// produce
|
||||||
|
return ProxyUtils.produce(proxies, platform);
|
||||||
|
} else if (type === 'collection') {
|
||||||
|
const allSubs = $.read(SUBS_KEY);
|
||||||
|
const collection = item;
|
||||||
|
const subs = collection['subscriptions'];
|
||||||
|
const results = {};
|
||||||
|
let processed = 0;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
subs.map(async (name) => {
|
subs.map(async (name) => {
|
||||||
const sub = allSubs[name];
|
const sub = allSubs[name];
|
||||||
try {
|
try {
|
||||||
$.info(`正在处理子订阅:${sub.name}...`);
|
$.info(`正在处理子订阅:${sub.name}...`);
|
||||||
const raw = await download(sub.url, sub.ua);
|
const raw = await download(sub.url, sub.ua);
|
||||||
// parse proxies
|
// parse proxies
|
||||||
let currentProxies = ProxyUtils.parse(raw);
|
let currentProxies = ProxyUtils.parse(raw);
|
||||||
if (!noProcessor) {
|
if (!noProcessor) {
|
||||||
// apply processors
|
// apply processors
|
||||||
currentProxies = await ProxyUtils.process(currentProxies, sub.process || [], platform);
|
currentProxies = await ProxyUtils.process(
|
||||||
}
|
currentProxies,
|
||||||
results[name] = currentProxies;
|
sub.process || [],
|
||||||
processed++;
|
platform,
|
||||||
$.info(`✅ 子订阅:${sub.name}加载成功,进度--${100 * (processed / subs.length).toFixed(1)}% `);
|
);
|
||||||
} catch (err) {
|
}
|
||||||
processed++;
|
results[name] = currentProxies;
|
||||||
$.error(
|
processed++;
|
||||||
`❌ 处理组合订阅中的子订阅: ${sub.name}时出现错误:${err},该订阅已被跳过!进度--${100 *
|
$.info(
|
||||||
(processed / subs.length).toFixed(1)}%`
|
`✅ 子订阅:${sub.name}加载成功,进度--${
|
||||||
);
|
100 * (processed / subs.length).toFixed(1)
|
||||||
}
|
}% `,
|
||||||
})
|
);
|
||||||
);
|
} catch (err) {
|
||||||
|
processed++;
|
||||||
|
$.error(
|
||||||
|
`❌ 处理组合订阅中的子订阅: ${
|
||||||
|
sub.name
|
||||||
|
}时出现错误:${err},该订阅已被跳过!进度--${
|
||||||
|
100 * (processed / subs.length).toFixed(1)
|
||||||
|
}%`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// merge proxies with the original order
|
// 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) {
|
if (!noProcessor) {
|
||||||
// apply own processors
|
// apply own processors
|
||||||
proxies = await ProxyUtils.process(proxies, collection.process || [], platform);
|
proxies = await ProxyUtils.process(
|
||||||
}
|
proxies,
|
||||||
if (proxies.length === 0) {
|
collection.process || [],
|
||||||
throw new Error(`组合订阅中不含有效节点!`);
|
platform,
|
||||||
}
|
);
|
||||||
// check duplicate
|
}
|
||||||
const exist = {};
|
if (proxies.length === 0) {
|
||||||
for (const proxy of proxies) {
|
throw new Error(`组合订阅中不含有效节点!`);
|
||||||
if (exist[proxy.name]) {
|
}
|
||||||
$.notify('🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』', '⚠️ 订阅包含重复节点!', '请仔细检测配置!', {
|
// check duplicate
|
||||||
'media-url':
|
const exist = {};
|
||||||
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png'
|
for (const proxy of proxies) {
|
||||||
});
|
if (exist[proxy.name]) {
|
||||||
break;
|
$.notify(
|
||||||
}
|
'🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』',
|
||||||
exist[proxy.name] = true;
|
'⚠️ 订阅包含重复节点!',
|
||||||
}
|
'请仔细检测配置!',
|
||||||
return ProxyUtils.produce(proxies, platform);
|
{
|
||||||
} else if (type === 'rule') {
|
'media-url':
|
||||||
const rule = item;
|
'https://cdn3.iconfinder.com/data/icons/seo-outline-1/512/25_code_program_programming_develop_bug_search_developer-512.png',
|
||||||
let rules = [];
|
},
|
||||||
for (let i = 0; i < rule.urls.length; i++) {
|
);
|
||||||
const url = rule.urls[i];
|
break;
|
||||||
$.info(`正在处理URL:${url},进度--${100 * ((i + 1) / rule.urls.length).toFixed(1)}% `);
|
}
|
||||||
try {
|
exist[proxy.name] = true;
|
||||||
const { body } = await download(url);
|
}
|
||||||
const currentRules = RuleUtils.parse(body);
|
return ProxyUtils.produce(proxies, platform);
|
||||||
rules = rules.concat(currentRules);
|
} else if (type === 'rule') {
|
||||||
} catch (err) {
|
const rule = item;
|
||||||
$.error(`处理分流订阅中的URL: ${url}时出现错误:${err}! 该订阅已被跳过。`);
|
let rules = [];
|
||||||
}
|
for (let i = 0; i < rule.urls.length; i++) {
|
||||||
}
|
const url = rule.urls[i];
|
||||||
// remove duplicates
|
$.info(
|
||||||
rules = await RuleUtils.process(rules, [ { type: 'Remove Duplicate Filter' } ]);
|
`正在处理URL:${url},进度--${
|
||||||
// produce output
|
100 * ((i + 1) / rule.urls.length).toFixed(1)
|
||||||
return RuleUtils.produce(rules, platform);
|
}% `,
|
||||||
}
|
);
|
||||||
|
try {
|
||||||
|
const { body } = await download(url);
|
||||||
|
const currentRules = RuleUtils.parse(body);
|
||||||
|
rules = rules.concat(currentRules);
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`处理分流订阅中的URL: ${url}时出现错误:${err}! 该订阅已被跳过。`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove duplicates
|
||||||
|
rules = await RuleUtils.process(rules, [
|
||||||
|
{ type: 'Remove Duplicate Filter' },
|
||||||
|
]);
|
||||||
|
// produce output
|
||||||
|
return RuleUtils.produce(rules, platform);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { register, produceArtifact };
|
export { register, produceArtifact };
|
||||||
|
@ -1,155 +1,166 @@
|
|||||||
const $ = require('../core/app');
|
import { getPlatformFromHeaders, getFlowHeaders } from './subscriptions';
|
||||||
const { SUBS_KEY, COLLECTIONS_KEY } = require('./constants');
|
import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
|
||||||
const { getPlatformFromHeaders, getFlowHeaders } = require('./subscriptions');
|
import { produceArtifact } from './artifacts';
|
||||||
const { produceArtifact } = require('./artifacts');
|
import $ from '../core/app';
|
||||||
|
|
||||||
function register($app) {
|
export function register($app) {
|
||||||
if (!$.read(COLLECTIONS_KEY)) $.write({}, COLLECTIONS_KEY);
|
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
|
// collection API
|
||||||
async function downloadCollection(req, res) {
|
async function downloadCollection(req, res) {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
const { raw } = req.query || 'false';
|
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 allCollections = $.read(COLLECTIONS_KEY);
|
||||||
const collection = allCollections[name];
|
const collection = allCollections[name];
|
||||||
|
|
||||||
$.info(`正在下载组合订阅:${name}`);
|
$.info(`正在下载组合订阅:${name}`);
|
||||||
|
|
||||||
// forward flow header from the first subscription in this collection
|
// forward flow header from the first subscription in this collection
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const subs = collection['subscriptions'];
|
const subs = collection['subscriptions'];
|
||||||
if (subs.length > 0) {
|
if (subs.length > 0) {
|
||||||
const sub = allSubs[subs[0]];
|
const sub = allSubs[subs[0]];
|
||||||
const flowInfo = await getFlowHeaders(sub.url);
|
const flowInfo = await getFlowHeaders(sub.url);
|
||||||
if (flowInfo) {
|
if (flowInfo) {
|
||||||
res.set('subscription-userinfo', flowInfo);
|
res.set('subscription-userinfo', flowInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collection) {
|
if (collection) {
|
||||||
try {
|
try {
|
||||||
const output = await produceArtifact({
|
const output = await produceArtifact({
|
||||||
type: 'collection',
|
type: 'collection',
|
||||||
item: collection,
|
item: collection,
|
||||||
platform,
|
platform,
|
||||||
noProcessor: raw
|
noProcessor: raw,
|
||||||
});
|
});
|
||||||
if (platform === 'JSON') {
|
if (platform === 'JSON') {
|
||||||
res.set('Content-Type', 'application/json;charset=utf-8').send(output);
|
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
||||||
} else {
|
output,
|
||||||
res.send(output);
|
);
|
||||||
}
|
} else {
|
||||||
} catch (err) {
|
res.send(output);
|
||||||
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`, `❌ 下载组合订阅错误:${name}!`, `🤔 原因:${err}`);
|
}
|
||||||
res.status(500).json({
|
} catch (err) {
|
||||||
status: 'failed',
|
$.notify(
|
||||||
message: err
|
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
|
||||||
});
|
`❌ 下载组合订阅错误:${name}!`,
|
||||||
}
|
`🤔 原因:${err}`,
|
||||||
} else {
|
);
|
||||||
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`, `❌ 未找到组合订阅:${name}!`);
|
res.status(500).json({
|
||||||
res.status(404).json({
|
status: 'failed',
|
||||||
status: 'failed'
|
message: err,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
$.notify(
|
||||||
|
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载组合订阅失败`,
|
||||||
|
`❌ 未找到组合订阅:${name}!`,
|
||||||
|
);
|
||||||
|
res.status(404).json({
|
||||||
|
status: 'failed',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCollection(req, res) {
|
function createCollection(req, res) {
|
||||||
const collection = req.body;
|
const collection = req.body;
|
||||||
$.info(`正在创建组合订阅:${collection.name}`);
|
$.info(`正在创建组合订阅:${collection.name}`);
|
||||||
const allCol = $.read(COLLECTIONS_KEY);
|
const allCol = $.read(COLLECTIONS_KEY);
|
||||||
if (allCol[collection.name]) {
|
if (allCol[collection.name]) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `订阅集${collection.name}已存在!`
|
message: `订阅集${collection.name}已存在!`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// validate name
|
// validate name
|
||||||
if (/^[\w-_]*$/.test(collection.name)) {
|
if (/^[\w-_]*$/.test(collection.name)) {
|
||||||
allCol[collection.name] = collection;
|
allCol[collection.name] = collection;
|
||||||
$.write(allCol, COLLECTIONS_KEY);
|
$.write(allCol, COLLECTIONS_KEY);
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: collection
|
data: collection,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `订阅集名称 ${collection.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
|
message: `订阅集名称 ${collection.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCollection(req, res) {
|
function getCollection(req, res) {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
const collection = $.read(COLLECTIONS_KEY)[name];
|
const collection = $.read(COLLECTIONS_KEY)[name];
|
||||||
if (collection) {
|
if (collection) {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: collection
|
data: collection,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `未找到订阅集:${name}!`
|
message: `未找到订阅集:${name}!`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCollection(req, res) {
|
function updateCollection(req, res) {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
let collection = req.body;
|
let collection = req.body;
|
||||||
const allCol = $.read(COLLECTIONS_KEY);
|
const allCol = $.read(COLLECTIONS_KEY);
|
||||||
if (allCol[name]) {
|
if (allCol[name]) {
|
||||||
const newCol = {
|
const newCol = {
|
||||||
...allCol[name],
|
...allCol[name],
|
||||||
...collection
|
...collection,
|
||||||
};
|
};
|
||||||
$.info(`正在更新组合订阅:${name}...`);
|
$.info(`正在更新组合订阅:${name}...`);
|
||||||
// allow users to update collection name
|
// allow users to update collection name
|
||||||
delete allCol[name];
|
delete allCol[name];
|
||||||
allCol[collection.name || name] = newCol;
|
allCol[collection.name || name] = newCol;
|
||||||
$.write(allCol, COLLECTIONS_KEY);
|
$.write(allCol, COLLECTIONS_KEY);
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: newCol
|
data: newCol,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `订阅集${name}不存在,无法更新!`
|
message: `订阅集${name}不存在,无法更新!`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteCollection(req, res) {
|
function deleteCollection(req, res) {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
$.info(`正在删除组合订阅:${name}`);
|
$.info(`正在删除组合订阅:${name}`);
|
||||||
let allCol = $.read(COLLECTIONS_KEY);
|
let allCol = $.read(COLLECTIONS_KEY);
|
||||||
delete allCol[name];
|
delete allCol[name];
|
||||||
$.write(allCol, COLLECTIONS_KEY);
|
$.write(allCol, COLLECTIONS_KEY);
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success'
|
status: 'success',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllCollections(req, res) {
|
function getAllCollections(req, res) {
|
||||||
const allCols = $.read(COLLECTIONS_KEY);
|
const allCols = $.read(COLLECTIONS_KEY);
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: allCols
|
data: allCols,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
register
|
|
||||||
};
|
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
module.exports = {
|
export const SETTINGS_KEY = 'settings';
|
||||||
SETTINGS_KEY: 'settings',
|
export const SUBS_KEY = 'subs';
|
||||||
SUBS_KEY: 'subs',
|
export const COLLECTIONS_KEY = 'collections';
|
||||||
COLLECTIONS_KEY: 'collections',
|
export const ARTIFACTS_KEY = 'artifacts';
|
||||||
RULES_KEY: 'rules',
|
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';
|
||||||
BUILT_IN_KEY: 'builtin',
|
export const GIST_BACKUP_FILE_NAME = 'Sub-Store';
|
||||||
ARTIFACTS_KEY: 'artifacts',
|
export const ARTIFACT_REPOSITORY_KEY = 'Sub-Store Artifacts Repository';
|
||||||
GIST_BACKUP_KEY: 'Auto Generated Sub-Store Backup',
|
|
||||||
GIST_BACKUP_FILE_NAME: 'Sub-Store',
|
|
||||||
ARTIFACT_REPOSITORY_KEY: 'Sub-Store Artifacts Repository'
|
|
||||||
};
|
|
||||||
|
@ -1,115 +1,120 @@
|
|||||||
const $ = require('../core/app');
|
import {
|
||||||
const { ENV } = require('../utils/open-api');
|
SETTINGS_KEY,
|
||||||
const { IP_API } = require('../utils/geo');
|
GIST_BACKUP_KEY,
|
||||||
const Gist = require('../utils/gist');
|
GIST_BACKUP_FILE_NAME,
|
||||||
const { SETTINGS_KEY, GIST_BACKUP_KEY, GIST_BACKUP_FILE_NAME } = require('./constants');
|
} 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() {
|
export default function serve() {
|
||||||
const express = require('../utils/express');
|
const $app = express();
|
||||||
const $app = express();
|
|
||||||
|
|
||||||
// register routes
|
// register routes
|
||||||
const collections = require('./collections');
|
const collections = require('./collections');
|
||||||
collections.register($app);
|
collections.register($app);
|
||||||
|
|
||||||
const subscriptions = require('./subscriptions');
|
const subscriptions = require('./subscriptions');
|
||||||
subscriptions.register($app);
|
subscriptions.register($app);
|
||||||
|
|
||||||
const settings = require('./settings');
|
const settings = require('./settings');
|
||||||
settings.register($app);
|
settings.register($app);
|
||||||
|
|
||||||
const artifacts = require('./artifacts');
|
const artifacts = require('./artifacts');
|
||||||
artifacts.register($app);
|
artifacts.register($app);
|
||||||
|
|
||||||
// utils
|
// utils
|
||||||
$app.get('/api/utils/IP_API/:server', IP_API); // IP-API reverse proxy
|
$app.get('/api/utils/IP_API/:server', IP_API); // IP-API reverse proxy
|
||||||
$app.get('/api/utils/env', getEnv); // get runtime environment
|
$app.get('/api/utils/env', getEnv); // get runtime environment
|
||||||
$app.get('/api/utils/backup', gistBackup); // gist backup actions
|
$app.get('/api/utils/backup', gistBackup); // gist backup actions
|
||||||
|
|
||||||
// Redirect sub.store to vercel webpage
|
// Redirect sub.store to vercel webpage
|
||||||
$app.get('/', async (req, res) => {
|
$app.get('/', async (req, res) => {
|
||||||
// 302 redirect
|
// 302 redirect
|
||||||
res.set('location', 'https://sub-store.vercel.app/').status(302).end();
|
res.set('location', 'https://sub-store.vercel.app/').status(302).end();
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle preflight request for QX
|
// handle preflight request for QX
|
||||||
if (ENV().isQX) {
|
if (ENV().isQX) {
|
||||||
$app.options('/', async (req, res) => {
|
$app.options('/', async (req, res) => {
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$app.all('/', (_, res) => {
|
$app.all('/', (_, res) => {
|
||||||
res.send('Hello from sub-store, made with ❤️ by Peng-YM');
|
res.send('Hello from sub-store, made with ❤️ by Peng-YM');
|
||||||
});
|
});
|
||||||
|
|
||||||
$app.start();
|
$app.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEnv(req, res) {
|
function getEnv(req, res) {
|
||||||
const { isNode, isQX, isLoon, isSurge } = ENV();
|
const { isNode, isQX, isLoon, isSurge } = ENV();
|
||||||
let backend = 'Node';
|
let backend = 'Node';
|
||||||
if (isNode) backend = 'Node';
|
if (isNode) backend = 'Node';
|
||||||
if (isQX) backend = 'QX';
|
if (isQX) backend = 'QX';
|
||||||
if (isLoon) backend = 'Loon';
|
if (isLoon) backend = 'Loon';
|
||||||
if (isSurge) backend = 'Surge';
|
if (isSurge) backend = 'Surge';
|
||||||
res.json({
|
res.json({
|
||||||
backend
|
backend,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function gistBackup(req, res) {
|
async function gistBackup(req, res) {
|
||||||
const { action } = req.query;
|
const { action } = req.query;
|
||||||
// read token
|
// read token
|
||||||
const { gistToken } = $.read(SETTINGS_KEY);
|
const { gistToken } = $.read(SETTINGS_KEY);
|
||||||
if (!gistToken) {
|
if (!gistToken) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: '未找到Gist备份Token!'
|
message: '未找到Gist备份Token!',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const gist = new Gist({
|
const gist = new Gist({
|
||||||
token: gistToken,
|
token: gistToken,
|
||||||
key: GIST_BACKUP_KEY
|
key: GIST_BACKUP_KEY,
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
let content;
|
let content;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'upload':
|
case 'upload':
|
||||||
// update syncTime.
|
// update syncTime.
|
||||||
const settings = $.read(SETTINGS_KEY);
|
const settings = $.read(SETTINGS_KEY);
|
||||||
settings.syncTime = new Date().getTime();
|
settings.syncTime = new Date().getTime();
|
||||||
$.write(settings, SETTINGS_KEY);
|
$.write(settings, SETTINGS_KEY);
|
||||||
content = $.read('#sub-store');
|
content = $.read('#sub-store');
|
||||||
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
|
if ($.env.isNode)
|
||||||
$.info(`上传备份中...`);
|
content = JSON.stringify($.cache, null, ` `);
|
||||||
await gist.upload({ [GIST_BACKUP_FILE_NAME]: { content } });
|
$.info(`上传备份中...`);
|
||||||
break;
|
await gist.upload({ [GIST_BACKUP_FILE_NAME]: { content } });
|
||||||
case 'download':
|
break;
|
||||||
$.info(`还原备份中...`);
|
case 'download':
|
||||||
content = await gist.download(GIST_BACKUP_FILE_NAME);
|
$.info(`还原备份中...`);
|
||||||
// restore settings
|
content = await gist.download(GIST_BACKUP_FILE_NAME);
|
||||||
$.write(content, '#sub-store');
|
// restore settings
|
||||||
if ($.env.isNode) {
|
$.write(content, '#sub-store');
|
||||||
content = JSON.parse(content);
|
if ($.env.isNode) {
|
||||||
Object.keys(content).forEach((key) => {
|
content = JSON.parse(content);
|
||||||
$.write(content[key], key);
|
Object.keys(content).forEach((key) => {
|
||||||
});
|
$.write(content[key], key);
|
||||||
}
|
});
|
||||||
break;
|
}
|
||||||
}
|
break;
|
||||||
res.json({
|
}
|
||||||
status: 'success'
|
res.json({
|
||||||
});
|
status: 'success',
|
||||||
} catch (err) {
|
});
|
||||||
const msg = `${action === 'upload' ? '上传' : '下载'}备份失败!${err}`;
|
} catch (err) {
|
||||||
$.error(msg);
|
const msg = `${
|
||||||
res.status(500).json({
|
action === 'upload' ? '上传' : '下载'
|
||||||
status: 'failed',
|
}备份失败!${err}`;
|
||||||
message: msg
|
$.error(msg);
|
||||||
});
|
res.status(500).json({
|
||||||
}
|
status: 'failed',
|
||||||
}
|
message: msg,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = serve;
|
|
||||||
|
@ -1,32 +1,27 @@
|
|||||||
const $ = require('../core/app');
|
import { SETTINGS_KEY } from './constants';
|
||||||
const { SETTINGS_KEY } = require('./constants');
|
import $ from '../core/app';
|
||||||
|
|
||||||
function register($app) {
|
export function register($app) {
|
||||||
if (!$.read(SETTINGS_KEY)) $.write({}, SETTINGS_KEY);
|
if (!$.read(SETTINGS_KEY)) $.write({}, SETTINGS_KEY);
|
||||||
|
$app.route('/api/settings').get(getSettings).patch(updateSettings);
|
||||||
$app.route('/api/settings').get(getSettings).patch(updateSettings);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSettings(req, res) {
|
function getSettings(req, res) {
|
||||||
const settings = $.read(SETTINGS_KEY);
|
const settings = $.read(SETTINGS_KEY);
|
||||||
res.json(settings);
|
res.json(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSettings(req, res) {
|
function updateSettings(req, res) {
|
||||||
const data = req.body;
|
const data = req.body;
|
||||||
const settings = $.read(SETTINGS_KEY);
|
const settings = $.read(SETTINGS_KEY);
|
||||||
$.write(
|
$.write(
|
||||||
{
|
{
|
||||||
...settings,
|
...settings,
|
||||||
...data
|
...data,
|
||||||
},
|
},
|
||||||
SETTINGS_KEY
|
SETTINGS_KEY,
|
||||||
);
|
);
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success'
|
status: 'success',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
register
|
|
||||||
};
|
|
||||||
|
@ -1,205 +1,216 @@
|
|||||||
const $ = require("../core/app");
|
import { SUBS_KEY, COLLECTIONS_KEY } from './constants';
|
||||||
const { produceArtifact } = require('./artifacts');
|
import { produceArtifact } from './artifacts';
|
||||||
const { SUBS_KEY, COLLECTIONS_KEY } = require('./constants');
|
import $ from '../core/app';
|
||||||
|
|
||||||
function register($app) {
|
export function register($app) {
|
||||||
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
|
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
|
||||||
|
|
||||||
$app.get('/download/:name', downloadSubscription);
|
$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);
|
$app.route('/api/subs').get(getAllSubscriptions).post(createSubscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
// subscriptions API
|
// subscriptions API
|
||||||
async function downloadSubscription(req, res) {
|
async function downloadSubscription(req, res) {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
const { raw } = req.query || 'false';
|
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}`);
|
$.info(`正在下载订阅:${name}`);
|
||||||
|
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const sub = allSubs[name];
|
const sub = allSubs[name];
|
||||||
if (sub) {
|
if (sub) {
|
||||||
try {
|
try {
|
||||||
const output = await produceArtifact({
|
const output = await produceArtifact({
|
||||||
type: 'subscription',
|
type: 'subscription',
|
||||||
item: sub,
|
item: sub,
|
||||||
platform,
|
platform,
|
||||||
noProcessor: raw
|
noProcessor: raw,
|
||||||
});
|
});
|
||||||
|
|
||||||
// forward flow headers
|
// forward flow headers
|
||||||
const flowInfo = await getFlowHeaders(sub.url);
|
const flowInfo = await getFlowHeaders(sub.url);
|
||||||
if (flowInfo) {
|
if (flowInfo) {
|
||||||
res.set('subscription-userinfo', flowInfo);
|
res.set('subscription-userinfo', flowInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (platform === 'JSON') {
|
if (platform === 'JSON') {
|
||||||
res.set('Content-Type', 'application/json;charset=utf-8').send(output);
|
res.set('Content-Type', 'application/json;charset=utf-8').send(
|
||||||
} else {
|
output,
|
||||||
res.send(output);
|
);
|
||||||
}
|
} else {
|
||||||
} catch (err) {
|
res.send(output);
|
||||||
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 无法下载订阅:${name}!`, `🤔 原因:${JSON.stringify(err)}`);
|
}
|
||||||
$.error(JSON.stringify(err));
|
} catch (err) {
|
||||||
res.status(500).json({
|
$.notify(
|
||||||
status: 'failed',
|
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`,
|
||||||
message: err
|
`❌ 无法下载订阅:${name}!`,
|
||||||
});
|
`🤔 原因:${JSON.stringify(err)}`,
|
||||||
}
|
);
|
||||||
} else {
|
$.error(JSON.stringify(err));
|
||||||
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 未找到订阅:${name}!`);
|
res.status(500).json({
|
||||||
res.status(404).json({
|
status: 'failed',
|
||||||
status: 'failed'
|
message: err,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
$.notify(`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 下载订阅失败`, `❌ 未找到订阅:${name}!`);
|
||||||
|
res.status(404).json({
|
||||||
|
status: 'failed',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSubscription(req, res) {
|
function createSubscription(req, res) {
|
||||||
const sub = req.body;
|
const sub = req.body;
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
$.info(`正在创建订阅: ${sub.name}`);
|
$.info(`正在创建订阅: ${sub.name}`);
|
||||||
if (allSubs[sub.name]) {
|
if (allSubs[sub.name]) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `订阅${sub.name}已存在!`
|
message: `订阅${sub.name}已存在!`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// validate name
|
// validate name
|
||||||
if (/^[\w-_]*$/.test(sub.name)) {
|
if (/^[\w-_]*$/.test(sub.name)) {
|
||||||
allSubs[sub.name] = sub;
|
allSubs[sub.name] = sub;
|
||||||
$.write(allSubs, SUBS_KEY);
|
$.write(allSubs, SUBS_KEY);
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: sub
|
data: sub,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`
|
message: `订阅名称 ${sub.name} 中含有非法字符!名称中只能包含英文字母、数字、下划线、横杠。`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubscription(req, res) {
|
function getSubscription(req, res) {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
const sub = $.read(SUBS_KEY)[name];
|
const sub = $.read(SUBS_KEY)[name];
|
||||||
if (sub) {
|
if (sub) {
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: sub
|
data: sub,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `未找到订阅:${name}!`
|
message: `未找到订阅:${name}!`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSubscription(req, res) {
|
function updateSubscription(req, res) {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
let sub = req.body;
|
let sub = req.body;
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
if (allSubs[name]) {
|
if (allSubs[name]) {
|
||||||
const newSub = {
|
const newSub = {
|
||||||
...allSubs[name],
|
...allSubs[name],
|
||||||
...sub
|
...sub,
|
||||||
};
|
};
|
||||||
$.info(`正在更新订阅: ${name}`);
|
$.info(`正在更新订阅: ${name}`);
|
||||||
// allow users to update the subscription name
|
// allow users to update the subscription name
|
||||||
if (name !== sub.name) {
|
if (name !== sub.name) {
|
||||||
// we need to find out all collections refer to this name
|
// we need to find out all collections refer to this name
|
||||||
const allCols = $.read(COLLECTIONS_KEY);
|
const allCols = $.read(COLLECTIONS_KEY);
|
||||||
for (const k of Object.keys(allCols)) {
|
for (const k of Object.keys(allCols)) {
|
||||||
const idx = allCols[k].subscriptions.indexOf(name);
|
const idx = allCols[k].subscriptions.indexOf(name);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
allCols[k].subscriptions[idx] = sub.name;
|
allCols[k].subscriptions[idx] = sub.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// update subscriptions
|
// update subscriptions
|
||||||
delete allSubs[name];
|
delete allSubs[name];
|
||||||
allSubs[sub.name] = newSub;
|
allSubs[sub.name] = newSub;
|
||||||
} else {
|
} else {
|
||||||
allSubs[name] = newSub;
|
allSubs[name] = newSub;
|
||||||
}
|
}
|
||||||
$.write(allSubs, SUBS_KEY);
|
$.write(allSubs, SUBS_KEY);
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: newSub
|
data: newSub,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `订阅${name}不存在,无法更新!`
|
message: `订阅${name}不存在,无法更新!`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSubscription(req, res) {
|
function deleteSubscription(req, res) {
|
||||||
const { name } = req.params;
|
const { name } = req.params;
|
||||||
$.info(`删除订阅:${name}...`);
|
$.info(`删除订阅:${name}...`);
|
||||||
// delete from subscriptions
|
// delete from subscriptions
|
||||||
let allSubs = $.read(SUBS_KEY);
|
let allSubs = $.read(SUBS_KEY);
|
||||||
delete allSubs[name];
|
delete allSubs[name];
|
||||||
$.write(allSubs, SUBS_KEY);
|
$.write(allSubs, SUBS_KEY);
|
||||||
// delete from collections
|
// delete from collections
|
||||||
let allCols = $.read(COLLECTIONS_KEY);
|
let allCols = $.read(COLLECTIONS_KEY);
|
||||||
for (const k of Object.keys(allCols)) {
|
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'
|
$.write(allCols, COLLECTIONS_KEY);
|
||||||
});
|
res.json({
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllSubscriptions(req, res) {
|
function getAllSubscriptions(req, res) {
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
res.json({
|
res.json({
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: allSubs
|
data: allSubs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getFlowHeaders(url) {
|
export async function getFlowHeaders(url) {
|
||||||
const { headers } = await $.http.get({
|
const { headers } = await $.http.get({
|
||||||
url,
|
url,
|
||||||
headers: {
|
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) =>
|
||||||
return headers[subkey];
|
/SUBSCRIPTION-USERINFO/i.test(k),
|
||||||
|
)[0];
|
||||||
|
return headers[subkey];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlatformFromHeaders(headers) {
|
export function getPlatformFromHeaders(headers) {
|
||||||
const keys = Object.keys(headers);
|
const keys = Object.keys(headers);
|
||||||
let UA = '';
|
let UA = '';
|
||||||
for (let k of keys) {
|
for (let k of keys) {
|
||||||
if (/USER-AGENT/i.test(k)) {
|
if (/USER-AGENT/i.test(k)) {
|
||||||
UA = headers[k];
|
UA = headers[k];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (UA.indexOf('Quantumult%20X') !== -1) {
|
if (UA.indexOf('Quantumult%20X') !== -1) {
|
||||||
return 'QX';
|
return 'QX';
|
||||||
} else if (UA.indexOf('Surge') !== -1) {
|
} else if (UA.indexOf('Surge') !== -1) {
|
||||||
return 'Surge';
|
return 'Surge';
|
||||||
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
|
} else if (UA.indexOf('Decar') !== -1 || UA.indexOf('Loon') !== -1) {
|
||||||
return 'Loon';
|
return 'Loon';
|
||||||
} else if (UA.indexOf('Stash') !== -1 || UA.indexOf('Shadowrocket') !== -1) {
|
} else if (
|
||||||
return 'Clash';
|
UA.indexOf('Stash') !== -1 ||
|
||||||
} else {
|
UA.indexOf('Shadowrocket') !== -1
|
||||||
return null;
|
) {
|
||||||
}
|
return 'Clash';
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
register,
|
|
||||||
getPlatformFromHeaders,
|
|
||||||
getFlowHeaders
|
|
||||||
};
|
|
||||||
|
@ -12,12 +12,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`
|
`
|
||||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||||
𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 © 𝑷𝒆𝒏𝒈-𝒀𝑴
|
𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 © 𝑷𝒆𝒏𝒈-𝒀𝑴
|
||||||
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅
|
||||||
`
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const serve = require('./facade');
|
import serve from './facade';
|
||||||
serve();
|
|
||||||
|
serve();
|
||||||
|
@ -1,30 +1,29 @@
|
|||||||
const { HTTP } = require('./open-api');
|
import { HTTP } from './open-api';
|
||||||
|
|
||||||
const cache = new Map();
|
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)';
|
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
|
||||||
const id = ua + url;
|
const id = ua + url;
|
||||||
if (cache.has(id)) {
|
if (cache.has(id)) {
|
||||||
return cache.get(id);
|
return cache.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $http = HTTP({
|
const $http = HTTP({
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': ua
|
'User-Agent': ua,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = new Promise((resolve, reject) => {
|
const result = new Promise((resolve, reject) => {
|
||||||
$http.get(url).then((resp) => {
|
$http.get(url).then((resp) => {
|
||||||
const body = resp.body;
|
const body = resp.body;
|
||||||
if (body.replace(/\s/g, '').length === 0) reject(new Error('订阅内容为空!'));
|
if (body.replace(/\s/g, '').length === 0)
|
||||||
else resolve(body);
|
reject(new Error('订阅内容为空!'));
|
||||||
});
|
else resolve(body);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
cache[id] = result;
|
cache.set(id, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = download;
|
|
||||||
|
@ -1,275 +1,285 @@
|
|||||||
const $ = require('../core/app');
|
import { ENV } from './open-api';
|
||||||
const { ENV } = require('./open-api');
|
import $ from '../core/app';
|
||||||
|
|
||||||
function express({ port } = { port: 3000 }) {
|
export default function express({ port } = { port: 3000 }) {
|
||||||
const { isNode } = ENV();
|
const { isNode } = ENV();
|
||||||
const DEFAULT_HEADERS = {
|
const DEFAULT_HEADERS = {
|
||||||
'Content-Type': 'text/plain;charset=UTF-8',
|
'Content-Type': 'text/plain;charset=UTF-8',
|
||||||
'Access-Control-Allow-Origin': '*',
|
'Access-Control-Allow-Origin': '*',
|
||||||
'Access-Control-Allow-Methods': 'POST,GET,OPTIONS,PATCH,PUT,DELETE',
|
'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
|
// node support
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
const express_ = eval(`require("express")`);
|
const express_ = eval(`require("express")`);
|
||||||
const bodyParser = eval(`require("body-parser")`);
|
const bodyParser = eval(`require("body-parser")`);
|
||||||
const app = express_();
|
const app = express_();
|
||||||
app.use(bodyParser.json({ verify: rawBodySaver }));
|
app.use(bodyParser.json({ verify: rawBodySaver }));
|
||||||
app.use(bodyParser.urlencoded({ verify: rawBodySaver, extended: true }));
|
app.use(
|
||||||
app.use(bodyParser.raw({ verify: rawBodySaver, type: '*/*' }));
|
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
|
||||||
app.use((_, res, next) => {
|
);
|
||||||
res.set(DEFAULT_HEADERS);
|
app.use(bodyParser.raw({ verify: rawBodySaver, type: '*/*' }));
|
||||||
next();
|
app.use((_, res, next) => {
|
||||||
});
|
res.set(DEFAULT_HEADERS);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// adapter
|
// adapter
|
||||||
app.start = () => {
|
app.start = () => {
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
$.log(`Express started on port: ${port}`);
|
$.log(`Express started on port: ${port}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
// route handlers
|
// route handlers
|
||||||
const handlers = [];
|
const handlers = [];
|
||||||
|
|
||||||
// http methods
|
// 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
|
// dispatch url to route
|
||||||
const dispatch = (request, start = 0) => {
|
const dispatch = (request, start = 0) => {
|
||||||
let { method, url, headers, body } = request;
|
let { method, url, headers, body } = request;
|
||||||
if (/json/i.test(headers['Content-Type'])) {
|
if (/json/i.test(headers['Content-Type'])) {
|
||||||
body = JSON.parse(body);
|
body = JSON.parse(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
method = method.toUpperCase();
|
method = method.toUpperCase();
|
||||||
const { path, query } = extractURL(url);
|
const { path, query } = extractURL(url);
|
||||||
|
|
||||||
// pattern match
|
// pattern match
|
||||||
let handler = null;
|
let handler = null;
|
||||||
let i;
|
let i;
|
||||||
let longestMatchedPattern = 0;
|
let longestMatchedPattern = 0;
|
||||||
for (i = start; i < handlers.length; i++) {
|
for (i = start; i < handlers.length; i++) {
|
||||||
if (handlers[i].method === 'ALL' || method === handlers[i].method) {
|
if (handlers[i].method === 'ALL' || method === handlers[i].method) {
|
||||||
const { pattern } = handlers[i];
|
const { pattern } = handlers[i];
|
||||||
if (patternMatched(pattern, path)) {
|
if (patternMatched(pattern, path)) {
|
||||||
if (pattern.split('/').length > longestMatchedPattern) {
|
if (pattern.split('/').length > longestMatchedPattern) {
|
||||||
handler = handlers[i];
|
handler = handlers[i];
|
||||||
longestMatchedPattern = pattern.split('/').length;
|
longestMatchedPattern = pattern.split('/').length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (handler) {
|
if (handler) {
|
||||||
// dispatch to next handler
|
// dispatch to next handler
|
||||||
const next = () => {
|
const next = () => {
|
||||||
dispatch(method, url, i);
|
dispatch(method, url, i);
|
||||||
};
|
};
|
||||||
const req = {
|
const req = {
|
||||||
method,
|
method,
|
||||||
url,
|
url,
|
||||||
path,
|
path,
|
||||||
query,
|
query,
|
||||||
params: extractPathParams(handler.pattern, path),
|
params: extractPathParams(handler.pattern, path),
|
||||||
headers,
|
headers,
|
||||||
body
|
body,
|
||||||
};
|
};
|
||||||
const res = Response();
|
const res = Response();
|
||||||
const cb = handler.callback;
|
const cb = handler.callback;
|
||||||
|
|
||||||
const errFunc = (err) => {
|
const errFunc = (err) => {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: `Internal Server Error: ${err}`
|
message: `Internal Server Error: ${err}`,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (cb.constructor.name === 'AsyncFunction') {
|
if (cb.constructor.name === 'AsyncFunction') {
|
||||||
cb(req, res, next).catch(errFunc);
|
cb(req, res, next).catch(errFunc);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
cb(req, res, next);
|
cb(req, res, next);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errFunc(err);
|
errFunc(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// no route, return 404
|
// no route, return 404
|
||||||
const res = Response();
|
const res = Response();
|
||||||
res.status(404).json({
|
res.status(404).json({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
message: 'ERROR: 404 not found'
|
message: 'ERROR: 404 not found',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const app = {};
|
const app = {};
|
||||||
|
|
||||||
// attach http methods
|
// attach http methods
|
||||||
METHODS_NAMES.forEach((method) => {
|
METHODS_NAMES.forEach((method) => {
|
||||||
app[method.toLowerCase()] = (pattern, callback) => {
|
app[method.toLowerCase()] = (pattern, callback) => {
|
||||||
// add handler
|
// add handler
|
||||||
handlers.push({ method, pattern, callback });
|
handlers.push({ method, pattern, callback });
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// chainable route
|
// chainable route
|
||||||
app.route = (pattern) => {
|
app.route = (pattern) => {
|
||||||
const chainApp = {};
|
const chainApp = {};
|
||||||
METHODS_NAMES.forEach((method) => {
|
METHODS_NAMES.forEach((method) => {
|
||||||
chainApp[method.toLowerCase()] = (callback) => {
|
chainApp[method.toLowerCase()] = (callback) => {
|
||||||
// add handler
|
// add handler
|
||||||
handlers.push({ method, pattern, callback });
|
handlers.push({ method, pattern, callback });
|
||||||
return chainApp;
|
return chainApp;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return chainApp;
|
return chainApp;
|
||||||
};
|
};
|
||||||
|
|
||||||
// start service
|
// start service
|
||||||
app.start = () => {
|
app.start = () => {
|
||||||
dispatch($request);
|
dispatch($request);
|
||||||
};
|
};
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
|
||||||
/************************************************
|
/************************************************
|
||||||
Utility Functions
|
Utility Functions
|
||||||
*************************************************/
|
*************************************************/
|
||||||
function rawBodySaver(req, res, buf, encoding) {
|
function rawBodySaver(req, res, buf, encoding) {
|
||||||
if (buf && buf.length) {
|
if (buf && buf.length) {
|
||||||
req.rawBody = buf.toString(encoding || 'utf8');
|
req.rawBody = buf.toString(encoding || 'utf8');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Response() {
|
function Response() {
|
||||||
let statusCode = 200;
|
let statusCode = 200;
|
||||||
const { isQX, isLoon, isSurge } = ENV();
|
const { isQX, isLoon, isSurge } = ENV();
|
||||||
const headers = DEFAULT_HEADERS;
|
const headers = DEFAULT_HEADERS;
|
||||||
const STATUS_CODE_MAP = {
|
const STATUS_CODE_MAP = {
|
||||||
200: 'HTTP/1.1 200 OK',
|
200: 'HTTP/1.1 200 OK',
|
||||||
201: 'HTTP/1.1 201 Created',
|
201: 'HTTP/1.1 201 Created',
|
||||||
302: 'HTTP/1.1 302 Found',
|
302: 'HTTP/1.1 302 Found',
|
||||||
307: 'HTTP/1.1 307 Temporary Redirect',
|
307: 'HTTP/1.1 307 Temporary Redirect',
|
||||||
308: 'HTTP/1.1 308 Permanent Redirect',
|
308: 'HTTP/1.1 308 Permanent Redirect',
|
||||||
404: 'HTTP/1.1 404 Not Found',
|
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) {
|
status(code) {
|
||||||
statusCode = code;
|
statusCode = code;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
send(body = '') {
|
send(body = '') {
|
||||||
const response = {
|
const response = {
|
||||||
status: isQX ? STATUS_CODE_MAP[statusCode] : statusCode,
|
status: isQX ? STATUS_CODE_MAP[statusCode] : statusCode,
|
||||||
body,
|
body,
|
||||||
headers
|
headers,
|
||||||
};
|
};
|
||||||
if (isQX) {
|
if (isQX) {
|
||||||
$done(response);
|
$done(response);
|
||||||
} else if (isLoon || isSurge) {
|
} else if (isLoon || isSurge) {
|
||||||
$done({
|
$done({
|
||||||
response
|
response,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
end() {
|
end() {
|
||||||
this.send();
|
this.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
html(data) {
|
html(data) {
|
||||||
this.set('Content-Type', 'text/html;charset=UTF-8');
|
this.set('Content-Type', 'text/html;charset=UTF-8');
|
||||||
this.send(data);
|
this.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
json(data) {
|
json(data) {
|
||||||
this.set('Content-Type', 'application/json;charset=UTF-8');
|
this.set('Content-Type', 'application/json;charset=UTF-8');
|
||||||
this.send(JSON.stringify(data));
|
this.send(JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key, val) {
|
set(key, val) {
|
||||||
headers[key] = val;
|
headers[key] = val;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
function patternMatched(pattern, path) {
|
function patternMatched(pattern, path) {
|
||||||
if (pattern instanceof RegExp && pattern.test(path)) {
|
if (pattern instanceof RegExp && pattern.test(path)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// root pattern, match all
|
// root pattern, match all
|
||||||
if (pattern === '/') return true;
|
if (pattern === '/') return true;
|
||||||
// normal string pattern
|
// normal string pattern
|
||||||
if (pattern.indexOf(':') === -1) {
|
if (pattern.indexOf(':') === -1) {
|
||||||
const spath = path.split('/');
|
const spath = path.split('/');
|
||||||
const spattern = pattern.split('/');
|
const spattern = pattern.split('/');
|
||||||
for (let i = 0; i < spattern.length; i++) {
|
for (let i = 0; i < spattern.length; i++) {
|
||||||
if (spath[i] !== spattern[i]) {
|
if (spath[i] !== spattern[i]) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} else if (extractPathParams(pattern, path)) {
|
} else if (extractPathParams(pattern, path)) {
|
||||||
// string pattern with path parameters
|
// string pattern with path parameters
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractURL(url) {
|
function extractURL(url) {
|
||||||
// extract path
|
// extract path
|
||||||
const match = url.match(/https?:\/\/[^\/]+(\/[^?]*)/) || [];
|
const match = url.match(/https?:\/\/[^\/]+(\/[^?]*)/) || [];
|
||||||
const path = match[1] || '/';
|
const path = match[1] || '/';
|
||||||
|
|
||||||
// extract query string
|
// extract query string
|
||||||
const split = url.indexOf('?');
|
const split = url.indexOf('?');
|
||||||
const query = {};
|
const query = {};
|
||||||
if (split !== -1) {
|
if (split !== -1) {
|
||||||
let hashes = url.slice(url.indexOf('?') + 1).split('&');
|
let hashes = url.slice(url.indexOf('?') + 1).split('&');
|
||||||
for (let i = 0; i < hashes.length; i++) {
|
for (let i = 0; i < hashes.length; i++) {
|
||||||
hash = hashes[i].split('=');
|
const hash = hashes[i].split('=');
|
||||||
query[hash[0]] = hash[1];
|
query[hash[0]] = hash[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
path,
|
path,
|
||||||
query
|
query,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractPathParams(pattern, path) {
|
function extractPathParams(pattern, path) {
|
||||||
if (pattern.indexOf(':') === -1) {
|
if (pattern.indexOf(':') === -1) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
const params = {};
|
const params = {};
|
||||||
for (let i = 0, j = 0; i < pattern.length; i++, j++) {
|
for (let i = 0, j = 0; i < pattern.length; i++, j++) {
|
||||||
if (pattern[i] === ':') {
|
if (pattern[i] === ':') {
|
||||||
let key = [];
|
let key = [];
|
||||||
let val = [];
|
let val = [];
|
||||||
while (pattern[++i] !== '/' && i < pattern.length) {
|
while (pattern[++i] !== '/' && i < pattern.length) {
|
||||||
key.push(pattern[i]);
|
key.push(pattern[i]);
|
||||||
}
|
}
|
||||||
while (path[j] !== '/' && j < path.length) {
|
while (path[j] !== '/' && j < path.length) {
|
||||||
val.push(path[j++]);
|
val.push(path[j++]);
|
||||||
}
|
}
|
||||||
params[key.join('')] = val.join('');
|
params[key.join('')] = val.join('');
|
||||||
} else {
|
} else {
|
||||||
if (pattern[i] !== path[j]) {
|
if (pattern[i] !== path[j]) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = express;
|
|
||||||
|
@ -1,209 +1,319 @@
|
|||||||
const { HTTP } = require('./open-api');
|
import { HTTP } from './open-api';
|
||||||
|
|
||||||
// get proxy flag according to its name
|
// 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
|
// flags from @KOP-XIAO: https://github.com/KOP-XIAO/QuantumultX/blob/master/Scripts/resource-parser.js
|
||||||
const flags = {
|
const flags = {
|
||||||
'🇦🇿': [ '阿塞拜疆' ],
|
'🇦🇿': ['阿塞拜疆'],
|
||||||
'🇦🇹': [ '奥地利', '奧地利', 'Austria', '维也纳' ],
|
'🇦🇹': ['奥地利', '奧地利', 'Austria', '维也纳'],
|
||||||
'🇦🇺': [ 'AU', 'Australia', 'Sydney', '澳大利亚', '澳洲', '墨尔本', '悉尼', '土澳', '京澳', '廣澳', '滬澳', '沪澳', '广澳' ],
|
'🇦🇺': [
|
||||||
'🇧🇪': [ 'BE', '比利時', '比利时' ],
|
'AU',
|
||||||
'🇧🇬': [ '保加利亚', '保加利亞', 'Bulgaria' ],
|
'Australia',
|
||||||
'🇧🇭': [ 'BH', '巴林' ],
|
'Sydney',
|
||||||
'🇧🇩': [ 'BD', '孟加拉' ],
|
'澳大利亚',
|
||||||
'🇵🇰': [ '巴基斯坦' ],
|
'澳洲',
|
||||||
'🇰🇭': [ '柬埔寨' ],
|
'墨尔本',
|
||||||
'🇺🇦': [ '烏克蘭', '乌克兰' ],
|
'悉尼',
|
||||||
'🇭🇷': [ '克罗地亚', 'HR', '克羅地亞' ],
|
'土澳',
|
||||||
'🇨🇦': [ 'Canada', 'CANADA', 'CAN', 'Waterloo', '加拿大', '蒙特利尔', '温哥华', '楓葉', '枫叶', '滑铁卢', '多伦多', 'CA' ],
|
'京澳',
|
||||||
'🇨🇭': [ '瑞士', '苏黎世', 'Switzerland', 'Zurich' ],
|
'廣澳',
|
||||||
'🇳🇬': [ '尼日利亚', 'NG', '尼日利亞' ],
|
'滬澳',
|
||||||
'🇨🇿': [ 'Czechia', '捷克' ],
|
'沪澳',
|
||||||
'🇸🇰': [ '斯洛伐克', 'SK' ],
|
'广澳',
|
||||||
'🇷🇸': [ 'RS', '塞尔维亚' ],
|
],
|
||||||
'🇲🇩': [ '摩爾多瓦', 'MD', '摩尔多瓦' ],
|
'🇧🇪': ['BE', '比利時', '比利时'],
|
||||||
'🇩🇪': [ 'DE', 'German', 'GERMAN', '德国', '德國', '法兰克福', '京德', '滬德', '廣德', '沪德', '广德', 'Frankfurt' ],
|
'🇧🇬': ['保加利亚', '保加利亞', 'Bulgaria'],
|
||||||
'🇩🇰': [ 'DK', 'DNK', '丹麦', '丹麥' ],
|
'🇧🇭': ['BH', '巴林'],
|
||||||
'🇪🇸': [ 'ES', '西班牙', 'Spain' ],
|
'🇧🇩': ['BD', '孟加拉'],
|
||||||
'🇪🇺': [ 'EU', '欧盟', '欧罗巴' ],
|
'🇵🇰': ['巴基斯坦'],
|
||||||
'🇫🇮': [ 'Finland', '芬兰', '芬蘭', '赫尔辛基' ],
|
'🇰🇭': ['柬埔寨'],
|
||||||
'🇫🇷': [ 'FR', 'France', '法国', '法國', '巴黎' ],
|
'🇺🇦': ['烏克蘭', '乌克兰'],
|
||||||
'🇬🇧': [ 'UK', 'GB', 'England', 'United Kingdom', '英国', '伦敦', '英', 'London' ],
|
'🇭🇷': ['克罗地亚', 'HR', '克羅地亞'],
|
||||||
'🇲🇴': [ 'MO', 'Macao', '澳门', '澳門', 'CTM' ],
|
'🇨🇦': [
|
||||||
'🇰🇿': [ '哈萨克斯坦', '哈萨克' ],
|
'Canada',
|
||||||
'🇭🇺': [ '匈牙利', 'Hungary' ],
|
'CANADA',
|
||||||
'🇭🇰': [
|
'CAN',
|
||||||
'HK',
|
'Waterloo',
|
||||||
'Hongkong',
|
'加拿大',
|
||||||
'Hong Kong',
|
'蒙特利尔',
|
||||||
'HongKong',
|
'温哥华',
|
||||||
'HONG KONG',
|
'楓葉',
|
||||||
'香港',
|
'枫叶',
|
||||||
'深港',
|
'滑铁卢',
|
||||||
'沪港',
|
'多伦多',
|
||||||
'呼港',
|
'CA',
|
||||||
'HKT',
|
],
|
||||||
'HKBN',
|
'🇨🇭': ['瑞士', '苏黎世', 'Switzerland', 'Zurich'],
|
||||||
'HGC',
|
'🇳🇬': ['尼日利亚', 'NG', '尼日利亞'],
|
||||||
'WTT',
|
'🇨🇿': ['Czechia', '捷克'],
|
||||||
'CMI',
|
'🇸🇰': ['斯洛伐克', 'SK'],
|
||||||
'穗港',
|
'🇷🇸': ['RS', '塞尔维亚'],
|
||||||
'京港',
|
'🇲🇩': ['摩爾多瓦', 'MD', '摩尔多瓦'],
|
||||||
'港'
|
'🇩🇪': [
|
||||||
],
|
'DE',
|
||||||
'🇮🇩': [ 'Indonesia', '印尼', '印度尼西亚', '雅加达' ],
|
'German',
|
||||||
'🇮🇪': [ 'Ireland', 'IRELAND', '爱尔兰', '愛爾蘭', '都柏林' ],
|
'GERMAN',
|
||||||
'🇮🇱': [ 'Israel', '以色列' ],
|
'德国',
|
||||||
'🇮🇳': [ 'India', 'IND', 'INDIA', '印度', '孟买', 'MFumbai' ],
|
'德國',
|
||||||
'🇮🇸': [ 'IS', 'ISL', '冰岛', '冰島' ],
|
'法兰克福',
|
||||||
'🇰🇵': [ 'KP', '朝鲜' ],
|
'京德',
|
||||||
'🇰🇷': [ 'KR', 'Korea', 'KOR', '韩国', '首尔', '韩', '韓', '春川', 'Chuncheon', 'Seoul' ],
|
'滬德',
|
||||||
'🇱🇺': [ '卢森堡' ],
|
'廣德',
|
||||||
'🇱🇻': [ 'Latvia', 'Latvija', '拉脱维亚' ],
|
'沪德',
|
||||||
'🇲🇽': [ 'MEX', 'MX', '墨西哥' ],
|
'广德',
|
||||||
'🇲🇾': [ 'MY', 'Malaysia', 'MALAYSIA', '马来西亚', '大馬', '馬來西亞', '吉隆坡' ],
|
'Frankfurt',
|
||||||
'🇳🇱': [ 'NL', 'Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹' ],
|
],
|
||||||
'🇳🇵': [ '尼泊尔' ],
|
'🇩🇰': ['DK', 'DNK', '丹麦', '丹麥'],
|
||||||
'🇵🇭': [ 'PH', 'Philippines', '菲律宾', '菲律賓' ],
|
'🇪🇸': ['ES', '西班牙', 'Spain'],
|
||||||
'🇵🇷': [ 'PR', '波多黎各' ],
|
'🇪🇺': ['EU', '欧盟', '欧罗巴'],
|
||||||
'🇷🇴': [ 'RO', '罗马尼亚' ],
|
'🇫🇮': ['Finland', '芬兰', '芬蘭', '赫尔辛基'],
|
||||||
'🇷🇺': [
|
'🇫🇷': ['FR', 'France', '法国', '法國', '巴黎'],
|
||||||
'RU',
|
'🇬🇧': [
|
||||||
'Russia',
|
'UK',
|
||||||
'俄罗斯',
|
'GB',
|
||||||
'俄国',
|
'England',
|
||||||
'俄羅斯',
|
'United Kingdom',
|
||||||
'伯力',
|
'英国',
|
||||||
'莫斯科',
|
'伦敦',
|
||||||
'圣彼得堡',
|
'英',
|
||||||
'西伯利亚',
|
'London',
|
||||||
'新西伯利亚',
|
],
|
||||||
'京俄',
|
'🇲🇴': ['MO', 'Macao', '澳门', '澳門', 'CTM'],
|
||||||
'杭俄',
|
'🇰🇿': ['哈萨克斯坦', '哈萨克'],
|
||||||
'廣俄',
|
'🇭🇺': ['匈牙利', 'Hungary'],
|
||||||
'滬俄',
|
'🇭🇰': [
|
||||||
'广俄',
|
'HK',
|
||||||
'沪俄',
|
'Hongkong',
|
||||||
'Moscow'
|
'Hong Kong',
|
||||||
],
|
'HongKong',
|
||||||
'🇸🇦': [ '沙特' ],
|
'HONG KONG',
|
||||||
'🇸🇪': [ 'SE', 'Sweden', '瑞典' ],
|
'香港',
|
||||||
'🇲🇹': [ '马耳他' ],
|
'深港',
|
||||||
'🇲🇦': [ 'MA', '摩洛哥' ],
|
'沪港',
|
||||||
'🇸🇬': [ 'SG', 'Singapore', 'SINGAPORE', '新加坡', '狮城', '沪新', '京新', '泉新', '穗新', '深新', '杭新', '广新', '廣新', '滬新' ],
|
'呼港',
|
||||||
'🇹🇭': [ 'TH', 'Thailand', '泰国', '泰國', '曼谷' ],
|
'HKT',
|
||||||
'🇹🇷': [ 'TR', 'Turkey', '土耳其', '伊斯坦布尔' ],
|
'HKBN',
|
||||||
'🇹🇼': [ 'TW', 'Taiwan', 'TAIWAN', '台湾', '台北', '台中', '新北', '彰化', 'CHT', '台', 'HINET', 'Taipei' ],
|
'HGC',
|
||||||
'🇺🇸': [
|
'WTT',
|
||||||
'US',
|
'CMI',
|
||||||
'USA',
|
'穗港',
|
||||||
'America',
|
'京港',
|
||||||
'United States',
|
'港',
|
||||||
'美国',
|
],
|
||||||
'美',
|
'🇮🇩': ['Indonesia', '印尼', '印度尼西亚', '雅加达'],
|
||||||
'京美',
|
'🇮🇪': ['Ireland', 'IRELAND', '爱尔兰', '愛爾蘭', '都柏林'],
|
||||||
'波特兰',
|
'🇮🇱': ['Israel', '以色列'],
|
||||||
'达拉斯',
|
'🇮🇳': ['India', 'IND', 'INDIA', '印度', '孟买', 'MFumbai'],
|
||||||
'俄勒冈',
|
'🇮🇸': ['IS', 'ISL', '冰岛', '冰島'],
|
||||||
'凤凰城',
|
'🇰🇵': ['KP', '朝鲜'],
|
||||||
'费利蒙',
|
'🇰🇷': [
|
||||||
'硅谷',
|
'KR',
|
||||||
'矽谷',
|
'Korea',
|
||||||
'拉斯维加斯',
|
'KOR',
|
||||||
'洛杉矶',
|
'韩国',
|
||||||
'圣何塞',
|
'首尔',
|
||||||
'圣克拉拉',
|
'韩',
|
||||||
'西雅图',
|
'韓',
|
||||||
'芝加哥',
|
'春川',
|
||||||
'沪美',
|
'Chuncheon',
|
||||||
'哥伦布',
|
'Seoul',
|
||||||
'纽约',
|
],
|
||||||
'Los Angeles',
|
'🇱🇺': ['卢森堡'],
|
||||||
'San Jose',
|
'🇱🇻': ['Latvia', 'Latvija', '拉脱维亚'],
|
||||||
'Sillicon Valley',
|
'🇲🇽': ['MEX', 'MX', '墨西哥'],
|
||||||
'Michigan'
|
'🇲🇾': [
|
||||||
],
|
'MY',
|
||||||
'🇻🇳': [ 'VN', '越南', '胡志明市' ],
|
'Malaysia',
|
||||||
'🇻🇪': [ 'VE', '委内瑞拉' ],
|
'MALAYSIA',
|
||||||
'🇮🇹': [ 'Italy', 'IT', 'Nachash', '意大利', '米兰', '義大利' ],
|
'马来西亚',
|
||||||
'🇿🇦': [ 'South Africa', '南非' ],
|
'大馬',
|
||||||
'🇦🇪': [ 'United Arab Emirates', '阿联酋', '迪拜', 'AE' ],
|
'馬來西亞',
|
||||||
'🇧🇷': [ 'BR', 'Brazil', '巴西', '圣保罗' ],
|
'吉隆坡',
|
||||||
'🇯🇵': [
|
],
|
||||||
'JP',
|
'🇳🇱': ['NL', 'Netherlands', '荷兰', '荷蘭', '尼德蘭', '阿姆斯特丹'],
|
||||||
'Japan',
|
'🇳🇵': ['尼泊尔'],
|
||||||
'JAPAN',
|
'🇵🇭': ['PH', 'Philippines', '菲律宾', '菲律賓'],
|
||||||
'日本',
|
'🇵🇷': ['PR', '波多黎各'],
|
||||||
'东京',
|
'🇷🇴': ['RO', '罗马尼亚'],
|
||||||
'大阪',
|
'🇷🇺': [
|
||||||
'埼玉',
|
'RU',
|
||||||
'沪日',
|
'Russia',
|
||||||
'穗日',
|
'俄罗斯',
|
||||||
'川日',
|
'俄国',
|
||||||
'中日',
|
'俄羅斯',
|
||||||
'泉日',
|
'伯力',
|
||||||
'杭日',
|
'莫斯科',
|
||||||
'深日',
|
'圣彼得堡',
|
||||||
'辽日',
|
'西伯利亚',
|
||||||
'广日',
|
'新西伯利亚',
|
||||||
'大坂',
|
'京俄',
|
||||||
'Osaka',
|
'杭俄',
|
||||||
'Tokyo'
|
'廣俄',
|
||||||
],
|
'滬俄',
|
||||||
'🇦🇷': [ 'AR', '阿根廷' ],
|
'广俄',
|
||||||
'🇳🇴': [ 'Norway', '挪威', 'NO' ],
|
'沪俄',
|
||||||
'🇨🇳': [ 'CN', 'China', '回国', '中国', '中國', '江苏', '北京', '上海', '广州', '深圳', '杭州', '徐州', '青岛', '宁波', '镇江', 'back' ],
|
'Moscow',
|
||||||
'🇵🇱': [ 'PL', 'POL', '波兰', '波蘭' ],
|
],
|
||||||
'🇨🇱': [ '智利' ],
|
'🇸🇦': ['沙特'],
|
||||||
'🇳🇿': [ '新西蘭', '新西兰' ],
|
'🇸🇪': ['SE', 'Sweden', '瑞典'],
|
||||||
'🇬🇷': [ '希腊', '希臘' ],
|
'🇲🇹': ['马耳他'],
|
||||||
'🇪🇬': [ '埃及' ],
|
'🇲🇦': ['MA', '摩洛哥'],
|
||||||
'🇨🇾': [ 'CY', '塞浦路斯' ],
|
'🇸🇬': [
|
||||||
'🇨🇷': [ 'CR', '哥斯达黎加' ],
|
'SG',
|
||||||
'🇸🇮': [ 'SI', '斯洛文尼亚' ],
|
'Singapore',
|
||||||
'🇱🇹': [ 'LT', '立陶宛' ],
|
'SINGAPORE',
|
||||||
'🇵🇦': [ 'PA', '巴拿马' ],
|
'新加坡',
|
||||||
'🇹🇳': [ 'TN', '突尼斯' ],
|
'狮城',
|
||||||
'🇮🇲': [ '马恩岛', '馬恩島' ],
|
'沪新',
|
||||||
'🇧🇾': [ 'BY', '白俄', '白俄罗斯' ],
|
'京新',
|
||||||
'🇵🇹': [ '葡萄牙' ],
|
'泉新',
|
||||||
'🇰🇪': [ 'KE', '肯尼亚' ],
|
'穗新',
|
||||||
'🇰🇬': [ 'KG', '吉尔吉斯坦' ],
|
'深新',
|
||||||
'🇯🇴': [ 'JO', '约旦' ],
|
'杭新',
|
||||||
'🇺🇾': [ 'UY', '乌拉圭' ],
|
'广新',
|
||||||
'🇲🇳': [ '蒙古' ],
|
'廣新',
|
||||||
'🇮🇷': [ 'IR', '伊朗' ],
|
'滬新',
|
||||||
'🇵🇪': [ '秘鲁', '祕魯' ],
|
],
|
||||||
'🇨🇴': [ '哥伦比亚' ],
|
'🇹🇭': ['TH', 'Thailand', '泰国', '泰國', '曼谷'],
|
||||||
'🇪🇪': [ '爱沙尼亚' ],
|
'🇹🇷': ['TR', 'Turkey', '土耳其', '伊斯坦布尔'],
|
||||||
'🇪🇨': [ 'EC', '厄瓜多尔' ],
|
'🇹🇼': [
|
||||||
'🇲🇰': [ '马其顿', '馬其頓' ],
|
'TW',
|
||||||
'🇧🇦': [ '波黑共和国', '波黑' ],
|
'Taiwan',
|
||||||
'🇬🇪': [ '格魯吉亞', '格鲁吉亚' ],
|
'TAIWAN',
|
||||||
'🇦🇱': [ '阿爾巴尼亞', '阿尔巴尼亚' ],
|
'台湾',
|
||||||
'🏳️🌈': [ '流量', '时间', '应急', '过期', 'Bandwidth', 'expire' ]
|
'台北',
|
||||||
};
|
'台中',
|
||||||
for (let k of Object.keys(flags)) {
|
'新北',
|
||||||
if (flags[k].some((item) => name.indexOf(item) !== -1)) {
|
'彰化',
|
||||||
return k;
|
'CHT',
|
||||||
}
|
'台',
|
||||||
}
|
'HINET',
|
||||||
// no flag found
|
'Taipei',
|
||||||
const oldFlag = (name.match(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/) || [])[0];
|
],
|
||||||
return oldFlag || '🏴☠️';
|
'🇺🇸': [
|
||||||
|
'US',
|
||||||
|
'USA',
|
||||||
|
'America',
|
||||||
|
'United States',
|
||||||
|
'美国',
|
||||||
|
'美',
|
||||||
|
'京美',
|
||||||
|
'波特兰',
|
||||||
|
'达拉斯',
|
||||||
|
'俄勒冈',
|
||||||
|
'凤凰城',
|
||||||
|
'费利蒙',
|
||||||
|
'硅谷',
|
||||||
|
'矽谷',
|
||||||
|
'拉斯维加斯',
|
||||||
|
'洛杉矶',
|
||||||
|
'圣何塞',
|
||||||
|
'圣克拉拉',
|
||||||
|
'西雅图',
|
||||||
|
'芝加哥',
|
||||||
|
'沪美',
|
||||||
|
'哥伦布',
|
||||||
|
'纽约',
|
||||||
|
'Los Angeles',
|
||||||
|
'San Jose',
|
||||||
|
'Sillicon Valley',
|
||||||
|
'Michigan',
|
||||||
|
],
|
||||||
|
'🇻🇳': ['VN', '越南', '胡志明市'],
|
||||||
|
'🇻🇪': ['VE', '委内瑞拉'],
|
||||||
|
'🇮🇹': ['Italy', 'IT', 'Nachash', '意大利', '米兰', '義大利'],
|
||||||
|
'🇿🇦': ['South Africa', '南非'],
|
||||||
|
'🇦🇪': ['United Arab Emirates', '阿联酋', '迪拜', 'AE'],
|
||||||
|
'🇧🇷': ['BR', 'Brazil', '巴西', '圣保罗'],
|
||||||
|
'🇯🇵': [
|
||||||
|
'JP',
|
||||||
|
'Japan',
|
||||||
|
'JAPAN',
|
||||||
|
'日本',
|
||||||
|
'东京',
|
||||||
|
'大阪',
|
||||||
|
'埼玉',
|
||||||
|
'沪日',
|
||||||
|
'穗日',
|
||||||
|
'川日',
|
||||||
|
'中日',
|
||||||
|
'泉日',
|
||||||
|
'杭日',
|
||||||
|
'深日',
|
||||||
|
'辽日',
|
||||||
|
'广日',
|
||||||
|
'大坂',
|
||||||
|
'Osaka',
|
||||||
|
'Tokyo',
|
||||||
|
],
|
||||||
|
'🇦🇷': ['AR', '阿根廷'],
|
||||||
|
'🇳🇴': ['Norway', '挪威', 'NO'],
|
||||||
|
'🇨🇳': [
|
||||||
|
'CN',
|
||||||
|
'China',
|
||||||
|
'回国',
|
||||||
|
'中国',
|
||||||
|
'中國',
|
||||||
|
'江苏',
|
||||||
|
'北京',
|
||||||
|
'上海',
|
||||||
|
'广州',
|
||||||
|
'深圳',
|
||||||
|
'杭州',
|
||||||
|
'徐州',
|
||||||
|
'青岛',
|
||||||
|
'宁波',
|
||||||
|
'镇江',
|
||||||
|
'back',
|
||||||
|
],
|
||||||
|
'🇵🇱': ['PL', 'POL', '波兰', '波蘭'],
|
||||||
|
'🇨🇱': ['智利'],
|
||||||
|
'🇳🇿': ['新西蘭', '新西兰'],
|
||||||
|
'🇬🇷': ['希腊', '希臘'],
|
||||||
|
'🇪🇬': ['埃及'],
|
||||||
|
'🇨🇾': ['CY', '塞浦路斯'],
|
||||||
|
'🇨🇷': ['CR', '哥斯达黎加'],
|
||||||
|
'🇸🇮': ['SI', '斯洛文尼亚'],
|
||||||
|
'🇱🇹': ['LT', '立陶宛'],
|
||||||
|
'🇵🇦': ['PA', '巴拿马'],
|
||||||
|
'🇹🇳': ['TN', '突尼斯'],
|
||||||
|
'🇮🇲': ['马恩岛', '馬恩島'],
|
||||||
|
'🇧🇾': ['BY', '白俄', '白俄罗斯'],
|
||||||
|
'🇵🇹': ['葡萄牙'],
|
||||||
|
'🇰🇪': ['KE', '肯尼亚'],
|
||||||
|
'🇰🇬': ['KG', '吉尔吉斯坦'],
|
||||||
|
'🇯🇴': ['JO', '约旦'],
|
||||||
|
'🇺🇾': ['UY', '乌拉圭'],
|
||||||
|
'🇲🇳': ['蒙古'],
|
||||||
|
'🇮🇷': ['IR', '伊朗'],
|
||||||
|
'🇵🇪': ['秘鲁', '祕魯'],
|
||||||
|
'🇨🇴': ['哥伦比亚'],
|
||||||
|
'🇪🇪': ['爱沙尼亚'],
|
||||||
|
'🇪🇨': ['EC', '厄瓜多尔'],
|
||||||
|
'🇲🇰': ['马其顿', '馬其頓'],
|
||||||
|
'🇧🇦': ['波黑共和国', '波黑'],
|
||||||
|
'🇬🇪': ['格魯吉亞', '格鲁吉亚'],
|
||||||
|
'🇦🇱': ['阿爾巴尼亞', '阿尔巴尼亚'],
|
||||||
|
'🏳️🌈': ['流量', '时间', '应急', '过期', 'Bandwidth', 'expire'],
|
||||||
|
};
|
||||||
|
for (let k of Object.keys(flags)) {
|
||||||
|
if (flags[k].some((item) => name.indexOf(item) !== -1)) {
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// no flag found
|
||||||
|
const oldFlag = (name.match(
|
||||||
|
/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/,
|
||||||
|
) || [])[0];
|
||||||
|
return oldFlag || '🏴☠️';
|
||||||
}
|
}
|
||||||
|
|
||||||
// util API
|
// util API
|
||||||
async function IP_API(req, res) {
|
export async function IP_API(req, res) {
|
||||||
const server = decodeURIComponent(req.params.server);
|
const server = decodeURIComponent(req.params.server);
|
||||||
const $http = HTTP();
|
const $http = HTTP();
|
||||||
const result = await $http
|
const result = await $http
|
||||||
.get(`http://ip-api.com/json/${server}?lang=zh-CN`)
|
.get(`http://ip-api.com/json/${server}?lang=zh-CN`)
|
||||||
.then((resp) => JSON.parse(resp.body));
|
.then((resp) => JSON.parse(resp.body));
|
||||||
res.json(result);
|
res.json(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
getFlag,
|
|
||||||
IP_API
|
|
||||||
};
|
|
||||||
|
@ -1,75 +1,77 @@
|
|||||||
const { HTTP } = require('./open-api');
|
import { HTTP } from './open-api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gist backup
|
* Gist backup
|
||||||
*/
|
*/
|
||||||
function Gist({ token, key }) {
|
export default function Gist({ token, key }) {
|
||||||
const http = HTTP({
|
const http = HTTP({
|
||||||
baseURL: 'https://api.github.com',
|
baseURL: 'https://api.github.com',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `token ${token}`,
|
Authorization: `token ${token}`,
|
||||||
'User-Agent':
|
'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: {
|
events: {
|
||||||
onResponse: (resp) => {
|
onResponse: (resp) => {
|
||||||
if (/^[45]/.test(String(resp.statusCode))) {
|
if (/^[45]/.test(String(resp.statusCode))) {
|
||||||
return Promise.reject(`ERROR: ${JSON.parse(resp.body).message}`);
|
return Promise.reject(
|
||||||
} else {
|
`ERROR: ${JSON.parse(resp.body).message}`,
|
||||||
return resp;
|
);
|
||||||
}
|
} else {
|
||||||
}
|
return resp;
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
async function locate() {
|
async function locate() {
|
||||||
return http.get('/gists').then((response) => {
|
return http.get('/gists').then((response) => {
|
||||||
const gists = JSON.parse(response.body);
|
const gists = JSON.parse(response.body);
|
||||||
for (let g of gists) {
|
for (let g of gists) {
|
||||||
if (g.description === key) {
|
if (g.description === key) {
|
||||||
return g.id;
|
return g.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.upload = async function(files) {
|
this.upload = async function (files) {
|
||||||
const id = await locate();
|
const id = await locate();
|
||||||
|
|
||||||
if (id === -1) {
|
if (id === -1) {
|
||||||
// create a new gist for backup
|
// create a new gist for backup
|
||||||
return http.post({
|
return http.post({
|
||||||
url: '/gists',
|
url: '/gists',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
description: key,
|
description: key,
|
||||||
public: false,
|
public: false,
|
||||||
files
|
files,
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// update an existing gist
|
// update an existing gist
|
||||||
return http.patch({
|
return http.patch({
|
||||||
url: `/gists/${id}`,
|
url: `/gists/${id}`,
|
||||||
body: JSON.stringify({ files })
|
body: JSON.stringify({ files }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.download = async function(filename) {
|
this.download = async function (filename) {
|
||||||
const id = await locate();
|
const id = await locate();
|
||||||
if (id === -1) {
|
if (id === -1) {
|
||||||
return Promise.reject('未找到Gist备份!');
|
return Promise.reject('未找到Gist备份!');
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
const { files } = await http.get(`/gists/${id}`).then((resp) => JSON.parse(resp.body));
|
const { files } = await http
|
||||||
const url = files[filename].raw_url;
|
.get(`/gists/${id}`)
|
||||||
return await http.get(url).then((resp) => resp.body);
|
.then((resp) => JSON.parse(resp.body));
|
||||||
} catch (err) {
|
const url = files[filename].raw_url;
|
||||||
return Promise.reject(err);
|
return await http.get(url).then((resp) => resp.body);
|
||||||
}
|
} catch (err) {
|
||||||
}
|
return Promise.reject(err);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Gist;
|
|
||||||
|
@ -14,9 +14,4 @@ function FULL(length, bool) {
|
|||||||
return [...Array(length).keys()].map(() => bool);
|
return [...Array(length).keys()].map(() => bool);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
export { AND, OR, NOT, FULL };
|
||||||
AND,
|
|
||||||
OR,
|
|
||||||
NOT,
|
|
||||||
FULL
|
|
||||||
}
|
|
||||||
|
@ -1,271 +1,318 @@
|
|||||||
function ENV() {
|
export function ENV() {
|
||||||
const isQX = typeof $task !== 'undefined';
|
const isQX = typeof $task !== 'undefined';
|
||||||
const isLoon = typeof $loon !== 'undefined';
|
const isLoon = typeof $loon !== 'undefined';
|
||||||
const isSurge = typeof $httpClient !== 'undefined' && !isLoon;
|
const isSurge = typeof $httpClient !== 'undefined' && !isLoon;
|
||||||
const isNode = eval(`typeof process !== "undefined"`);
|
const isNode = eval(`typeof process !== "undefined"`);
|
||||||
return { isQX, isLoon, isSurge, isNode };
|
return { isQX, isLoon, isSurge, isNode };
|
||||||
}
|
}
|
||||||
|
|
||||||
function HTTP(defaultOptions = { baseURL: '' }) {
|
export function HTTP(defaultOptions = { baseURL: '' }) {
|
||||||
const { isQX, isLoon, isSurge, isNode } = ENV();
|
const { isQX, isLoon, isSurge, isNode } = ENV();
|
||||||
const methods = [ 'GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH' ];
|
const methods = [
|
||||||
const URL_REGEX = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
|
'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) {
|
function send(method, options) {
|
||||||
options = typeof options === 'string' ? { url: options } : options;
|
options = typeof options === 'string' ? { url: options } : options;
|
||||||
const baseURL = defaultOptions.baseURL;
|
const baseURL = defaultOptions.baseURL;
|
||||||
if (baseURL && !URL_REGEX.test(options.url || '')) {
|
if (baseURL && !URL_REGEX.test(options.url || '')) {
|
||||||
options.url = baseURL ? baseURL + options.url : options.url;
|
options.url = baseURL ? baseURL + options.url : options.url;
|
||||||
}
|
}
|
||||||
options = { ...defaultOptions, ...options };
|
options = { ...defaultOptions, ...options };
|
||||||
const timeout = options.timeout;
|
const timeout = options.timeout;
|
||||||
const events = {
|
const events = {
|
||||||
...{
|
...{
|
||||||
onRequest: () => {},
|
onRequest: () => {},
|
||||||
onResponse: (resp) => resp,
|
onResponse: (resp) => resp,
|
||||||
onTimeout: () => {}
|
onTimeout: () => {},
|
||||||
},
|
},
|
||||||
...options.events
|
...options.events,
|
||||||
};
|
};
|
||||||
|
|
||||||
events.onRequest(method, options);
|
events.onRequest(method, options);
|
||||||
|
|
||||||
let worker;
|
let worker;
|
||||||
if (isQX) {
|
if (isQX) {
|
||||||
worker = $task.fetch({
|
worker = $task.fetch({
|
||||||
method,
|
method,
|
||||||
url: options.url,
|
url: options.url,
|
||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
body: options.body
|
body: options.body,
|
||||||
});
|
});
|
||||||
} else if (isLoon || isSurge || isNode) {
|
} else if (isLoon || isSurge || isNode) {
|
||||||
worker = new Promise((resolve, reject) => {
|
worker = new Promise((resolve, reject) => {
|
||||||
const request = isNode ? eval("require('request')") : $httpClient;
|
const request = isNode
|
||||||
request[method.toLowerCase()](options, (err, response, body) => {
|
? eval("require('request')")
|
||||||
if (err) reject(err);
|
: $httpClient;
|
||||||
else
|
request[method.toLowerCase()](
|
||||||
resolve({
|
options,
|
||||||
statusCode: response.status || response.statusCode,
|
(err, response, body) => {
|
||||||
headers: response.headers,
|
if (err) reject(err);
|
||||||
body
|
else
|
||||||
});
|
resolve({
|
||||||
});
|
statusCode:
|
||||||
});
|
response.status || response.statusCode,
|
||||||
}
|
headers: response.headers,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let timeoutid;
|
let timeoutid;
|
||||||
const timer = timeout
|
const timer = timeout
|
||||||
? new Promise((_, reject) => {
|
? new Promise((_, reject) => {
|
||||||
timeoutid = setTimeout(() => {
|
timeoutid = setTimeout(() => {
|
||||||
events.onTimeout();
|
events.onTimeout();
|
||||||
return reject(`${method} URL: ${options.url} exceeds the timeout ${timeout} ms`);
|
return reject(
|
||||||
}, timeout);
|
`${method} URL: ${options.url} exceeds the timeout ${timeout} ms`,
|
||||||
})
|
);
|
||||||
: null;
|
}, timeout);
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
return (timer
|
return (
|
||||||
? Promise.race([ timer, worker ]).then((res) => {
|
timer
|
||||||
clearTimeout(timeoutid);
|
? Promise.race([timer, worker]).then((res) => {
|
||||||
return res;
|
clearTimeout(timeoutid);
|
||||||
})
|
return res;
|
||||||
: worker).then((resp) => events.onResponse(resp));
|
})
|
||||||
}
|
: worker
|
||||||
|
).then((resp) => events.onResponse(resp));
|
||||||
|
}
|
||||||
|
|
||||||
const http = {};
|
const http = {};
|
||||||
methods.forEach((method) => (http[method.toLowerCase()] = (options) => send(method, options)));
|
methods.forEach(
|
||||||
return http;
|
(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();
|
const { isQX, isLoon, isSurge, isNode } = ENV();
|
||||||
return new class {
|
return new (class {
|
||||||
constructor(name, debug) {
|
constructor(name, debug) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.debug = debug;
|
this.debug = debug;
|
||||||
|
|
||||||
this.http = HTTP();
|
this.http = HTTP();
|
||||||
this.env = ENV();
|
this.env = ENV();
|
||||||
|
|
||||||
this.node = (() => {
|
this.node = (() => {
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
const fs = eval("require('fs')");
|
const fs = eval("require('fs')");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fs
|
fs,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
this.initCache();
|
this.initCache();
|
||||||
|
|
||||||
const delay = (t, v) =>
|
const delay = (t, v) =>
|
||||||
new Promise(function(resolve) {
|
new Promise(function (resolve) {
|
||||||
setTimeout(resolve.bind(null, v), t);
|
setTimeout(resolve.bind(null, v), t);
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.prototype.delay = function(t) {
|
Promise.prototype.delay = function (t) {
|
||||||
return this.then(function(v) {
|
return this.then(function (v) {
|
||||||
return delay(t, v);
|
return delay(t, v);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// persistence
|
// persistence
|
||||||
// initialize cache
|
// initialize cache
|
||||||
initCache() {
|
initCache() {
|
||||||
if (isQX) this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}');
|
if (isQX)
|
||||||
if (isLoon || isSurge) this.cache = JSON.parse($persistentStore.read(this.name) || '{}');
|
this.cache = JSON.parse($prefs.valueForKey(this.name) || '{}');
|
||||||
|
if (isLoon || isSurge)
|
||||||
|
this.cache = JSON.parse(
|
||||||
|
$persistentStore.read(this.name) || '{}',
|
||||||
|
);
|
||||||
|
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
// create a json for root cache
|
// create a json for root cache
|
||||||
let fpath = 'root.json';
|
let fpath = 'root.json';
|
||||||
if (!this.node.fs.existsSync(fpath)) {
|
if (!this.node.fs.existsSync(fpath)) {
|
||||||
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { flag: 'wx' }, (err) => console.log(err));
|
this.node.fs.writeFileSync(
|
||||||
}
|
fpath,
|
||||||
this.root = {};
|
JSON.stringify({}),
|
||||||
|
{ flag: 'wx' },
|
||||||
|
(err) => console.log(err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.root = {};
|
||||||
|
|
||||||
// create a json file with the given name if not exists
|
// create a json file with the given name if not exists
|
||||||
fpath = `${this.name}.json`;
|
fpath = `${this.name}.json`;
|
||||||
if (!this.node.fs.existsSync(fpath)) {
|
if (!this.node.fs.existsSync(fpath)) {
|
||||||
this.node.fs.writeFileSync(fpath, JSON.stringify({}), { flag: 'wx' }, (err) => console.log(err));
|
this.node.fs.writeFileSync(
|
||||||
this.cache = {};
|
fpath,
|
||||||
} else {
|
JSON.stringify({}),
|
||||||
this.cache = JSON.parse(this.node.fs.readFileSync(`${this.name}.json`));
|
{ flag: 'wx' },
|
||||||
}
|
(err) => console.log(err),
|
||||||
}
|
);
|
||||||
}
|
this.cache = {};
|
||||||
|
} else {
|
||||||
|
this.cache = JSON.parse(
|
||||||
|
this.node.fs.readFileSync(`${this.name}.json`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// store cache
|
// store cache
|
||||||
persistCache() {
|
persistCache() {
|
||||||
const data = JSON.stringify(this.cache, null, 2);
|
const data = JSON.stringify(this.cache, null, 2);
|
||||||
if (isQX) $prefs.setValueForKey(data, this.name);
|
if (isQX) $prefs.setValueForKey(data, this.name);
|
||||||
if (isLoon || isSurge) $persistentStore.write(data, this.name);
|
if (isLoon || isSurge) $persistentStore.write(data, this.name);
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
this.node.fs.writeFileSync(`${this.name}.json`, data, { flag: 'w' }, (err) => console.log(err));
|
this.node.fs.writeFileSync(
|
||||||
this.node.fs.writeFileSync('root.json', JSON.stringify(this.root, null, 2), { flag: 'w' }, (err) =>
|
`${this.name}.json`,
|
||||||
console.log(err)
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
write(data, key) {
|
write(data, key) {
|
||||||
this.log(`SET ${key}`);
|
this.log(`SET ${key}`);
|
||||||
if (key.indexOf('#') !== -1) {
|
if (key.indexOf('#') !== -1) {
|
||||||
key = key.substr(1);
|
key = key.substr(1);
|
||||||
if (isSurge || isLoon) {
|
if (isSurge || isLoon) {
|
||||||
return $persistentStore.write(data, key);
|
return $persistentStore.write(data, key);
|
||||||
}
|
}
|
||||||
if (isQX) {
|
if (isQX) {
|
||||||
return $prefs.setValueForKey(data, key);
|
return $prefs.setValueForKey(data, key);
|
||||||
}
|
}
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
this.root[key] = data;
|
this.root[key] = data;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.cache[key] = data;
|
this.cache[key] = data;
|
||||||
}
|
}
|
||||||
this.persistCache();
|
this.persistCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
read(key) {
|
read(key) {
|
||||||
this.log(`READ ${key}`);
|
this.log(`READ ${key}`);
|
||||||
if (key.indexOf('#') !== -1) {
|
if (key.indexOf('#') !== -1) {
|
||||||
key = key.substr(1);
|
key = key.substr(1);
|
||||||
if (isSurge || isLoon) {
|
if (isSurge || isLoon) {
|
||||||
return $persistentStore.read(key);
|
return $persistentStore.read(key);
|
||||||
}
|
}
|
||||||
if (isQX) {
|
if (isQX) {
|
||||||
return $prefs.valueForKey(key);
|
return $prefs.valueForKey(key);
|
||||||
}
|
}
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
return this.root[key];
|
return this.root[key];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return this.cache[key];
|
return this.cache[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(key) {
|
delete(key) {
|
||||||
this.log(`DELETE ${key}`);
|
this.log(`DELETE ${key}`);
|
||||||
if (key.indexOf('#') !== -1) {
|
if (key.indexOf('#') !== -1) {
|
||||||
key = key.substr(1);
|
key = key.substr(1);
|
||||||
if (isSurge || isLoon) {
|
if (isSurge || isLoon) {
|
||||||
return $persistentStore.write(null, key);
|
return $persistentStore.write(null, key);
|
||||||
}
|
}
|
||||||
if (isQX) {
|
if (isQX) {
|
||||||
return $prefs.removeValueForKey(key);
|
return $prefs.removeValueForKey(key);
|
||||||
}
|
}
|
||||||
if (isNode) {
|
if (isNode) {
|
||||||
delete this.root[key];
|
delete this.root[key];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
delete this.cache[key];
|
delete this.cache[key];
|
||||||
}
|
}
|
||||||
this.persistCache();
|
this.persistCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
// notification
|
// notification
|
||||||
notify(title, subtitle = '', content = '', options = {}) {
|
notify(title, subtitle = '', content = '', options = {}) {
|
||||||
const openURL = options['open-url'];
|
const openURL = options['open-url'];
|
||||||
const mediaURL = options['media-url'];
|
const mediaURL = options['media-url'];
|
||||||
|
|
||||||
if (isQX) $notify(title, subtitle, content, options);
|
if (isQX) $notify(title, subtitle, content, options);
|
||||||
if (isSurge) {
|
if (isSurge) {
|
||||||
$notification.post(title, subtitle, content + `${mediaURL ? '\n多媒体:' + mediaURL : ''}`, {
|
$notification.post(
|
||||||
url: openURL
|
title,
|
||||||
});
|
subtitle,
|
||||||
}
|
content + `${mediaURL ? '\n多媒体:' + mediaURL : ''}`,
|
||||||
if (isLoon) {
|
{
|
||||||
let opts = {};
|
url: openURL,
|
||||||
if (openURL) opts['openUrl'] = openURL;
|
},
|
||||||
if (mediaURL) opts['mediaUrl'] = mediaURL;
|
);
|
||||||
if (JSON.stringify(opts) === '{}') {
|
}
|
||||||
$notification.post(title, subtitle, content);
|
if (isLoon) {
|
||||||
} else {
|
let opts = {};
|
||||||
$notification.post(title, subtitle, content, opts);
|
if (openURL) opts['openUrl'] = openURL;
|
||||||
}
|
if (mediaURL) opts['mediaUrl'] = mediaURL;
|
||||||
}
|
if (JSON.stringify(opts) === '{}') {
|
||||||
if (isNode) {
|
$notification.post(title, subtitle, content);
|
||||||
const content_ =
|
} else {
|
||||||
content + (openURL ? `\n点击跳转: ${openURL}` : '') + (mediaURL ? `\n多媒体: ${mediaURL}` : '');
|
$notification.post(title, subtitle, content, opts);
|
||||||
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
|
}
|
||||||
}
|
}
|
||||||
}
|
if (isNode) {
|
||||||
|
const content_ =
|
||||||
|
content +
|
||||||
|
(openURL ? `\n点击跳转: ${openURL}` : '') +
|
||||||
|
(mediaURL ? `\n多媒体: ${mediaURL}` : '');
|
||||||
|
console.log(`${title}\n${subtitle}\n${content_}\n\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// other helper functions
|
// other helper functions
|
||||||
log(msg) {
|
log(msg) {
|
||||||
if (this.debug) console.log(`[${this.name}] LOG: ${msg}`);
|
if (this.debug) console.log(`[${this.name}] LOG: ${msg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
info(msg) {
|
info(msg) {
|
||||||
console.log(`[${this.name}] INFO: ${msg}`);
|
console.log(`[${this.name}] INFO: ${msg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
error(msg) {
|
error(msg) {
|
||||||
console.log(`[${this.name}] ERROR: ${msg}`);
|
console.log(`[${this.name}] ERROR: ${msg}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
wait(millisec) {
|
wait(millisec) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, millisec));
|
return new Promise((resolve) => setTimeout(resolve, millisec));
|
||||||
}
|
}
|
||||||
|
|
||||||
done(value = {}) {
|
done(value = {}) {
|
||||||
if (isQX || isLoon || isSurge) {
|
if (isQX || isLoon || isSurge) {
|
||||||
$done(value);
|
$done(value);
|
||||||
} else if (isNode) {
|
} else if (isNode) {
|
||||||
if (typeof $context !== 'undefined') {
|
if (typeof $context !== 'undefined') {
|
||||||
$context.headers = value.headers;
|
$context.headers = value.headers;
|
||||||
$context.statusCode = value.statusCode;
|
$context.statusCode = value.statusCode;
|
||||||
$context.body = value.body;
|
$context.body = value.body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}(name, debug);
|
})(name, debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
HTTP,
|
|
||||||
ENV,
|
|
||||||
API
|
|
||||||
};
|
|
||||||
|
7
backend/sub-store.min.js
vendored
7
backend/sub-store.min.js
vendored
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user