mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-02 19:40:36 +08:00
371 lines
11 KiB
JavaScript
371 lines
11 KiB
JavaScript
import {
|
|
NetworkError,
|
|
InternalServerError,
|
|
ResourceNotFoundError,
|
|
RequestInvalidError,
|
|
} from './errors';
|
|
import { deleteByName, findByName, updateByName } from '@/utils/database';
|
|
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
|
|
import {
|
|
getFlowHeaders,
|
|
parseFlowHeaders,
|
|
getRmainingDays,
|
|
} from '@/utils/flow';
|
|
import { success, failed } from './response';
|
|
import $ from '@/core/app';
|
|
import { formatDateTime } from '@/utils';
|
|
|
|
if (!$.read(SUBS_KEY)) $.write({}, SUBS_KEY);
|
|
|
|
export default function register($app) {
|
|
$app.get('/api/sub/flow/:name', getFlowInfo);
|
|
|
|
$app.route('/api/sub/:name')
|
|
.get(getSubscription)
|
|
.patch(updateSubscription)
|
|
.delete(deleteSubscription);
|
|
|
|
$app.route('/api/subs')
|
|
.get(getAllSubscriptions)
|
|
.post(createSubscription)
|
|
.put(replaceSubscriptions);
|
|
}
|
|
|
|
// subscriptions API
|
|
async function getFlowInfo(req, res) {
|
|
let { name } = req.params;
|
|
name = decodeURIComponent(name);
|
|
let { url } = req.query;
|
|
if (url) {
|
|
url = decodeURIComponent(url);
|
|
$.info(`指定远程订阅 URL: ${url}`);
|
|
}
|
|
const allSubs = $.read(SUBS_KEY);
|
|
const sub = findByName(allSubs, name);
|
|
if (!sub) {
|
|
failed(
|
|
res,
|
|
new ResourceNotFoundError(
|
|
'RESOURCE_NOT_FOUND',
|
|
`Subscription ${name} does not exist!`,
|
|
),
|
|
404,
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
sub.source === 'local' &&
|
|
!['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
|
) {
|
|
if (sub.subUserinfo) {
|
|
let subUserInfo;
|
|
if (/^https?:\/\//.test(sub.subUserinfo)) {
|
|
try {
|
|
subUserInfo = await getFlowHeaders(
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
sub.proxy,
|
|
sub.subUserinfo,
|
|
);
|
|
} catch (e) {
|
|
$.error(
|
|
`订阅 ${name} 使用自定义流量链接 ${
|
|
sub.subUserinfo
|
|
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
|
);
|
|
}
|
|
} else {
|
|
subUserInfo = sub.subUserinfo;
|
|
}
|
|
try {
|
|
success(res, {
|
|
...parseFlowHeaders(subUserInfo),
|
|
});
|
|
} catch (e) {
|
|
$.error(
|
|
`Failed to parse flow info for local subscription ${name}: ${
|
|
e.message ?? e
|
|
}`,
|
|
);
|
|
failed(
|
|
res,
|
|
new RequestInvalidError(
|
|
'NO_FLOW_INFO',
|
|
'N/A',
|
|
`Failed to parse flow info`,
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
failed(
|
|
res,
|
|
new RequestInvalidError(
|
|
'NO_FLOW_INFO',
|
|
'N/A',
|
|
`Local subscription ${name} has no flow information!`,
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
try {
|
|
url =
|
|
`${url || sub.url}`
|
|
.split(/[\r\n]+/)
|
|
.map((i) => i.trim())
|
|
.filter((i) => i.length)?.[0] || '';
|
|
|
|
let $arguments = {};
|
|
const rawArgs = url.split('#');
|
|
url = url.split('#')[0];
|
|
if (rawArgs.length > 1) {
|
|
try {
|
|
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
|
|
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
|
|
} catch (e) {
|
|
for (const pair of rawArgs[1].split('&')) {
|
|
const key = pair.split('=')[0];
|
|
const value = pair.split('=')[1];
|
|
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
|
|
$arguments[key] =
|
|
value == null || value === ''
|
|
? true
|
|
: decodeURIComponent(value);
|
|
}
|
|
}
|
|
}
|
|
if ($arguments.noFlow) {
|
|
failed(
|
|
res,
|
|
new RequestInvalidError(
|
|
'NO_FLOW_INFO',
|
|
'N/A',
|
|
`Subscription ${name}: noFlow`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
const flowHeaders = await getFlowHeaders(
|
|
$arguments?.insecure ? `${url}#insecure` : url,
|
|
$arguments.flowUserAgent,
|
|
undefined,
|
|
sub.proxy,
|
|
$arguments.flowUrl,
|
|
);
|
|
if (!flowHeaders && !sub.subUserinfo) {
|
|
failed(
|
|
res,
|
|
new InternalServerError(
|
|
'NO_FLOW_INFO',
|
|
'No flow info',
|
|
`Failed to fetch flow headers`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
try {
|
|
const remainingDays = getRmainingDays({
|
|
resetDay: $arguments.resetDay,
|
|
startDate: $arguments.startDate,
|
|
cycleDays: $arguments.cycleDays,
|
|
});
|
|
let subUserInfo;
|
|
if (/^https?:\/\//.test(sub.subUserinfo)) {
|
|
try {
|
|
subUserInfo = await getFlowHeaders(
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
sub.proxy,
|
|
sub.subUserinfo,
|
|
);
|
|
} catch (e) {
|
|
$.error(
|
|
`订阅 ${name} 使用自定义流量链接 ${
|
|
sub.subUserinfo
|
|
} 获取流量信息时发生错误: ${JSON.stringify(e)}`,
|
|
);
|
|
}
|
|
} else {
|
|
subUserInfo = sub.subUserinfo;
|
|
}
|
|
const result = {
|
|
...parseFlowHeaders(
|
|
[subUserInfo, flowHeaders].filter((i) => i).join('; '),
|
|
),
|
|
};
|
|
if (remainingDays != null) {
|
|
result.remainingDays = remainingDays;
|
|
}
|
|
success(res, result);
|
|
} catch (e) {
|
|
$.error(
|
|
`Failed to parse flow info for local subscription ${name}: ${
|
|
e.message ?? e
|
|
}`,
|
|
);
|
|
failed(
|
|
res,
|
|
new RequestInvalidError(
|
|
'NO_FLOW_INFO',
|
|
'N/A',
|
|
`Failed to parse flow info`,
|
|
),
|
|
);
|
|
}
|
|
} catch (err) {
|
|
failed(
|
|
res,
|
|
new NetworkError(
|
|
`URL_NOT_ACCESSIBLE`,
|
|
`The URL for subscription ${name} is inaccessible.`,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
function createSubscription(req, res) {
|
|
const sub = req.body;
|
|
$.info(`正在创建订阅: ${sub.name}`);
|
|
if (/\//.test(sub.name)) {
|
|
failed(
|
|
res,
|
|
new RequestInvalidError(
|
|
'INVALID_NAME',
|
|
`Subscription ${sub.name} is invalid`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
const allSubs = $.read(SUBS_KEY);
|
|
if (findByName(allSubs, sub.name)) {
|
|
failed(
|
|
res,
|
|
new RequestInvalidError(
|
|
'DUPLICATE_KEY',
|
|
`Subscription ${sub.name} already exists.`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
allSubs.push(sub);
|
|
$.write(allSubs, SUBS_KEY);
|
|
success(res, sub, 201);
|
|
}
|
|
|
|
function getSubscription(req, res) {
|
|
let { name } = req.params;
|
|
let { raw } = req.query;
|
|
name = decodeURIComponent(name);
|
|
const allSubs = $.read(SUBS_KEY);
|
|
const sub = findByName(allSubs, name);
|
|
if (sub) {
|
|
if (raw) {
|
|
res.set('content-type', 'application/json')
|
|
.set(
|
|
'content-disposition',
|
|
`attachment; filename="${encodeURIComponent(
|
|
`sub-store_subscription_${name}_${formatDateTime(
|
|
new Date(),
|
|
)}.json`,
|
|
)}"`,
|
|
)
|
|
.send(JSON.stringify(sub));
|
|
} else {
|
|
success(res, sub);
|
|
}
|
|
} else {
|
|
failed(
|
|
res,
|
|
new ResourceNotFoundError(
|
|
`SUBSCRIPTION_NOT_FOUND`,
|
|
`Subscription ${name} does not exist`,
|
|
404,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
function updateSubscription(req, res) {
|
|
let { name } = req.params;
|
|
name = decodeURIComponent(name); // the original name
|
|
let sub = req.body;
|
|
const allSubs = $.read(SUBS_KEY);
|
|
const oldSub = findByName(allSubs, name);
|
|
if (oldSub) {
|
|
const newSub = {
|
|
...oldSub,
|
|
...sub,
|
|
};
|
|
$.info(`正在更新订阅: ${name}`);
|
|
// allow users to update the subscription name
|
|
if (name !== sub.name) {
|
|
// update all collections refer to this name
|
|
const allCols = $.read(COLLECTIONS_KEY) || [];
|
|
for (const collection of allCols) {
|
|
const idx = collection.subscriptions.indexOf(name);
|
|
if (idx !== -1) {
|
|
collection.subscriptions[idx] = sub.name;
|
|
}
|
|
}
|
|
|
|
// update all artifacts referring this subscription
|
|
const allArtifacts = $.read(ARTIFACTS_KEY) || [];
|
|
for (const artifact of allArtifacts) {
|
|
if (
|
|
artifact.type === 'subscription' &&
|
|
artifact.source == name
|
|
) {
|
|
artifact.source = sub.name;
|
|
}
|
|
}
|
|
|
|
$.write(allCols, COLLECTIONS_KEY);
|
|
$.write(allArtifacts, ARTIFACTS_KEY);
|
|
}
|
|
updateByName(allSubs, name, newSub);
|
|
$.write(allSubs, SUBS_KEY);
|
|
success(res, newSub);
|
|
} else {
|
|
failed(
|
|
res,
|
|
new ResourceNotFoundError(
|
|
'RESOURCE_NOT_FOUND',
|
|
`Subscription ${name} does not exist!`,
|
|
),
|
|
404,
|
|
);
|
|
}
|
|
}
|
|
|
|
function deleteSubscription(req, res) {
|
|
let { name } = req.params;
|
|
name = decodeURIComponent(name);
|
|
$.info(`删除订阅:${name}...`);
|
|
// delete from subscriptions
|
|
let allSubs = $.read(SUBS_KEY);
|
|
deleteByName(allSubs, name);
|
|
$.write(allSubs, SUBS_KEY);
|
|
// delete from collections
|
|
const allCols = $.read(COLLECTIONS_KEY);
|
|
for (const collection of allCols) {
|
|
collection.subscriptions = collection.subscriptions.filter(
|
|
(s) => s !== name,
|
|
);
|
|
}
|
|
$.write(allCols, COLLECTIONS_KEY);
|
|
success(res);
|
|
}
|
|
|
|
function getAllSubscriptions(req, res) {
|
|
const allSubs = $.read(SUBS_KEY);
|
|
success(res, allSubs);
|
|
}
|
|
|
|
function replaceSubscriptions(req, res) {
|
|
const allSubs = req.body;
|
|
$.write(allSubs, SUBS_KEY);
|
|
success(res);
|
|
}
|