mirror of
https://github.com/SukkaW/Surge.git
synced 2025-12-12 09:10:35 +08:00
Refactor: new output
This commit is contained in:
parent
5448d677fe
commit
b119fa652d
@ -1,11 +1,9 @@
|
||||
// @ts-check
|
||||
import { createRuleset } from './lib/create-file';
|
||||
import { parseFelixDnsmasq } from './lib/parse-dnsmasq';
|
||||
import { task } from './trace';
|
||||
import { SHARED_DESCRIPTION } from './lib/constants';
|
||||
import { createMemoizedPromise } from './lib/memo-promise';
|
||||
import { TTL, deserializeArray, fsFetchCache, serializeArray, createCacheKey } from './lib/cache-filesystem';
|
||||
import { output } from './lib/misc';
|
||||
import { DomainsetOutput } from './lib/create-file-new';
|
||||
|
||||
const cacheKey = createCacheKey(__filename);
|
||||
|
||||
@ -23,24 +21,16 @@ export const getAppleCdnDomainsPromise = createMemoizedPromise(() => fsFetchCach
|
||||
export const buildAppleCdn = task(require.main === module, __filename)(async (span) => {
|
||||
const res: string[] = await span.traceChildPromise('get apple cdn domains', getAppleCdnDomainsPromise());
|
||||
|
||||
const description = [
|
||||
...SHARED_DESCRIPTION,
|
||||
'',
|
||||
'This file contains Apple\'s domains using their China mainland CDN servers.',
|
||||
'',
|
||||
'Data from:',
|
||||
' - https://github.com/felixonmars/dnsmasq-china-list'
|
||||
];
|
||||
|
||||
const domainset = res.map(i => `.${i}`);
|
||||
|
||||
return createRuleset(
|
||||
span,
|
||||
'Sukka\'s Ruleset - Apple CDN',
|
||||
description,
|
||||
new Date(),
|
||||
domainset,
|
||||
'domainset',
|
||||
output('apple_cdn', 'domainset')
|
||||
);
|
||||
return new DomainsetOutput(span, 'apple_cdn')
|
||||
.withTitle('Sukka\'s Ruleset - Apple CDN')
|
||||
.withDescription([
|
||||
...SHARED_DESCRIPTION,
|
||||
'',
|
||||
'This file contains Apple\'s domains using their China mainland CDN servers.',
|
||||
'',
|
||||
'Data from:',
|
||||
' - https://github.com/felixonmars/dnsmasq-china-list'
|
||||
])
|
||||
.bulkAddDomainSuffix(res)
|
||||
.write();
|
||||
});
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
import path from 'node:path';
|
||||
import { createRuleset } from './lib/create-file';
|
||||
import { readFileIntoProcessedArray } from './lib/fetch-text-by-line';
|
||||
import { createTrie } from './lib/trie';
|
||||
import { task } from './trace';
|
||||
import { SHARED_DESCRIPTION } from './lib/constants';
|
||||
import { getPublicSuffixListTextPromise } from './lib/download-publicsuffixlist';
|
||||
import { domainsetDeduper } from './lib/domain-deduper';
|
||||
import { appendArrayInPlace } from './lib/append-array-in-place';
|
||||
import { sortDomains } from './lib/stable-sort-domain';
|
||||
import { output } from './lib/misc';
|
||||
import { SOURCE_DIR } from './constants/dir';
|
||||
import { processLine } from './lib/process-line';
|
||||
import { DomainsetOutput } from './lib/create-file-new';
|
||||
|
||||
const getS3OSSDomainsPromise = (async (): Promise<string[]> => {
|
||||
const trie = createTrie(
|
||||
@ -77,31 +74,24 @@ export const buildCdnDownloadConf = task(require.main === module, __filename)(as
|
||||
appendArrayInPlace(downloadDomainSet, steamDomainSet);
|
||||
|
||||
return Promise.all([
|
||||
createRuleset(
|
||||
span,
|
||||
'Sukka\'s Ruleset - CDN Domains',
|
||||
[
|
||||
new DomainsetOutput(span, 'cdn')
|
||||
.withTitle('Sukka\'s Ruleset - CDN Domains')
|
||||
.withDescription([
|
||||
...SHARED_DESCRIPTION,
|
||||
'',
|
||||
'This file contains object storage and static assets CDN domains.'
|
||||
],
|
||||
new Date(),
|
||||
sortDomains(domainsetDeduper(cdnDomainsList)),
|
||||
'domainset',
|
||||
output('cdn', 'domainset')
|
||||
),
|
||||
createRuleset(
|
||||
span,
|
||||
'Sukka\'s Ruleset - Large Files Hosting Domains',
|
||||
[
|
||||
])
|
||||
.addFromDomainset(cdnDomainsList)
|
||||
.write(),
|
||||
|
||||
new DomainsetOutput(span, 'download')
|
||||
.withTitle('Sukka\'s Ruleset - Large Files Hosting Domains')
|
||||
.withDescription([
|
||||
...SHARED_DESCRIPTION,
|
||||
'',
|
||||
'This file contains domains for software updating & large file hosting.'
|
||||
],
|
||||
new Date(),
|
||||
sortDomains(domainsetDeduper(downloadDomainSet)),
|
||||
'domainset',
|
||||
output('download', 'domainset')
|
||||
)
|
||||
])
|
||||
.addFromDomainset(downloadDomainSet)
|
||||
.write()
|
||||
]);
|
||||
});
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { fetchRemoteTextByLine } from './lib/fetch-text-by-line';
|
||||
import { createRuleset } from './lib/create-file';
|
||||
import { processLineFromReadline } from './lib/process-line';
|
||||
import { task } from './trace';
|
||||
|
||||
@ -7,7 +6,7 @@ import { exclude } from 'fast-cidr-tools';
|
||||
import { createMemoizedPromise } from './lib/memo-promise';
|
||||
import { CN_CIDR_NOT_INCLUDED_IN_CHNROUTE, NON_CN_CIDR_INCLUDED_IN_CHNROUTE } from './constants/cidr';
|
||||
import { appendArrayInPlace } from './lib/append-array-in-place';
|
||||
import { output } from './lib/misc';
|
||||
import { IPListOutput } from './lib/create-file-new';
|
||||
|
||||
export const getChnCidrPromise = createMemoizedPromise(async () => {
|
||||
const cidr4 = await processLineFromReadline(await fetchRemoteTextByLine('https://raw.githubusercontent.com/misakaio/chnroutes2/master/chnroutes.txt'));
|
||||
@ -28,31 +27,22 @@ export const buildChnCidr = task(require.main === module, __filename)(async (spa
|
||||
''
|
||||
];
|
||||
|
||||
// Can not use createRuleset here, as Clash support advanced ipset syntax
|
||||
return Promise.all([
|
||||
createRuleset(
|
||||
span,
|
||||
'Sukka\'s Ruleset - Mainland China IPv4 CIDR',
|
||||
[
|
||||
new IPListOutput(span, 'china_ip', false)
|
||||
.withTitle('Sukka\'s Ruleset - Mainland China IPv4 CIDR')
|
||||
.withDescription([
|
||||
...description,
|
||||
'Data from https://misaka.io (misakaio @ GitHub)'
|
||||
],
|
||||
new Date(),
|
||||
filteredCidr4,
|
||||
'ipcidr',
|
||||
output('china_ip', 'ip')
|
||||
),
|
||||
createRuleset(
|
||||
span,
|
||||
'Sukka\'s Ruleset - Mainland China IPv6 CIDR',
|
||||
[
|
||||
])
|
||||
.bulkAddCIDR4(filteredCidr4)
|
||||
.write(),
|
||||
new IPListOutput(span, 'china_ip_ipv6', false)
|
||||
.withTitle('Sukka\'s Ruleset - Mainland China IPv6 CIDR')
|
||||
.withDescription([
|
||||
...description,
|
||||
'Data from https://github.com/gaoyifan/china-operator-ip'
|
||||
],
|
||||
new Date(),
|
||||
cidr6,
|
||||
'ipcidr6',
|
||||
output('china_ip_ipv6', 'ip')
|
||||
)
|
||||
])
|
||||
.bulkAddCIDR6(cidr6)
|
||||
.write()
|
||||
]);
|
||||
});
|
||||
|
||||
@ -4,13 +4,13 @@ import * as path from 'node:path';
|
||||
import { readFileByLine } from './lib/fetch-text-by-line';
|
||||
import { processLine } from './lib/process-line';
|
||||
import { createRuleset } from './lib/create-file';
|
||||
import { domainsetDeduper } from './lib/domain-deduper';
|
||||
import type { Span } from './trace';
|
||||
import { task } from './trace';
|
||||
import { SHARED_DESCRIPTION } from './lib/constants';
|
||||
import { fdir as Fdir } from 'fdir';
|
||||
import { appendArrayInPlace } from './lib/append-array-in-place';
|
||||
import { OUTPUT_CLASH_DIR, OUTPUT_SINGBOX_DIR, OUTPUT_SURGE_DIR, SOURCE_DIR } from './constants/dir';
|
||||
import { DomainsetOutput } from './lib/create-file-new';
|
||||
|
||||
const MAGIC_COMMAND_SKIP = '# $ custom_build_script';
|
||||
const MAGIC_COMMAND_TITLE = '# $ meta_title ';
|
||||
@ -113,10 +113,8 @@ function transformDomainset(parentSpan: Span, sourcePath: string, relativePath:
|
||||
const res = await processFile(span, sourcePath);
|
||||
if (res === $skip) return;
|
||||
|
||||
const clashFileBasename = relativePath.slice(0, -path.extname(relativePath).length);
|
||||
|
||||
const id = path.basename(relativePath).slice(0, -path.extname(relativePath).length);
|
||||
const [title, descriptions, lines] = res;
|
||||
const deduped = domainsetDeduper(lines);
|
||||
|
||||
let description: string[];
|
||||
if (descriptions.length) {
|
||||
@ -127,19 +125,11 @@ function transformDomainset(parentSpan: Span, sourcePath: string, relativePath:
|
||||
description = SHARED_DESCRIPTION;
|
||||
}
|
||||
|
||||
return createRuleset(
|
||||
span,
|
||||
title,
|
||||
description,
|
||||
new Date(),
|
||||
deduped,
|
||||
'domainset',
|
||||
[
|
||||
path.resolve(OUTPUT_SURGE_DIR, relativePath),
|
||||
path.resolve(OUTPUT_CLASH_DIR, `${clashFileBasename}.txt`),
|
||||
path.resolve(OUTPUT_SINGBOX_DIR, `${clashFileBasename}.json`)
|
||||
]
|
||||
);
|
||||
return new DomainsetOutput(span, id)
|
||||
.withTitle(title)
|
||||
.withDescription(description)
|
||||
.addFromDomainset(lines)
|
||||
.write();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import path from 'node:path';
|
||||
import { createRuleset } from './lib/create-file';
|
||||
import { sortDomains } from './lib/stable-sort-domain';
|
||||
|
||||
import { Sema } from 'async-sema';
|
||||
import { getHostname } from 'tldts';
|
||||
@ -10,8 +8,132 @@ import { SHARED_DESCRIPTION } from './lib/constants';
|
||||
import { readFileIntoProcessedArray } from './lib/fetch-text-by-line';
|
||||
import { TTL, deserializeArray, fsFetchCache, serializeArray, createCacheKey } from './lib/cache-filesystem';
|
||||
|
||||
import { createTrie } from './lib/trie';
|
||||
import { output } from './lib/misc';
|
||||
import { DomainsetOutput } from './lib/create-file-new';
|
||||
import { OUTPUT_SURGE_DIR } from './constants/dir';
|
||||
|
||||
const KEYWORDS = [
|
||||
'Hong Kong',
|
||||
'Taiwan',
|
||||
'China Telecom',
|
||||
'China Mobile',
|
||||
'China Unicom',
|
||||
'Japan',
|
||||
'Tokyo',
|
||||
'Singapore',
|
||||
'Korea',
|
||||
'Seoul',
|
||||
'Canada',
|
||||
'Toronto',
|
||||
'Montreal',
|
||||
'Los Ang',
|
||||
'San Jos',
|
||||
'Seattle',
|
||||
'New York',
|
||||
'Dallas',
|
||||
'Miami',
|
||||
'Berlin',
|
||||
'Frankfurt',
|
||||
'London',
|
||||
'Paris',
|
||||
'Amsterdam',
|
||||
'Moscow',
|
||||
'Australia',
|
||||
'Sydney',
|
||||
'Brazil',
|
||||
'Turkey'
|
||||
];
|
||||
|
||||
const PREDEFINE_DOMAINS = [
|
||||
// speedtest.net
|
||||
'.speedtest.net',
|
||||
'.speedtestcustom.com',
|
||||
'.ooklaserver.net',
|
||||
'.speed.misaka.one',
|
||||
'.speedtest.rt.ru',
|
||||
'.speedtest.aptg.com.tw',
|
||||
'.speedtest.gslnetworks.com',
|
||||
'.speedtest.jsinfo.net',
|
||||
'.speedtest.i3d.net',
|
||||
'.speedtestkorea.com',
|
||||
'.speedtest.telus.com',
|
||||
'.speedtest.telstra.net',
|
||||
'.speedtest.clouvider.net',
|
||||
'.speedtest.idv.tw',
|
||||
'.speedtest.frontier.com',
|
||||
'.speedtest.orange.fr',
|
||||
'.speedtest.centurylink.net',
|
||||
'.srvr.bell.ca',
|
||||
'.speedtest.contabo.net',
|
||||
'speedtest.hk.chinamobile.com',
|
||||
'speedtestbb.hk.chinamobile.com',
|
||||
'.hizinitestet.com',
|
||||
'.linknetspeedtest.net.br',
|
||||
'speedtest.rit.edu',
|
||||
'speedtest.ropa.de',
|
||||
'speedtest.sits.su',
|
||||
'speedtest.tigo.cr',
|
||||
'speedtest.upp.com',
|
||||
'.speedtest.pni.tw',
|
||||
'.speed.pfm.gg',
|
||||
'.speedtest.faelix.net',
|
||||
'.speedtest.labixe.net',
|
||||
'.speedtest.warian.net',
|
||||
'.speedtest.starhub.com',
|
||||
'.speedtest.gibir.net.tr',
|
||||
'.speedtest.ozarksgo.net',
|
||||
'.speedtest.exetel.com.au',
|
||||
'.speedtest.sbcglobal.net',
|
||||
'.speedtest.leaptel.com.au',
|
||||
'.speedtest.windstream.net',
|
||||
'.speedtest.vodafone.com.au',
|
||||
'.speedtest.rascom.ru',
|
||||
'.speedtest.dchost.com',
|
||||
'.speedtest.highnet.com',
|
||||
'.speedtest.seattle.wa.limewave.net',
|
||||
'.speedtest.optitel.com.au',
|
||||
'.speednet.net.tr',
|
||||
'.speedtest.angolacables.co.ao',
|
||||
'.ookla-speedtest.fsr.com',
|
||||
'.speedtest.comnet.com.tr',
|
||||
'.speedtest.gslnetworks.com.au',
|
||||
'.test.gslnetworks.com.au',
|
||||
'.speedtest.gslnetworks.com',
|
||||
'.speedtestunonet.com.br',
|
||||
'.speedtest.alagas.net',
|
||||
'speedtest.surfshark.com',
|
||||
'.speedtest.aarnet.net.au',
|
||||
'.ookla.rcp.net',
|
||||
'.ookla-speedtests.e2ro.com',
|
||||
'.speedtest.com.sg',
|
||||
'.ookla.ddnsgeek.com',
|
||||
'.speedtest.pni.tw',
|
||||
'.speedtest.cmcnetworks.net',
|
||||
'.speedtestwnet.com.br',
|
||||
// Cloudflare
|
||||
'.speed.cloudflare.com',
|
||||
// Wi-Fi Man
|
||||
'.wifiman.com',
|
||||
'.wifiman.me',
|
||||
'.wifiman.ubncloud.com',
|
||||
// Fast.com
|
||||
'.fast.com',
|
||||
// MacPaw
|
||||
'speedtest.macpaw.com',
|
||||
// speedtestmaster
|
||||
'.netspeedtestmaster.com',
|
||||
// Google Search Result of "speedtest", powered by this
|
||||
'.measurement-lab.org',
|
||||
'.measurementlab.net',
|
||||
// Google Fiber legacy speedtest site (new fiber speedtest use speedtestcustom.com)
|
||||
'.speed.googlefiber.net',
|
||||
// librespeed
|
||||
'.backend.librespeed.org',
|
||||
// Apple,
|
||||
'mensura.cdn-apple.com', // From netQuality command
|
||||
// OpenSpeedtest
|
||||
'open.cachefly.net' // This is also used for openspeedtest server download
|
||||
|
||||
];
|
||||
|
||||
const s = new Sema(2);
|
||||
const cacheKey = createCacheKey(__filename);
|
||||
@ -85,170 +207,24 @@ const querySpeedtestApi = async (keyword: string): Promise<Array<string | null>>
|
||||
};
|
||||
|
||||
export const buildSpeedtestDomainSet = task(require.main === module, __filename)(async (span) => {
|
||||
const domainTrie = createTrie(
|
||||
[
|
||||
// speedtest.net
|
||||
'.speedtest.net',
|
||||
'.speedtestcustom.com',
|
||||
'.ooklaserver.net',
|
||||
'.speed.misaka.one',
|
||||
'.speedtest.rt.ru',
|
||||
'.speedtest.aptg.com.tw',
|
||||
'.speedtest.gslnetworks.com',
|
||||
'.speedtest.jsinfo.net',
|
||||
'.speedtest.i3d.net',
|
||||
'.speedtestkorea.com',
|
||||
'.speedtest.telus.com',
|
||||
'.speedtest.telstra.net',
|
||||
'.speedtest.clouvider.net',
|
||||
'.speedtest.idv.tw',
|
||||
'.speedtest.frontier.com',
|
||||
'.speedtest.orange.fr',
|
||||
'.speedtest.centurylink.net',
|
||||
'.srvr.bell.ca',
|
||||
'.speedtest.contabo.net',
|
||||
'speedtest.hk.chinamobile.com',
|
||||
'speedtestbb.hk.chinamobile.com',
|
||||
'.hizinitestet.com',
|
||||
'.linknetspeedtest.net.br',
|
||||
'speedtest.rit.edu',
|
||||
'speedtest.ropa.de',
|
||||
'speedtest.sits.su',
|
||||
'speedtest.tigo.cr',
|
||||
'speedtest.upp.com',
|
||||
'.speedtest.pni.tw',
|
||||
'.speed.pfm.gg',
|
||||
'.speedtest.faelix.net',
|
||||
'.speedtest.labixe.net',
|
||||
'.speedtest.warian.net',
|
||||
'.speedtest.starhub.com',
|
||||
'.speedtest.gibir.net.tr',
|
||||
'.speedtest.ozarksgo.net',
|
||||
'.speedtest.exetel.com.au',
|
||||
'.speedtest.sbcglobal.net',
|
||||
'.speedtest.leaptel.com.au',
|
||||
'.speedtest.windstream.net',
|
||||
'.speedtest.vodafone.com.au',
|
||||
'.speedtest.rascom.ru',
|
||||
'.speedtest.dchost.com',
|
||||
'.speedtest.highnet.com',
|
||||
'.speedtest.seattle.wa.limewave.net',
|
||||
'.speedtest.optitel.com.au',
|
||||
'.speednet.net.tr',
|
||||
'.speedtest.angolacables.co.ao',
|
||||
'.ookla-speedtest.fsr.com',
|
||||
'.speedtest.comnet.com.tr',
|
||||
'.speedtest.gslnetworks.com.au',
|
||||
'.test.gslnetworks.com.au',
|
||||
'.speedtest.gslnetworks.com',
|
||||
'.speedtestunonet.com.br',
|
||||
'.speedtest.alagas.net',
|
||||
'speedtest.surfshark.com',
|
||||
'.speedtest.aarnet.net.au',
|
||||
'.ookla.rcp.net',
|
||||
'.ookla-speedtests.e2ro.com',
|
||||
'.speedtest.com.sg',
|
||||
'.ookla.ddnsgeek.com',
|
||||
'.speedtest.pni.tw',
|
||||
'.speedtest.cmcnetworks.net',
|
||||
'.speedtestwnet.com.br',
|
||||
// Cloudflare
|
||||
'.speed.cloudflare.com',
|
||||
// Wi-Fi Man
|
||||
'.wifiman.com',
|
||||
'.wifiman.me',
|
||||
'.wifiman.ubncloud.com',
|
||||
// Fast.com
|
||||
'.fast.com',
|
||||
// MacPaw
|
||||
'speedtest.macpaw.com',
|
||||
// speedtestmaster
|
||||
'.netspeedtestmaster.com',
|
||||
// Google Search Result of "speedtest", powered by this
|
||||
'.measurement-lab.org',
|
||||
'.measurementlab.net',
|
||||
// Google Fiber legacy speedtest site (new fiber speedtest use speedtestcustom.com)
|
||||
'.speed.googlefiber.net',
|
||||
// librespeed
|
||||
'.backend.librespeed.org',
|
||||
// Apple,
|
||||
'mensura.cdn-apple.com', // From netQuality command
|
||||
// OpenSpeedtest
|
||||
'open.cachefly.net' // This is also used for openspeedtest server download
|
||||
],
|
||||
true
|
||||
);
|
||||
const output = new DomainsetOutput(span, 'speedtest')
|
||||
.withTitle('Sukka\'s Ruleset - Speedtest Domains')
|
||||
.withDescription([
|
||||
...SHARED_DESCRIPTION,
|
||||
'',
|
||||
'This file contains common speedtest endpoints.'
|
||||
])
|
||||
.addFromDomainset(PREDEFINE_DOMAINS)
|
||||
.addFromDomainset(await readFileIntoProcessedArray(path.resolve(OUTPUT_SURGE_DIR, 'domainset/speedtest.conf')));
|
||||
|
||||
await span.traceChildAsync(
|
||||
'fetch previous speedtest domainset',
|
||||
async () => {
|
||||
try {
|
||||
(
|
||||
await readFileIntoProcessedArray(path.resolve(__dirname, '../List/domainset/speedtest.conf'))
|
||||
) .forEach(line => {
|
||||
const hn = getHostname(line, { detectIp: false, validateHostname: true });
|
||||
if (hn) {
|
||||
domainTrie.add(hn);
|
||||
}
|
||||
});
|
||||
} catch { }
|
||||
}
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
'Hong Kong',
|
||||
'Taiwan',
|
||||
'China Telecom',
|
||||
'China Mobile',
|
||||
'China Unicom',
|
||||
'Japan',
|
||||
'Tokyo',
|
||||
'Singapore',
|
||||
'Korea',
|
||||
'Seoul',
|
||||
'Canada',
|
||||
'Toronto',
|
||||
'Montreal',
|
||||
'Los Ang',
|
||||
'San Jos',
|
||||
'Seattle',
|
||||
'New York',
|
||||
'Dallas',
|
||||
'Miami',
|
||||
'Berlin',
|
||||
'Frankfurt',
|
||||
'London',
|
||||
'Paris',
|
||||
'Amsterdam',
|
||||
'Moscow',
|
||||
'Australia',
|
||||
'Sydney',
|
||||
'Brazil',
|
||||
'Turkey'
|
||||
].map((keyword) => span.traceChildAsync(
|
||||
await Promise.all(KEYWORDS.map((keyword) => span.traceChildAsync(
|
||||
`fetch speedtest endpoints: ${keyword}`,
|
||||
() => querySpeedtestApi(keyword)
|
||||
).then(hostnameGroup => hostnameGroup.forEach(hostname => {
|
||||
if (hostname) {
|
||||
domainTrie.add(hostname);
|
||||
output.addDomain(hostname);
|
||||
}
|
||||
}))));
|
||||
|
||||
const deduped = span.traceChildSync('sort result', () => sortDomains(domainTrie.dump()));
|
||||
|
||||
const description = [
|
||||
...SHARED_DESCRIPTION,
|
||||
'',
|
||||
'This file contains common speedtest endpoints.'
|
||||
];
|
||||
|
||||
return createRuleset(
|
||||
span,
|
||||
'Sukka\'s Ruleset - Speedtest Domains',
|
||||
description,
|
||||
new Date(),
|
||||
deduped,
|
||||
'domainset',
|
||||
output('speedtest', 'domainset')
|
||||
);
|
||||
return output.write();
|
||||
});
|
||||
|
||||
249
Build/lib/create-file-new.ts
Normal file
249
Build/lib/create-file-new.ts
Normal file
@ -0,0 +1,249 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import type { Span } from '../trace';
|
||||
import { surgeDomainsetToClashDomainset } from './clash';
|
||||
import { compareAndWriteFile, withBannerArray } from './create-file';
|
||||
import { ipCidrListToSingbox, surgeDomainsetToSingbox } from './singbox';
|
||||
import { sortDomains } from './stable-sort-domain';
|
||||
import { createTrie } from './trie';
|
||||
import { invariant } from 'foxact/invariant';
|
||||
import { OUTPUT_CLASH_DIR, OUTPUT_SINGBOX_DIR, OUTPUT_SURGE_DIR } from '../constants/dir';
|
||||
import stringify from 'json-stringify-pretty-compact';
|
||||
import { appendArrayInPlace } from './append-array-in-place';
|
||||
|
||||
abstract class RuleOutput {
|
||||
protected domainTrie = createTrie<unknown>(null, true);
|
||||
protected domainKeywords = new Set<string>();
|
||||
protected domainWildcard = new Set<string>();
|
||||
protected ipcidr = new Set<string>();
|
||||
protected ipcidrNoResolve = new Set<string>();
|
||||
protected ipcidr6 = new Set<string>();
|
||||
protected ipcidr6NoResolve = new Set<string>();
|
||||
protected otherRules = new Set<string>();
|
||||
protected abstract type: 'domainset' | 'non_ip' | 'ip';
|
||||
|
||||
protected pendingPromise = Promise.resolve();
|
||||
|
||||
static jsonToLines(this: void, json: unknown): string[] {
|
||||
return stringify(json).split('\n');
|
||||
}
|
||||
|
||||
constructor(
|
||||
protected readonly span: Span,
|
||||
protected readonly id: string
|
||||
) {}
|
||||
|
||||
protected title: string | null = null;
|
||||
withTitle(title: string) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected description: string[] | readonly string[] | null = null;
|
||||
withDescription(description: string[] | readonly string[]) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected date = new Date();
|
||||
withDate(date: Date) {
|
||||
this.date = date;
|
||||
return this;
|
||||
}
|
||||
|
||||
protected apexDomainMap: Map<string, string> | null = null;
|
||||
protected subDomainMap: Map<string, string> | null = null;
|
||||
withDomainMap(apexDomainMap: Map<string, string>, subDomainMap: Map<string, string>) {
|
||||
this.apexDomainMap = apexDomainMap;
|
||||
this.subDomainMap = subDomainMap;
|
||||
return this;
|
||||
}
|
||||
|
||||
addDomain(domain: string) {
|
||||
this.domainTrie.add(domain);
|
||||
return this;
|
||||
}
|
||||
|
||||
addDomainSuffix(domain: string) {
|
||||
this.domainTrie.add(domain[0] === '.' ? domain : '.' + domain);
|
||||
return this;
|
||||
}
|
||||
|
||||
bulkAddDomainSuffix(domains: string[]) {
|
||||
for (let i = 0, len = domains.length; i < len; i++) {
|
||||
this.addDomainSuffix(domains[i]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
addDomainKeyword(keyword: string) {
|
||||
this.domainKeywords.add(keyword);
|
||||
return this;
|
||||
}
|
||||
|
||||
addDomainWildcard(wildcard: string) {
|
||||
this.domainWildcard.add(wildcard);
|
||||
return this;
|
||||
}
|
||||
|
||||
private async addFromDomainsetPromise(source: AsyncIterable<string> | Iterable<string> | string[]) {
|
||||
for await (const line of source) {
|
||||
if (line[0] === '.') {
|
||||
this.addDomainSuffix(line);
|
||||
} else {
|
||||
this.addDomain(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addFromDomainset(source: AsyncIterable<string> | Iterable<string> | string[]) {
|
||||
this.pendingPromise = this.pendingPromise.then(() => this.addFromDomainsetPromise(source));
|
||||
return this;
|
||||
}
|
||||
|
||||
async addFromRuleset(source: AsyncIterable<string> | Iterable<string>) {
|
||||
for await (const line of source) {
|
||||
const [type, value, arg] = line.split(',');
|
||||
switch (type) {
|
||||
case 'DOMAIN':
|
||||
this.addDomain(value);
|
||||
break;
|
||||
case 'DOMAIN-SUFFIX':
|
||||
this.addDomainSuffix(value);
|
||||
break;
|
||||
case 'DOMAIN-KEYWORD':
|
||||
this.addDomainKeyword(value);
|
||||
break;
|
||||
case 'DOMAIN-WILDCARD':
|
||||
this.addDomainWildcard(value);
|
||||
break;
|
||||
case 'IP-CIDR':
|
||||
(arg === 'no-resolve' ? this.ipcidrNoResolve : this.ipcidr).add(value);
|
||||
break;
|
||||
case 'IP-CIDR6':
|
||||
(arg === 'no-resolve' ? this.ipcidr6NoResolve : this.ipcidr6).add(value);
|
||||
break;
|
||||
default:
|
||||
this.otherRules.add(line);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
bulkAddCIDR4(cidr: string[]) {
|
||||
for (let i = 0, len = cidr.length; i < len; i++) {
|
||||
this.ipcidr.add(cidr[i]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
bulkAddCIDR6(cidr: string[]) {
|
||||
for (let i = 0, len = cidr.length; i < len; i++) {
|
||||
this.ipcidr6.add(cidr[i]);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
abstract write(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DomainsetOutput extends RuleOutput {
|
||||
protected type = 'domainset' as const;
|
||||
|
||||
async write() {
|
||||
await this.pendingPromise;
|
||||
|
||||
invariant(this.title, 'Missing title');
|
||||
invariant(this.description, 'Missing description');
|
||||
|
||||
const sorted = sortDomains(this.domainTrie.dump(), this.apexDomainMap, this.subDomainMap);
|
||||
sorted.push('this_ruleset_is_made_by_sukkaw.ruleset.skk.moe');
|
||||
|
||||
const surge = sorted;
|
||||
const clash = surgeDomainsetToClashDomainset(sorted);
|
||||
const singbox = RuleOutput.jsonToLines(surgeDomainsetToSingbox(sorted));
|
||||
|
||||
await Promise.all([
|
||||
compareAndWriteFile(
|
||||
this.span,
|
||||
withBannerArray(
|
||||
this.title,
|
||||
this.description,
|
||||
this.date,
|
||||
surge
|
||||
),
|
||||
path.join(OUTPUT_SURGE_DIR, this.type, this.id + '.conf')
|
||||
),
|
||||
compareAndWriteFile(
|
||||
this.span,
|
||||
withBannerArray(
|
||||
this.title,
|
||||
this.description,
|
||||
this.date,
|
||||
clash
|
||||
),
|
||||
path.join(OUTPUT_CLASH_DIR, this.type, this.id + '.txt')
|
||||
),
|
||||
compareAndWriteFile(
|
||||
this.span,
|
||||
singbox,
|
||||
path.join(OUTPUT_SINGBOX_DIR, this.type, this.id + '.json')
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
export class IPListOutput extends RuleOutput {
|
||||
protected type = 'ip' as const;
|
||||
|
||||
constructor(span: Span, id: string, private readonly clashUseRule = true) {
|
||||
super(span, id);
|
||||
}
|
||||
|
||||
async write() {
|
||||
await this.pendingPromise;
|
||||
|
||||
invariant(this.title, 'Missing title');
|
||||
invariant(this.description, 'Missing description');
|
||||
|
||||
const sorted4 = Array.from(this.ipcidr);
|
||||
const sorted6 = Array.from(this.ipcidr6);
|
||||
const merged = appendArrayInPlace(appendArrayInPlace([], sorted4), sorted6);
|
||||
|
||||
const surge = sorted4.map(i => `IP-CIDR,${i}`);
|
||||
appendArrayInPlace(surge, sorted6.map(i => `IP-CIDR6,${i}`));
|
||||
surge.push('DOMAIN,this_ruleset_is_made_by_sukkaw.ruleset.skk.moe');
|
||||
|
||||
const clash = this.clashUseRule ? surge : merged;
|
||||
const singbox = RuleOutput.jsonToLines(ipCidrListToSingbox(merged));
|
||||
|
||||
await Promise.all([
|
||||
compareAndWriteFile(
|
||||
this.span,
|
||||
withBannerArray(
|
||||
this.title,
|
||||
this.description,
|
||||
this.date,
|
||||
surge
|
||||
),
|
||||
path.join(OUTPUT_SURGE_DIR, this.type, this.id + '.conf')
|
||||
),
|
||||
compareAndWriteFile(
|
||||
this.span,
|
||||
withBannerArray(
|
||||
this.title,
|
||||
this.description,
|
||||
this.date,
|
||||
clash
|
||||
),
|
||||
path.join(OUTPUT_CLASH_DIR, this.type, this.id + '.txt')
|
||||
),
|
||||
compareAndWriteFile(
|
||||
this.span,
|
||||
singbox,
|
||||
path.join(OUTPUT_SINGBOX_DIR, this.type, this.id + '.json')
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ import fs from 'node:fs';
|
||||
import { fastStringArrayJoin, writeFile } from './misc';
|
||||
import { readFileByLine } from './fetch-text-by-line';
|
||||
import stringify from 'json-stringify-pretty-compact';
|
||||
import { ipCidrListToSingbox, surgeDomainsetToSingbox, surgeRulesetToSingbox } from './singbox';
|
||||
import { surgeDomainsetToSingbox, surgeRulesetToSingbox } from './singbox';
|
||||
import { createTrie } from './trie';
|
||||
import { pack, unpackFirst, unpackSecond } from './bitwise';
|
||||
import { asyncWriteToStream } from './async-write-to-stream';
|
||||
@ -23,8 +23,7 @@ export const fileEqual = async (linesA: string[], source: AsyncIterable<string>)
|
||||
|
||||
if (index > linesA.length - 1) {
|
||||
if (index === linesA.length && lineB === '') {
|
||||
index--;
|
||||
continue;
|
||||
return true;
|
||||
}
|
||||
// The file becomes smaller
|
||||
return false;
|
||||
@ -51,7 +50,7 @@ export const fileEqual = async (linesA: string[], source: AsyncIterable<string>)
|
||||
}
|
||||
}
|
||||
|
||||
if (index !== linesA.length - 1) {
|
||||
if (index < linesA.length - 1) {
|
||||
// The file becomes larger
|
||||
return false;
|
||||
}
|
||||
@ -96,7 +95,7 @@ export async function compareAndWriteFile(span: Span, linesA: string[], filePath
|
||||
});
|
||||
}
|
||||
|
||||
const withBannerArray = (title: string, description: string[] | readonly string[], date: Date, content: string[]) => {
|
||||
export const withBannerArray = (title: string, description: string[] | readonly string[], date: Date, content: string[]) => {
|
||||
return [
|
||||
'#########################################',
|
||||
`# ${title}`,
|
||||
@ -191,7 +190,7 @@ const MARK = 'this_ruleset_is_made_by_sukkaw.ruleset.skk.moe';
|
||||
export const createRuleset = (
|
||||
parentSpan: Span,
|
||||
title: string, description: string[] | readonly string[], date: Date, content: string[],
|
||||
type: 'ruleset' | 'domainset' | 'ipcidr' | 'ipcidr6',
|
||||
type: 'ruleset' | 'domainset',
|
||||
[surgePath, clashPath, singBoxPath, _clashMrsPath]: readonly [
|
||||
surgePath: string,
|
||||
clashPath: string,
|
||||
@ -210,12 +209,6 @@ export const createRuleset = (
|
||||
case 'ruleset':
|
||||
_surgeContent = [`DOMAIN,${MARK}`, ...processRuleSet(content)];
|
||||
break;
|
||||
case 'ipcidr':
|
||||
_surgeContent = [`DOMAIN,${MARK}`, ...processRuleSet(content.map(i => `IP-CIDR,${i}`))];
|
||||
break;
|
||||
case 'ipcidr6':
|
||||
_surgeContent = [`DOMAIN,${MARK}`, ...processRuleSet(content.map(i => `IP-CIDR6,${i}`))];
|
||||
break;
|
||||
default:
|
||||
throw new TypeError(`Unknown type: ${type}`);
|
||||
}
|
||||
@ -232,10 +225,6 @@ export const createRuleset = (
|
||||
case 'ruleset':
|
||||
_clashContent = [`DOMAIN,${MARK}`, ...surgeRulesetToClashClassicalTextRuleset(processRuleSet(content))];
|
||||
break;
|
||||
case 'ipcidr':
|
||||
case 'ipcidr6':
|
||||
_clashContent = content;
|
||||
break;
|
||||
default:
|
||||
throw new TypeError(`Unknown type: ${type}`);
|
||||
}
|
||||
@ -250,10 +239,6 @@ export const createRuleset = (
|
||||
case 'ruleset':
|
||||
_singBoxContent = surgeRulesetToSingbox([`DOMAIN,${MARK}`, ...processRuleSet(content)]);
|
||||
break;
|
||||
case 'ipcidr':
|
||||
case 'ipcidr6':
|
||||
_singBoxContent = ipCidrListToSingbox(content);
|
||||
break;
|
||||
default:
|
||||
throw new TypeError(`Unknown type: ${type}`);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user