mirror of
https://github.com/SukkaW/Surge.git
synced 2025-12-16 03:00:34 +08:00
Chore: refactor output dir
This commit is contained in:
parent
5bc2e017df
commit
315b38b999
14
.gitignore
vendored
14
.gitignore
vendored
@ -5,17 +5,3 @@ node_modules
|
|||||||
.cache
|
.cache
|
||||||
public
|
public
|
||||||
tmp*
|
tmp*
|
||||||
|
|
||||||
# $ build output
|
|
||||||
List/
|
|
||||||
Clash/
|
|
||||||
Internal/
|
|
||||||
sing-box/
|
|
||||||
Modules/sukka_local_dns_mapping.sgmodule
|
|
||||||
Modules/sukka_url_redirect.sgmodule
|
|
||||||
Modules/sukka_common_always_realip.sgmodule
|
|
||||||
Mock/www-google-analytics-com_ga.js
|
|
||||||
Mock/www-googletagservices-com_gpt.js
|
|
||||||
Mock/www-google-analytics-com_analytics.js
|
|
||||||
Mock/www-googlesyndication-com_adsbygoogle.js
|
|
||||||
Mock/amazon-adsystem-com_amazon-apstag.js
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { domainDeduper } from './lib/domain-deduper';
|
|||||||
import { appendArrayInPlace } from './lib/append-array-in-place';
|
import { appendArrayInPlace } from './lib/append-array-in-place';
|
||||||
import { sortDomains } from './lib/stable-sort-domain';
|
import { sortDomains } from './lib/stable-sort-domain';
|
||||||
import { output } from './lib/misc';
|
import { output } from './lib/misc';
|
||||||
|
import { SOURCE_DIR } from './constants/dir';
|
||||||
|
|
||||||
const getS3OSSDomainsPromise = (async (): Promise<string[]> => {
|
const getS3OSSDomainsPromise = (async (): Promise<string[]> => {
|
||||||
const trie = createTrie(
|
const trie = createTrie(
|
||||||
@ -58,9 +59,9 @@ export const buildCdnDownloadConf = task(require.main === module, __filename)(as
|
|||||||
steamDomainSet
|
steamDomainSet
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getS3OSSDomainsPromise,
|
getS3OSSDomainsPromise,
|
||||||
readFileIntoProcessedArray(path.resolve(__dirname, '../Source/domainset/cdn.conf')),
|
readFileIntoProcessedArray(path.join(SOURCE_DIR, 'domainset/cdn.conf')),
|
||||||
readFileIntoProcessedArray(path.resolve(__dirname, '../Source/domainset/download.conf')),
|
readFileIntoProcessedArray(path.join(SOURCE_DIR, 'domainset/download.conf')),
|
||||||
readFileIntoProcessedArray(path.resolve(__dirname, '../Source/domainset/steam.conf'))
|
readFileIntoProcessedArray(path.join(SOURCE_DIR, 'domainset/steam.conf'))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
appendArrayInPlace(downloadDomainSet, S3OSSDomains.map(domain => `.${domain}`));
|
appendArrayInPlace(downloadDomainSet, S3OSSDomains.map(domain => `.${domain}`));
|
||||||
|
|||||||
@ -11,17 +11,13 @@ import { SHARED_DESCRIPTION } from './lib/constants';
|
|||||||
import { fdir as Fdir } from 'fdir';
|
import { fdir as Fdir } from 'fdir';
|
||||||
import { appendArrayInPlace } from './lib/append-array-in-place';
|
import { appendArrayInPlace } from './lib/append-array-in-place';
|
||||||
import { removeFiles } from './lib/misc';
|
import { removeFiles } from './lib/misc';
|
||||||
|
import { OUTPUT_CLASH_DIR, OUTPUT_SINGBOX_DIR, OUTPUT_SURGE_DIR, SOURCE_DIR } from './constants/dir';
|
||||||
|
|
||||||
const MAGIC_COMMAND_SKIP = '# $ custom_build_script';
|
const MAGIC_COMMAND_SKIP = '# $ custom_build_script';
|
||||||
const MAGIC_COMMAND_RM = '# $ custom_no_output';
|
const MAGIC_COMMAND_RM = '# $ custom_no_output';
|
||||||
const MAGIC_COMMAND_TITLE = '# $ meta_title ';
|
const MAGIC_COMMAND_TITLE = '# $ meta_title ';
|
||||||
const MAGIC_COMMAND_DESCRIPTION = '# $ meta_description ';
|
const MAGIC_COMMAND_DESCRIPTION = '# $ meta_description ';
|
||||||
|
|
||||||
const sourceDir = path.resolve(__dirname, '../Source');
|
|
||||||
const outputSurgeDir = path.resolve(__dirname, '../List');
|
|
||||||
const outputClashDir = path.resolve(__dirname, '../Clash');
|
|
||||||
const outputSingboxDir = path.resolve(__dirname, '../sing-box');
|
|
||||||
|
|
||||||
const domainsetSrcFolder = 'domainset' + path.sep;
|
const domainsetSrcFolder = 'domainset' + path.sep;
|
||||||
|
|
||||||
export const buildCommon = task(require.main === module, __filename)(async (span) => {
|
export const buildCommon = task(require.main === module, __filename)(async (span) => {
|
||||||
@ -46,12 +42,12 @@ export const buildCommon = task(require.main === module, __filename)(async (span
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.crawl(sourceDir)
|
.crawl(SOURCE_DIR)
|
||||||
.withPromise();
|
.withPromise();
|
||||||
|
|
||||||
for (let i = 0, len = paths.length; i < len; i++) {
|
for (let i = 0, len = paths.length; i < len; i++) {
|
||||||
const relativePath = paths[i];
|
const relativePath = paths[i];
|
||||||
const fullPath = sourceDir + path.sep + relativePath;
|
const fullPath = SOURCE_DIR + path.sep + relativePath;
|
||||||
|
|
||||||
if (relativePath.startsWith(domainsetSrcFolder)) {
|
if (relativePath.startsWith(domainsetSrcFolder)) {
|
||||||
promises.push(transformDomainset(span, fullPath, relativePath));
|
promises.push(transformDomainset(span, fullPath, relativePath));
|
||||||
@ -127,9 +123,9 @@ function transformDomainset(parentSpan: Span, sourcePath: string, relativePath:
|
|||||||
|
|
||||||
if (res === $rm) {
|
if (res === $rm) {
|
||||||
return removeFiles([
|
return removeFiles([
|
||||||
path.resolve(outputSurgeDir, relativePath),
|
path.resolve(OUTPUT_SURGE_DIR, relativePath),
|
||||||
path.resolve(outputClashDir, `${clashFileBasename}.txt`),
|
path.resolve(OUTPUT_CLASH_DIR, `${clashFileBasename}.txt`),
|
||||||
path.resolve(outputSingboxDir, `${clashFileBasename}.json`)
|
path.resolve(OUTPUT_SINGBOX_DIR, `${clashFileBasename}.json`)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,9 +149,9 @@ function transformDomainset(parentSpan: Span, sourcePath: string, relativePath:
|
|||||||
deduped,
|
deduped,
|
||||||
'domainset',
|
'domainset',
|
||||||
[
|
[
|
||||||
path.resolve(outputSurgeDir, relativePath),
|
path.resolve(OUTPUT_SURGE_DIR, relativePath),
|
||||||
path.resolve(outputClashDir, `${clashFileBasename}.txt`),
|
path.resolve(OUTPUT_CLASH_DIR, `${clashFileBasename}.txt`),
|
||||||
path.resolve(outputSingboxDir, `${clashFileBasename}.json`)
|
path.resolve(OUTPUT_SINGBOX_DIR, `${clashFileBasename}.json`)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -176,9 +172,9 @@ async function transformRuleset(parentSpan: Span, sourcePath: string, relativePa
|
|||||||
|
|
||||||
if (res === $rm) {
|
if (res === $rm) {
|
||||||
return removeFiles([
|
return removeFiles([
|
||||||
path.resolve(outputSurgeDir, relativePath),
|
path.resolve(OUTPUT_SURGE_DIR, relativePath),
|
||||||
path.resolve(outputClashDir, `${clashFileBasename}.txt`),
|
path.resolve(OUTPUT_CLASH_DIR, `${clashFileBasename}.txt`),
|
||||||
path.resolve(outputSingboxDir, `${clashFileBasename}.json`)
|
path.resolve(OUTPUT_SINGBOX_DIR, `${clashFileBasename}.json`)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,9 +197,9 @@ async function transformRuleset(parentSpan: Span, sourcePath: string, relativePa
|
|||||||
lines,
|
lines,
|
||||||
'ruleset',
|
'ruleset',
|
||||||
[
|
[
|
||||||
path.resolve(outputSurgeDir, relativePath),
|
path.resolve(OUTPUT_SURGE_DIR, relativePath),
|
||||||
path.resolve(outputClashDir, `${clashFileBasename}.txt`),
|
path.resolve(OUTPUT_CLASH_DIR, `${clashFileBasename}.txt`),
|
||||||
path.resolve(outputSingboxDir, `${clashFileBasename}.json`)
|
path.resolve(OUTPUT_SINGBOX_DIR, `${clashFileBasename}.json`)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { OUTPUT_CLASH_DIR, OUTPUT_SURGE_DIR } from './constants/dir';
|
||||||
import { compareAndWriteFile } from './lib/create-file';
|
import { compareAndWriteFile } from './lib/create-file';
|
||||||
import { task } from './trace';
|
import { task } from './trace';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
@ -8,15 +9,12 @@ const DEPRECATED_FILES = [
|
|||||||
['domainset/reject_phishing', 'This file has been merged with domainset/reject']
|
['domainset/reject_phishing', 'This file has been merged with domainset/reject']
|
||||||
];
|
];
|
||||||
|
|
||||||
const outputSurgeDir = path.resolve(__dirname, '../List');
|
|
||||||
const outputClashDir = path.resolve(__dirname, '../Clash');
|
|
||||||
|
|
||||||
export const buildDeprecateFiles = task(require.main === module, __filename)((span) => span.traceChildAsync('create deprecated files', async (childSpan) => {
|
export const buildDeprecateFiles = task(require.main === module, __filename)((span) => span.traceChildAsync('create deprecated files', async (childSpan) => {
|
||||||
const promises: Array<Promise<unknown>> = [];
|
const promises: Array<Promise<unknown>> = [];
|
||||||
|
|
||||||
for (const [filePath, description] of DEPRECATED_FILES) {
|
for (const [filePath, description] of DEPRECATED_FILES) {
|
||||||
const surgeFile = path.resolve(outputSurgeDir, `${filePath}.conf`);
|
const surgeFile = path.resolve(OUTPUT_SURGE_DIR, `${filePath}.conf`);
|
||||||
const clashFile = path.resolve(outputClashDir, `${filePath}.txt`);
|
const clashFile = path.resolve(OUTPUT_CLASH_DIR, `${filePath}.txt`);
|
||||||
|
|
||||||
const content = [
|
const content = [
|
||||||
'#########################################',
|
'#########################################',
|
||||||
|
|||||||
@ -10,10 +10,11 @@ import { createMemoizedPromise } from './lib/memo-promise';
|
|||||||
import * as yaml from 'yaml';
|
import * as yaml from 'yaml';
|
||||||
import { appendArrayInPlace } from './lib/append-array-in-place';
|
import { appendArrayInPlace } from './lib/append-array-in-place';
|
||||||
import { output, writeFile } from './lib/misc';
|
import { output, writeFile } from './lib/misc';
|
||||||
|
import { OUTPUT_INTERNAL_DIR, OUTPUT_MODULES_DIR, SOURCE_DIR } from './constants/dir';
|
||||||
|
|
||||||
export const getDomesticAndDirectDomainsRulesetPromise = createMemoizedPromise(async () => {
|
export const getDomesticAndDirectDomainsRulesetPromise = createMemoizedPromise(async () => {
|
||||||
const domestics = await readFileIntoProcessedArray(path.resolve(__dirname, '../Source/non_ip/domestic.conf'));
|
const domestics = await readFileIntoProcessedArray(path.join(SOURCE_DIR, 'non_ip/domestic.conf'));
|
||||||
const directs = await readFileIntoProcessedArray(path.resolve(__dirname, '../Source/non_ip/direct.conf'));
|
const directs = await readFileIntoProcessedArray(path.resolve(SOURCE_DIR, 'non_ip/direct.conf'));
|
||||||
const lans: string[] = [];
|
const lans: string[] = [];
|
||||||
|
|
||||||
Object.entries(DOMESTICS).forEach(([, { domains }]) => {
|
Object.entries(DOMESTICS).forEach(([, { domains }]) => {
|
||||||
@ -91,10 +92,10 @@ export const buildDomesticRuleset = task(require.main === module, __filename)(as
|
|||||||
])
|
])
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
path.resolve(__dirname, '../Modules/sukka_local_dns_mapping.sgmodule')
|
path.resolve(OUTPUT_MODULES_DIR, 'sukka_local_dns_mapping.sgmodule')
|
||||||
),
|
),
|
||||||
writeFile(
|
writeFile(
|
||||||
path.resolve(__dirname, '../Internal/clash_nameserver_policy.yaml'),
|
path.join(OUTPUT_INTERNAL_DIR, 'clash_nameserver_policy.yaml'),
|
||||||
yaml.stringify(
|
yaml.stringify(
|
||||||
{
|
{
|
||||||
dns: {
|
dns: {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { getChnCidrPromise } from './build-chn-cidr';
|
|||||||
import { NON_CN_CIDR_INCLUDED_IN_CHNROUTE, RESERVED_IPV4_CIDR } from './constants/cidr';
|
import { NON_CN_CIDR_INCLUDED_IN_CHNROUTE, RESERVED_IPV4_CIDR } from './constants/cidr';
|
||||||
|
|
||||||
import { writeFile } from './lib/misc';
|
import { writeFile } from './lib/misc';
|
||||||
|
import { OUTPUT_INTERNAL_DIR } from './constants/dir';
|
||||||
|
|
||||||
export const buildInternalReverseChnCIDR = task(require.main === module, __filename)(async () => {
|
export const buildInternalReverseChnCIDR = task(require.main === module, __filename)(async () => {
|
||||||
const [cidr] = await getChnCidrPromise();
|
const [cidr] = await getChnCidrPromise();
|
||||||
@ -21,8 +22,7 @@ export const buildInternalReverseChnCIDR = task(require.main === module, __filen
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const outputDir = path.resolve(__dirname, '../Internal');
|
const outputFile = path.join(OUTPUT_INTERNAL_DIR, 'reversed-chn-cidr.txt');
|
||||||
const outputFile = path.join(outputDir, 'reversed-chn-cidr.txt');
|
|
||||||
|
|
||||||
return writeFile(
|
return writeFile(
|
||||||
outputFile,
|
outputFile,
|
||||||
|
|||||||
@ -1,69 +1,52 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import fsp from 'node:fs/promises';
|
import fsp from 'node:fs/promises';
|
||||||
|
|
||||||
import { task } from './trace';
|
import { task } from './trace';
|
||||||
import { treeDir } from './lib/tree-dir';
|
import { treeDir } from './lib/tree-dir';
|
||||||
import type { TreeType, TreeTypeArray } from './lib/tree-dir';
|
import type { TreeType, TreeTypeArray } from './lib/tree-dir';
|
||||||
import { fdir as Fdir } from 'fdir';
|
|
||||||
|
|
||||||
import Trie from 'mnemonist/trie';
|
import { OUTPUT_MOCK_DIR, OUTPUT_MODULES_DIR, PUBLIC_DIR, ROOT_DIR } from './constants/dir';
|
||||||
import { writeFile } from './lib/misc';
|
import { writeFile } from './lib/misc';
|
||||||
|
import picocolors from 'picocolors';
|
||||||
|
|
||||||
const rootPath = path.resolve(__dirname, '../');
|
const mockDir = path.join(ROOT_DIR, 'Mock');
|
||||||
const publicPath = path.resolve(__dirname, '../public');
|
const modulesDir = path.join(ROOT_DIR, 'Modules');
|
||||||
|
|
||||||
const folderAndFilesToBeDeployed = [
|
const copyDirContents = async (srcDir: string, destDir: string) => {
|
||||||
`Mock${path.sep}`,
|
const promises: Array<Promise<void>> = [];
|
||||||
`List${path.sep}`,
|
|
||||||
`Clash${path.sep}`,
|
for await (const entry of await fsp.opendir(srcDir)) {
|
||||||
`sing-box${path.sep}`,
|
const src = path.join(srcDir, entry.name);
|
||||||
`Modules${path.sep}`,
|
const dest = path.join(destDir, entry.name);
|
||||||
`Script${path.sep}`,
|
if (entry.isDirectory()) {
|
||||||
`Internal${path.sep}`,
|
console.warn(picocolors.red('[build public] cant copy directory'), src);
|
||||||
'LICENSE'
|
} else {
|
||||||
];
|
promises.push(fsp.copyFile(src, dest, fs.constants.COPYFILE_FICLONE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
export const buildPublic = task(require.main === module, __filename)(async (span) => {
|
export const buildPublic = task(require.main === module, __filename)(async (span) => {
|
||||||
fs.mkdirSync(publicPath, { recursive: true });
|
await span.traceChildAsync('copy rest of the files', async () => {
|
||||||
|
await Promise.all([
|
||||||
|
fsp.mkdir(OUTPUT_MODULES_DIR, { recursive: true }),
|
||||||
|
fsp.mkdir(OUTPUT_MOCK_DIR, { recursive: true })
|
||||||
|
]);
|
||||||
|
|
||||||
await span
|
await Promise.all([
|
||||||
.traceChild('copy public files')
|
copyDirContents(modulesDir, OUTPUT_MODULES_DIR),
|
||||||
.traceAsyncFn(async () => {
|
copyDirContents(mockDir, OUTPUT_MOCK_DIR)
|
||||||
const trie = Trie.from(await new Fdir()
|
]);
|
||||||
.withRelativePaths()
|
});
|
||||||
.exclude((dirName) => (
|
|
||||||
dirName === 'node_modules'
|
|
||||||
|| dirName === 'Build'
|
|
||||||
|| dirName === 'public'
|
|
||||||
|| dirName[0] === '.'
|
|
||||||
))
|
|
||||||
.crawl(rootPath)
|
|
||||||
.withPromise());
|
|
||||||
|
|
||||||
const filesToBeCopied = folderAndFilesToBeDeployed.flatMap(folderOrFile => trie.find(folderOrFile));
|
|
||||||
|
|
||||||
return Promise.all(filesToBeCopied.map(file => {
|
|
||||||
const src = path.join(rootPath, file);
|
|
||||||
const dest = path.join(publicPath, file);
|
|
||||||
|
|
||||||
const destParen = path.dirname(dest);
|
|
||||||
if (!fs.existsSync(destParen)) {
|
|
||||||
fs.mkdirSync(destParen, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
return fsp.copyFile(
|
|
||||||
src,
|
|
||||||
dest,
|
|
||||||
fs.constants.COPYFILE_FICLONE
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const html = await span
|
const html = await span
|
||||||
.traceChild('generate index.html')
|
.traceChild('generate index.html')
|
||||||
.traceAsyncFn(() => treeDir(publicPath).then(generateHtml));
|
.traceAsyncFn(() => treeDir(PUBLIC_DIR).then(generateHtml));
|
||||||
|
|
||||||
return writeFile(path.join(publicPath, 'index.html'), html);
|
return writeFile(path.join(PUBLIC_DIR, 'index.html'), html);
|
||||||
});
|
});
|
||||||
|
|
||||||
const priorityOrder: Record<'default' | string & {}, number> = {
|
const priorityOrder: Record<'default' | string & {}, number> = {
|
||||||
|
|||||||
@ -21,8 +21,9 @@ import { getPhishingDomains } from './lib/get-phishing-domains';
|
|||||||
import { setAddFromArray, setAddFromArrayCurried } from './lib/set-add-from-array';
|
import { setAddFromArray, setAddFromArrayCurried } from './lib/set-add-from-array';
|
||||||
import { output } from './lib/misc';
|
import { output } from './lib/misc';
|
||||||
import { appendArrayInPlace } from './lib/append-array-in-place';
|
import { appendArrayInPlace } from './lib/append-array-in-place';
|
||||||
|
import { OUTPUT_INTERNAL_DIR, SOURCE_DIR } from './constants/dir';
|
||||||
|
|
||||||
const getRejectSukkaConfPromise = readFileIntoProcessedArray(path.resolve(__dirname, '../Source/domainset/reject_sukka.conf'));
|
const getRejectSukkaConfPromise = readFileIntoProcessedArray(path.join(SOURCE_DIR, 'domainset/reject_sukka.conf'));
|
||||||
|
|
||||||
export const buildRejectDomainSet = task(require.main === module, __filename)(async (span) => {
|
export const buildRejectDomainSet = task(require.main === module, __filename)(async (span) => {
|
||||||
/** Whitelists */
|
/** Whitelists */
|
||||||
@ -214,7 +215,7 @@ export const buildRejectDomainSet = task(require.main === module, __filename)(as
|
|||||||
compareAndWriteFile(
|
compareAndWriteFile(
|
||||||
span,
|
span,
|
||||||
rejectDomainsStats,
|
rejectDomainsStats,
|
||||||
path.resolve(__dirname, '../Internal/reject-stats.txt')
|
path.join(OUTPUT_INTERNAL_DIR, 'reject-stats.txt')
|
||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { compareAndWriteFile } from './lib/create-file';
|
|||||||
import { DIRECTS, LANS } from '../Source/non_ip/direct';
|
import { DIRECTS, LANS } from '../Source/non_ip/direct';
|
||||||
import * as yaml from 'yaml';
|
import * as yaml from 'yaml';
|
||||||
import { writeFile } from './lib/misc';
|
import { writeFile } from './lib/misc';
|
||||||
|
import { OUTPUT_INTERNAL_DIR, OUTPUT_MODULES_DIR } from './constants/dir';
|
||||||
|
|
||||||
const HOSTNAMES = [
|
const HOSTNAMES = [
|
||||||
// Network Detection, Captive Portal
|
// Network Detection, Captive Portal
|
||||||
@ -59,10 +60,10 @@ export const buildAlwaysRealIPModule = task(require.main === module, __filename)
|
|||||||
'[General]',
|
'[General]',
|
||||||
`always-real-ip = %APPEND% ${HOSTNAMES.concat(surge).join(', ')}`
|
`always-real-ip = %APPEND% ${HOSTNAMES.concat(surge).join(', ')}`
|
||||||
],
|
],
|
||||||
path.resolve(__dirname, '../Modules/sukka_common_always_realip.sgmodule')
|
path.resolve(OUTPUT_MODULES_DIR, 'sukka_common_always_realip.sgmodule')
|
||||||
),
|
),
|
||||||
writeFile(
|
writeFile(
|
||||||
path.resolve(__dirname, '../Internal/clash_fake_ip_filter.yaml'),
|
path.join(OUTPUT_INTERNAL_DIR, 'clash_fake_ip_filter.yaml'),
|
||||||
yaml.stringify(
|
yaml.stringify(
|
||||||
{
|
{
|
||||||
dns: {
|
dns: {
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { task } from './trace';
|
|||||||
import { compareAndWriteFile } from './lib/create-file';
|
import { compareAndWriteFile } from './lib/create-file';
|
||||||
import { getHostname } from 'tldts';
|
import { getHostname } from 'tldts';
|
||||||
import { isTruthy } from './lib/misc';
|
import { isTruthy } from './lib/misc';
|
||||||
|
import { OUTPUT_MODULES_DIR } from './constants/dir';
|
||||||
|
|
||||||
function escapeRegExp(string = '') {
|
function escapeRegExp(string = '') {
|
||||||
const reRegExpChar = /[$()*+.?[\\\]^{|}]/g;
|
const reRegExpChar = /[$()*+.?[\\\]^{|}]/g;
|
||||||
@ -148,6 +149,6 @@ export const buildRedirectModule = task(require.main === module, __filename)(asy
|
|||||||
...REDIRECT_MIRROR.map(([from, to]) => `^https?://${escapeRegExp(from)}(.*) ${to}$1 header`),
|
...REDIRECT_MIRROR.map(([from, to]) => `^https?://${escapeRegExp(from)}(.*) ${to}$1 header`),
|
||||||
...REDIRECT_FAKEWEBSITES.map(([from, to]) => `^https?://(www.)?${escapeRegExp(from)} ${to} 307`)
|
...REDIRECT_FAKEWEBSITES.map(([from, to]) => `^https?://(www.)?${escapeRegExp(from)} ${to} 307`)
|
||||||
],
|
],
|
||||||
path.resolve(__dirname, '../Modules/sukka_url_redirect.sgmodule')
|
path.join(OUTPUT_MODULES_DIR, 'sukka_url_redirect.sgmodule')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { compareAndWriteFile } from './lib/create-file';
|
|||||||
import { getMicrosoftCdnRulesetPromise } from './build-microsoft-cdn';
|
import { getMicrosoftCdnRulesetPromise } from './build-microsoft-cdn';
|
||||||
import { isTruthy } from './lib/misc';
|
import { isTruthy } from './lib/misc';
|
||||||
import { appendArrayInPlace } from './lib/append-array-in-place';
|
import { appendArrayInPlace } from './lib/append-array-in-place';
|
||||||
|
import { OUTPUT_INTERNAL_DIR, SOURCE_DIR } from './constants/dir';
|
||||||
|
|
||||||
const POLICY_GROUPS: Array<[name: string, insertProxy: boolean, insertDirect: boolean]> = [
|
const POLICY_GROUPS: Array<[name: string, insertProxy: boolean, insertDirect: boolean]> = [
|
||||||
['Default Proxy', true, false],
|
['Default Proxy', true, false],
|
||||||
@ -55,18 +56,18 @@ export const buildSSPanelUIMAppProfile = task(require.main === module, __filenam
|
|||||||
),
|
),
|
||||||
getAppleCdnDomainsPromise().then(domains => domains.map(domain => `DOMAIN-SUFFIX,${domain}`)),
|
getAppleCdnDomainsPromise().then(domains => domains.map(domain => `DOMAIN-SUFFIX,${domain}`)),
|
||||||
getMicrosoftCdnRulesetPromise().then(surgeRulesetToClashClassicalTextRuleset),
|
getMicrosoftCdnRulesetPromise().then(surgeRulesetToClashClassicalTextRuleset),
|
||||||
readFileIntoProcessedArray(path.resolve(__dirname, '../Source/non_ip/apple_cn.conf')),
|
readFileIntoProcessedArray(path.join(SOURCE_DIR, 'non_ip/apple_cn.conf')),
|
||||||
readFileIntoProcessedArray(path.resolve(__dirname, '../Source/non_ip/neteasemusic.conf')).then(surgeRulesetToClashClassicalTextRuleset),
|
readFileIntoProcessedArray(path.join(SOURCE_DIR, 'non_ip/neteasemusic.conf')).then(surgeRulesetToClashClassicalTextRuleset),
|
||||||
// microsoft & apple - domains
|
// microsoft & apple - domains
|
||||||
readFileIntoProcessedArray(path.resolve(__dirname, '../Source/non_ip/microsoft.conf')),
|
readFileIntoProcessedArray(path.join(SOURCE_DIR, 'non_ip/microsoft.conf')),
|
||||||
readFileIntoProcessedArray(path.resolve(__dirname, '../Source/non_ip/apple_services.conf')).then(surgeRulesetToClashClassicalTextRuleset),
|
readFileIntoProcessedArray(path.join(SOURCE_DIR, 'non_ip/apple_services.conf')).then(surgeRulesetToClashClassicalTextRuleset),
|
||||||
// stream - domains
|
// stream - domains
|
||||||
surgeRulesetToClashClassicalTextRuleset(AllStreamServices.flatMap((i) => i.rules)),
|
surgeRulesetToClashClassicalTextRuleset(AllStreamServices.flatMap((i) => i.rules)),
|
||||||
// steam - domains
|
// steam - domains
|
||||||
readFileIntoProcessedArray(path.resolve(__dirname, '../Source/domainset/steam.conf')).then(surgeDomainsetToClashRuleset),
|
readFileIntoProcessedArray(path.join(SOURCE_DIR, 'domainset/steam.conf')).then(surgeDomainsetToClashRuleset),
|
||||||
// global - domains
|
// global - domains
|
||||||
readFileIntoProcessedArray(path.resolve(__dirname, '../Source/non_ip/global.conf')).then(surgeRulesetToClashClassicalTextRuleset),
|
readFileIntoProcessedArray(path.join(SOURCE_DIR, 'non_ip/global.conf')).then(surgeRulesetToClashClassicalTextRuleset),
|
||||||
readFileIntoProcessedArray(path.resolve(__dirname, '../Source/non_ip/telegram.conf')).then(surgeRulesetToClashClassicalTextRuleset),
|
readFileIntoProcessedArray(path.join(SOURCE_DIR, 'non_ip/telegram.conf')).then(surgeRulesetToClashClassicalTextRuleset),
|
||||||
// domestic - ip cidr
|
// domestic - ip cidr
|
||||||
getChnCidrPromise().then(([cidrs4, cidrs6]) => [
|
getChnCidrPromise().then(([cidrs4, cidrs6]) => [
|
||||||
...cidrs4.map(cidr => `IP-CIDR,${cidr}`),
|
...cidrs4.map(cidr => `IP-CIDR,${cidr}`),
|
||||||
@ -83,7 +84,7 @@ export const buildSSPanelUIMAppProfile = task(require.main === module, __filenam
|
|||||||
// global - ip cidr
|
// global - ip cidr
|
||||||
getTelegramCIDRPromise(),
|
getTelegramCIDRPromise(),
|
||||||
// lan - ip cidr
|
// lan - ip cidr
|
||||||
readFileIntoProcessedArray(path.resolve(__dirname, '../Source/ip/lan.conf'))
|
readFileIntoProcessedArray(path.join(SOURCE_DIR, 'ip/lan.conf'))
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const telegramCidrs = rawTelegramCidrs.map(removeNoResolved);
|
const telegramCidrs = rawTelegramCidrs.map(removeNoResolved);
|
||||||
@ -121,7 +122,7 @@ export const buildSSPanelUIMAppProfile = task(require.main === module, __filenam
|
|||||||
await compareAndWriteFile(
|
await compareAndWriteFile(
|
||||||
span,
|
span,
|
||||||
output,
|
output,
|
||||||
path.resolve(__dirname, '../Internal/appprofile.php')
|
path.resolve(OUTPUT_INTERNAL_DIR, 'appprofile.php')
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
13
Build/constants/dir.ts
Normal file
13
Build/constants/dir.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
export const ROOT_DIR = path.resolve(__dirname, '../..');
|
||||||
|
|
||||||
|
export const SOURCE_DIR = path.join(ROOT_DIR, 'Source');
|
||||||
|
|
||||||
|
export const PUBLIC_DIR = path.resolve(ROOT_DIR, 'public');
|
||||||
|
export const OUTPUT_SURGE_DIR = path.join(PUBLIC_DIR, 'List');
|
||||||
|
export const OUTPUT_CLASH_DIR = path.resolve(PUBLIC_DIR, 'Clash');
|
||||||
|
export const OUTPUT_SINGBOX_DIR = path.resolve(PUBLIC_DIR, 'sing-box');
|
||||||
|
export const OUTPUT_MODULES_DIR = path.resolve(PUBLIC_DIR, 'Modules');
|
||||||
|
export const OUTPUT_INTERNAL_DIR = path.resolve(PUBLIC_DIR, 'Internal');
|
||||||
|
export const OUTPUT_MOCK_DIR = path.resolve(PUBLIC_DIR, 'Mock');
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import { task } from './trace';
|
import { task } from './trace';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import fsp from 'node:fs/promises';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
import { pipeline } from 'node:stream/promises';
|
import { pipeline } from 'node:stream/promises';
|
||||||
import { fetchWithRetry } from './lib/fetch-retry';
|
import { fetchWithRetry } from './lib/fetch-retry';
|
||||||
|
import { OUTPUT_MOCK_DIR } from './constants/dir';
|
||||||
|
|
||||||
const ASSETS_LIST = {
|
const ASSETS_LIST = {
|
||||||
'www-google-analytics-com_ga.js': 'https://raw.githubusercontent.com/AdguardTeam/Scriptlets/master/dist/redirect-files/google-analytics-ga.js',
|
'www-google-analytics-com_ga.js': 'https://raw.githubusercontent.com/AdguardTeam/Scriptlets/master/dist/redirect-files/google-analytics-ga.js',
|
||||||
@ -13,19 +15,20 @@ const ASSETS_LIST = {
|
|||||||
'amazon-adsystem-com_amazon-apstag.js': 'https://raw.githubusercontent.com/AdguardTeam/Scriptlets/master/dist/redirect-files/amazon-apstag.js'
|
'amazon-adsystem-com_amazon-apstag.js': 'https://raw.githubusercontent.com/AdguardTeam/Scriptlets/master/dist/redirect-files/amazon-apstag.js'
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const mockDir = path.resolve(__dirname, '../Mock');
|
|
||||||
|
|
||||||
export const downloadMockAssets = task(require.main === module, __filename)((span) => Promise.all(Object.entries(ASSETS_LIST).map(
|
export const downloadMockAssets = task(require.main === module, __filename)((span) => Promise.all(Object.entries(ASSETS_LIST).map(
|
||||||
([filename, url]) => span
|
([filename, url]) => span
|
||||||
.traceChildAsync(url, () => fetchWithRetry(url).then(res => {
|
.traceChildAsync(url, async () => {
|
||||||
const src = path.join(mockDir, filename);
|
const res = await fetchWithRetry(url);
|
||||||
|
|
||||||
|
const src = path.join(OUTPUT_MOCK_DIR, filename);
|
||||||
if (!res.body) {
|
if (!res.body) {
|
||||||
throw new Error(`Empty body from ${url}`);
|
throw new Error(`Empty body from ${url}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await fsp.mkdir(OUTPUT_MOCK_DIR, { recursive: true });
|
||||||
return pipeline(
|
return pipeline(
|
||||||
Readable.fromWeb(res.body),
|
Readable.fromWeb(res.body),
|
||||||
fs.createWriteStream(src, 'utf-8')
|
fs.createWriteStream(src, 'utf-8')
|
||||||
);
|
);
|
||||||
}))
|
})
|
||||||
)));
|
)));
|
||||||
|
|||||||
@ -1,57 +1,17 @@
|
|||||||
import { existsSync, createWriteStream } from 'node:fs';
|
import { createWriteStream } from 'node:fs';
|
||||||
import { mkdir } from 'node:fs/promises';
|
import { mkdir } from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { pipeline } from 'node:stream/promises';
|
import { pipeline } from 'node:stream/promises';
|
||||||
import { readFileByLine } from './lib/fetch-text-by-line';
|
|
||||||
import { isCI } from 'ci-info';
|
|
||||||
import { task } from './trace';
|
import { task } from './trace';
|
||||||
import { defaultRequestInit, fetchWithRetry } from './lib/fetch-retry';
|
import { defaultRequestInit, fetchWithRetry } from './lib/fetch-retry';
|
||||||
import tarStream from 'tar-stream';
|
import tarStream from 'tar-stream';
|
||||||
import zlib from 'node:zlib';
|
import zlib from 'node:zlib';
|
||||||
import { Readable } from 'node:stream';
|
import { Readable } from 'node:stream';
|
||||||
|
|
||||||
const IS_READING_BUILD_OUTPUT = 1 << 2;
|
|
||||||
const ALL_FILES_EXISTS = 1 << 3;
|
|
||||||
|
|
||||||
const GITHUB_CODELOAD_URL = 'https://codeload.github.com/sukkalab/ruleset.skk.moe/tar.gz/master';
|
const GITHUB_CODELOAD_URL = 'https://codeload.github.com/sukkalab/ruleset.skk.moe/tar.gz/master';
|
||||||
const GITLAB_CODELOAD_URL = 'https://gitlab.com/SukkaW/ruleset.skk.moe/-/archive/master/ruleset.skk.moe-master.tar.gz';
|
const GITLAB_CODELOAD_URL = 'https://gitlab.com/SukkaW/ruleset.skk.moe/-/archive/master/ruleset.skk.moe-master.tar.gz';
|
||||||
|
|
||||||
export const downloadPreviousBuild = task(require.main === module, __filename)(async (span) => {
|
export const downloadPreviousBuild = task(require.main === module, __filename)(async (span) => {
|
||||||
const buildOutputList: string[] = [];
|
|
||||||
|
|
||||||
let flag = 1 | ALL_FILES_EXISTS;
|
|
||||||
|
|
||||||
await span
|
|
||||||
.traceChild('read .gitignore')
|
|
||||||
.traceAsyncFn(async () => {
|
|
||||||
for await (const line of readFileByLine(path.resolve(__dirname, '../.gitignore'))) {
|
|
||||||
if (line === '# $ build output') {
|
|
||||||
flag = flag | IS_READING_BUILD_OUTPUT;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!(flag & IS_READING_BUILD_OUTPUT)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildOutputList.push(line);
|
|
||||||
|
|
||||||
if (!isCI && !existsSync(path.join(__dirname, '..', line))) {
|
|
||||||
flag = flag & ~ALL_FILES_EXISTS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isCI) {
|
|
||||||
flag = flag & ~ALL_FILES_EXISTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flag & ALL_FILES_EXISTS) {
|
|
||||||
console.log('All files exists, skip download.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const filesList = buildOutputList.map(f => path.join('ruleset.skk.moe-master', f));
|
|
||||||
|
|
||||||
const tarGzUrl = await span.traceChildAsync('get tar.gz url', async () => {
|
const tarGzUrl = await span.traceChildAsync('get tar.gz url', async () => {
|
||||||
const resp = await fetchWithRetry(GITHUB_CODELOAD_URL, {
|
const resp = await fetchWithRetry(GITHUB_CODELOAD_URL, {
|
||||||
...defaultRequestInit,
|
...defaultRequestInit,
|
||||||
@ -68,6 +28,8 @@ export const downloadPreviousBuild = task(require.main === module, __filename)(a
|
|||||||
return GITHUB_CODELOAD_URL;
|
return GITHUB_CODELOAD_URL;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const publicDir = path.resolve(__dirname, '..', 'public');
|
||||||
|
|
||||||
return span.traceChildAsync('download & extract previoud build', async () => {
|
return span.traceChildAsync('download & extract previoud build', async () => {
|
||||||
const resp = await fetchWithRetry(tarGzUrl, {
|
const resp = await fetchWithRetry(tarGzUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
@ -112,14 +74,9 @@ export const downloadPreviousBuild = task(require.main === module, __filename)(a
|
|||||||
entry.resume(); // Drain the entry
|
entry.resume(); // Drain the entry
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// filter entry
|
|
||||||
if (!filesList.some(f => entry.header.name.startsWith(f))) {
|
|
||||||
entry.resume(); // Drain the entry
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relativeEntryPath = entry.header.name.replace(pathPrefix, '');
|
const relativeEntryPath = entry.header.name.replace(pathPrefix, '');
|
||||||
const targetPath = path.join(__dirname, '..', relativeEntryPath);
|
const targetPath = path.join(publicDir, relativeEntryPath);
|
||||||
|
|
||||||
await mkdir(path.dirname(targetPath), { recursive: true });
|
await mkdir(path.dirname(targetPath), { recursive: true });
|
||||||
await pipeline(entry, createWriteStream(targetPath));
|
await pipeline(entry, createWriteStream(targetPath));
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import { processLine, processLineFromReadline } from './process-line';
|
|||||||
import { readFileByLine } from './fetch-text-by-line';
|
import { readFileByLine } from './fetch-text-by-line';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fsp from 'node:fs/promises';
|
import fsp from 'node:fs/promises';
|
||||||
|
import { SOURCE_DIR } from '../constants/dir';
|
||||||
|
|
||||||
const file = path.resolve(__dirname, '../../Source/domainset/cdn.conf');
|
const file = path.join(SOURCE_DIR, 'domainset/cdn.conf');
|
||||||
|
|
||||||
group('read file by line', () => {
|
group('read file by line', () => {
|
||||||
bench('readFileByLine', () => processLineFromReadline(readFileByLine(file)));
|
bench('readFileByLine', () => processLineFromReadline(readFileByLine(file)));
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import path, { dirname } from 'node:path';
|
import path, { dirname } from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import fsp from 'node:fs/promises';
|
import fsp from 'node:fs/promises';
|
||||||
|
import { OUTPUT_CLASH_DIR, OUTPUT_SINGBOX_DIR, OUTPUT_SURGE_DIR } from '../constants/dir';
|
||||||
|
|
||||||
export const isTruthy = <T>(i: T | 0 | '' | false | null | undefined): i is T => !!i;
|
export const isTruthy = <T>(i: T | 0 | '' | false | null | undefined): i is T => !!i;
|
||||||
|
|
||||||
@ -52,10 +53,6 @@ export const domainWildCardToRegex = (domain: string) => {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OUTPUT_SURGE_DIR = path.resolve(__dirname, '../../List');
|
|
||||||
const OUTPUT_CLASH_DIR = path.resolve(__dirname, '../../Clash');
|
|
||||||
const OUTPUT_SINGBOX_DIR = path.resolve(__dirname, '../../sing-box');
|
|
||||||
|
|
||||||
export const output = (id: string, type: 'non_ip' | 'ip' | 'domainset') => {
|
export const output = (id: string, type: 'non_ip' | 'ip' | 'domainset') => {
|
||||||
return [
|
return [
|
||||||
path.join(OUTPUT_SURGE_DIR, type, id + '.conf'),
|
path.join(OUTPUT_SURGE_DIR, type, id + '.conf'),
|
||||||
|
|||||||
@ -25,6 +25,11 @@ export const treeDir = async (rootPath: string): Promise<TreeTypeArray> => {
|
|||||||
const walk = async (dir: string, node: TreeTypeArray, dirRelativeToRoot = ''): Promise<VoidOrVoidArray> => {
|
const walk = async (dir: string, node: TreeTypeArray, dirRelativeToRoot = ''): Promise<VoidOrVoidArray> => {
|
||||||
const promises: Array<Promise<VoidOrVoidArray>> = [];
|
const promises: Array<Promise<VoidOrVoidArray>> = [];
|
||||||
for await (const child of await fsp.opendir(dir)) {
|
for await (const child of await fsp.opendir(dir)) {
|
||||||
|
// Ignore hidden files
|
||||||
|
if (child.name[0] === '.' || child.name === 'CNAME') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const childFullPath = child.parentPath + sep + child.name;
|
const childFullPath = child.parentPath + sep + child.name;
|
||||||
const childRelativeToRoot = dirRelativeToRoot + sep + child.name;
|
const childRelativeToRoot = dirRelativeToRoot + sep + child.name;
|
||||||
|
|
||||||
|
|||||||
@ -2,8 +2,7 @@ import path from 'node:path';
|
|||||||
import fsp from 'node:fs/promises';
|
import fsp from 'node:fs/promises';
|
||||||
import { fdir as Fdir } from 'fdir';
|
import { fdir as Fdir } from 'fdir';
|
||||||
import { readFileByLine } from './lib/fetch-text-by-line';
|
import { readFileByLine } from './lib/fetch-text-by-line';
|
||||||
|
import { SOURCE_DIR } from './constants/dir';
|
||||||
const sourceDir = path.resolve(__dirname, '../Source');
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const promises: Array<Promise<unknown>> = [];
|
const promises: Array<Promise<unknown>> = [];
|
||||||
@ -27,7 +26,7 @@ const sourceDir = path.resolve(__dirname, '../Source');
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.crawl(sourceDir)
|
.crawl(SOURCE_DIR)
|
||||||
.withPromise();
|
.withPromise();
|
||||||
|
|
||||||
for (let i = 0, len = paths.length; i < len; i++) {
|
for (let i = 0, len = paths.length; i < len; i++) {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { createTrie } from './lib/trie';
|
|||||||
import { parse } from 'csv-parse/sync';
|
import { parse } from 'csv-parse/sync';
|
||||||
import { readFileByLine } from './lib/fetch-text-by-line';
|
import { readFileByLine } from './lib/fetch-text-by-line';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { SOURCE_DIR } from './constants/dir';
|
||||||
|
|
||||||
export const parseGfwList = async () => {
|
export const parseGfwList = async () => {
|
||||||
const whiteSet = new Set<string>();
|
const whiteSet = new Set<string>();
|
||||||
@ -105,8 +106,8 @@ export const parseGfwList = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
runAgainstRuleset(path.resolve(__dirname, '../Source/non_ip/global.conf')),
|
runAgainstRuleset(path.join(SOURCE_DIR, 'non_ip/global.conf')),
|
||||||
runAgainstRuleset(path.resolve(__dirname, '../Source/non_ip/telegram.conf')),
|
runAgainstRuleset(path.join(SOURCE_DIR, 'non_ip/telegram.conf')),
|
||||||
runAgainstRuleset(path.resolve(__dirname, '../List/non_ip/stream.conf'))
|
runAgainstRuleset(path.resolve(__dirname, '../List/non_ip/stream.conf'))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user