Compare commits
46 Commits
a67b0879e7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e5691f7bb | |||
| c30ad90e34 | |||
| 100044e79c | |||
| bd4af66d63 | |||
| f62b3ff776 | |||
| 373685ca59 | |||
| 3933195746 | |||
| 50bce03cf8 | |||
| 49caf5dc65 | |||
| 8826675dd8 | |||
| 9dc7c8dda9 | |||
| 7836eceda2 | |||
| ef4cf861ff | |||
| 34c3e242e5 | |||
| 41127d10d1 | |||
| ae1b71f741 | |||
| eeb60ab5ad | |||
| 67704659e6 | |||
| 4f73cd0071 | |||
| df7d2d555f | |||
| 3dafd087cd | |||
| 2fce3a192a | |||
| c140b08e12 | |||
| 0847e23918 | |||
| 4b607f8d6f | |||
| e0b79a217f | |||
| ba21e17168 | |||
| 26867d4175 | |||
| e8477c3322 | |||
| 5cef2d7bca | |||
| 6b76e578b5 | |||
| e2f855bb39 | |||
| 6525bbab11 | |||
| 0ae3f1100c | |||
| faaf393325 | |||
| 76d11a6621 | |||
| 38ba2fd576 | |||
| b81b395282 | |||
| 143f41dcde | |||
| 8f535becc2 | |||
| 82e97b2f57 | |||
| 75439cf6df | |||
| b7f2adba40 | |||
| 5c8851a292 | |||
| 46e8ef128f | |||
| 3933f84a75 |
@@ -40,6 +40,7 @@ lgbm-auto-update: true
|
||||
lgbm-update-interval: 72 # model auto update interval, the default is 72 (hours)
|
||||
lgbm-url: "https://github.com/vernesong/mihomo/releases/download/LightGBM-Model/Model.bin" # model update url
|
||||
|
||||
|
||||
# ———————————————————————————————————————————————————————— HOSTS ————————————————————————————————————————————————————————
|
||||
hosts:
|
||||
'gitea.shamanlanding.org': '79.120.77.123'
|
||||
|
||||
233
config-sub-converter/scripts/assemble-mihomo-config-dev.js
Normal file
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* SUB STORE YAML ASSEMBLER (v4: Fix Duplicate Headers)
|
||||
* * Arguments:
|
||||
* - clear-comments=true
|
||||
* - clear-manifest=true
|
||||
* - clear-replacements=true
|
||||
* * Requires: Header "# @file: filename.yaml" in input files.
|
||||
*/
|
||||
|
||||
// --- OPTIONS PARSING ---
|
||||
function normalizeOptions() {
|
||||
const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {};
|
||||
const asBool = (v, def = false) => {
|
||||
if (v === undefined || v === null || v === "") return def;
|
||||
if (typeof v === "boolean") return v;
|
||||
const s = String(v).toLowerCase().trim();
|
||||
if (["1", "true", "yes", "y", "on"].includes(s)) return true;
|
||||
if (["0", "false", "no", "n", "off"].includes(s)) return false;
|
||||
return def;
|
||||
};
|
||||
return {
|
||||
clearComments: asBool(args['clear-comments'], false),
|
||||
clearManifest: asBool(args['clear-manifest'], false),
|
||||
clearReplacements: asBool(args['clear-replacements'], false),
|
||||
};
|
||||
}
|
||||
|
||||
// --- UTILS ---
|
||||
function normalizeFiles(rawFiles) {
|
||||
const map = new Map();
|
||||
if (!rawFiles || !Array.isArray(rawFiles)) return map;
|
||||
rawFiles.forEach((content) => {
|
||||
if (typeof content !== 'string') return;
|
||||
const match = content.match(/^#\s*@file:\s*(.+?)(\s|$)/m);
|
||||
if (match) map.set(match[1].trim(), content);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
function cleanText(text) {
|
||||
return String(text || "").replace(/\r\n/g, "\n");
|
||||
}
|
||||
|
||||
function extractName(block) {
|
||||
const match = block.match(/^ {4}name:\s*(?:["']?)(.*?)(?:["']?)\s*(?:#.*)?$/m);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
function extractKey(block, indentLevel) {
|
||||
const spaceStr = " ".repeat(indentLevel);
|
||||
const re = new RegExp(`^${spaceStr}([^#\\s][^:]+):`);
|
||||
const match = block.match(re);
|
||||
return match ? match[1].trim().replace(/['"]/g, "") : null;
|
||||
}
|
||||
|
||||
function stripFullLineComments(text) {
|
||||
return text.split('\n')
|
||||
.filter(line => !line.trim().startsWith('#'))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function splitBlocks(text, type) {
|
||||
const lines = cleanText(text).split("\n");
|
||||
const blocks = [];
|
||||
let currentBuf = [];
|
||||
|
||||
const isListStart = (l) => l.match(/^ {2}-\s/);
|
||||
const isMapStart2 = (l) => l.match(/^ {2}[^ \-#][^:]*:/);
|
||||
const isMapStart0 = (l) => l.match(/^[^ \-#][^:]*:/);
|
||||
|
||||
const isStart = (l) => {
|
||||
if (type === 'list') return isListStart(l);
|
||||
if (type === 'map2') return isMapStart2(l);
|
||||
if (type === 'map0') return isMapStart0(l);
|
||||
return false;
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.match(/^#\s*@file:/)) continue;
|
||||
|
||||
if (isStart(line)) {
|
||||
if (currentBuf.length > 0) {
|
||||
blocks.push(currentBuf.join("\n"));
|
||||
currentBuf = [];
|
||||
}
|
||||
}
|
||||
if (currentBuf.length === 0 && !line.trim()) continue;
|
||||
currentBuf.push(line);
|
||||
}
|
||||
if (currentBuf.length > 0) blocks.push(currentBuf.join("\n"));
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// --- MERGE LOGIC ---
|
||||
function processSection(sectionName, manifestEntries, fileMap, opts) {
|
||||
let sectionOutput = [];
|
||||
const seenKeys = new Set();
|
||||
|
||||
// Добавляем заголовок секции
|
||||
if (!['root', 'x-substore', 'rules'].includes(sectionName)) {
|
||||
sectionOutput.push(`${sectionName}:`);
|
||||
}
|
||||
if (sectionName === 'x-substore') {
|
||||
sectionOutput.push(`x-substore:`);
|
||||
}
|
||||
|
||||
for (const entry of manifestEntries) {
|
||||
let content = fileMap.get(entry.file);
|
||||
if (!content) throw new Error(`CRITICAL: File "${entry.file}" not found.`);
|
||||
|
||||
if (!opts.clearComments) {
|
||||
sectionOutput.push(`\n# --- source: ${entry.file} | mode: ${entry.mode || "concat"} ---`);
|
||||
}
|
||||
|
||||
if (opts.clearComments) {
|
||||
content = stripFullLineComments(content);
|
||||
}
|
||||
|
||||
// [FIX] Удаляем дублирующий заголовок x-substore из контента файла,
|
||||
// но оставляем содержимое (оно уже имеет правильный отступ 2 пробела)
|
||||
if (sectionName === 'x-substore') {
|
||||
content = content.replace(/^x-substore:\s*(?:#.*)?$/m, '');
|
||||
}
|
||||
|
||||
const mode = entry.mode || "concat";
|
||||
|
||||
if (mode === 'concat') {
|
||||
const lines = cleanText(content).split('\n').filter(l => !l.match(/^#\s*@file:/));
|
||||
sectionOutput.push(lines.join('\n'));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode === 'first_wins') {
|
||||
let blockType = 'map2';
|
||||
if (sectionName === 'root') blockType = 'map0';
|
||||
if (['proxies', 'proxy-groups'].includes(sectionName)) blockType = 'list';
|
||||
|
||||
const blocks = splitBlocks(content, blockType);
|
||||
|
||||
if (blocks.length === 0 && !opts.clearComments) {
|
||||
sectionOutput.push(`# WARNING: No valid blocks found in ${entry.file}`);
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
let id = null;
|
||||
if (blockType === 'list') {
|
||||
id = extractName(block);
|
||||
} else if (blockType === 'map0') {
|
||||
id = extractKey(block, 0);
|
||||
} else {
|
||||
id = extractKey(block, 2);
|
||||
}
|
||||
|
||||
// Apply cleaning options for x-substore content
|
||||
if (sectionName === 'x-substore' && id) {
|
||||
if (opts.clearManifest && id === 'manifest') continue;
|
||||
if (opts.clearReplacements && id === 'replacements') continue;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
if (seenKeys.has(id)) {
|
||||
if (!opts.clearComments) sectionOutput.push(`# [SKIP] Duplicate "${id}" ignored`);
|
||||
continue;
|
||||
}
|
||||
seenKeys.add(id);
|
||||
}
|
||||
sectionOutput.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sectionOutput.join("\n");
|
||||
}
|
||||
|
||||
// --- MAIN EXECUTION ---
|
||||
try {
|
||||
const opts = normalizeOptions();
|
||||
const fileMap = normalizeFiles($files);
|
||||
|
||||
let manifestKey = null;
|
||||
for (const k of fileMap.keys()) {
|
||||
if (k.startsWith("00-manifest")) {
|
||||
manifestKey = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!manifestKey) throw new Error("Manifest file (00-manifest-...) not found.");
|
||||
|
||||
const manifestRaw = fileMap.get(manifestKey);
|
||||
const manifestObj = ProxyUtils.yaml.safeLoad(manifestRaw);
|
||||
|
||||
if (!manifestObj?.['x-substore']?.manifest) {
|
||||
throw new Error("Invalid Manifest structure.");
|
||||
}
|
||||
|
||||
const manifestList = manifestObj['x-substore'].manifest;
|
||||
const replacements = manifestObj['x-substore'].replacements || [];
|
||||
|
||||
const sectionOrder = [
|
||||
"x-substore", "root", "hosts", "sniffer", "tun", "dns",
|
||||
"proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules"
|
||||
];
|
||||
|
||||
const plan = {};
|
||||
sectionOrder.forEach(s => plan[s] = []);
|
||||
manifestList.forEach(entry => {
|
||||
if (!plan[entry.section]) plan[entry.section] = [];
|
||||
plan[entry.section].push(entry);
|
||||
});
|
||||
|
||||
const finalChunks = [];
|
||||
for (const sec of sectionOrder) {
|
||||
if (!plan[sec] || plan[sec].length === 0) continue;
|
||||
const secStr = processSection(sec, plan[sec], fileMap, opts);
|
||||
finalChunks.push(secStr);
|
||||
}
|
||||
|
||||
let result = finalChunks.join("\n\n");
|
||||
|
||||
if (Array.isArray(replacements)) {
|
||||
replacements.forEach(rep => {
|
||||
if (rep.from && rep.to) {
|
||||
result = result.split(rep.from).join(rep.to);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$content = result;
|
||||
|
||||
} catch (err) {
|
||||
$content = `# CRITICAL ERROR:\n# ${err.message}\n# Stack: ${err.stack}`;
|
||||
}
|
||||
239
config-sub-converter/scripts/assemble-mihomo-config.js
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* SUB STORE YAML ASSEMBLER (Content-Tag Aware)
|
||||
* * Требование: Каждый файл должен начинаться с комментария:
|
||||
* # @file: filename.yaml
|
||||
*/
|
||||
|
||||
// --- UTILS ---
|
||||
|
||||
// Функция нормализации теперь парсит содержимое, чтобы найти имя файла
|
||||
function normalizeFiles(rawFiles) {
|
||||
const map = new Map();
|
||||
if (!rawFiles || !Array.isArray(rawFiles)) return map;
|
||||
|
||||
rawFiles.forEach((content, index) => {
|
||||
if (typeof content !== 'string') return;
|
||||
|
||||
// Ищем магический тег: # @file: filename.yaml
|
||||
const match = content.match(/^#\s*@file:\s*(.+?)(\s|$)/m);
|
||||
|
||||
if (match) {
|
||||
const filename = match[1].trim();
|
||||
map.set(filename, content);
|
||||
} else {
|
||||
// Если тега нет, файл остается "анонимным" и недоступным через манифест,
|
||||
// но мы можем логировать это.
|
||||
// console.log(`File at index ${index} has no @file tag`);
|
||||
}
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function cleanText(text) {
|
||||
return String(text || "").replace(/\r\n/g, "\n");
|
||||
}
|
||||
|
||||
// Извлечение name из элемента списка
|
||||
function extractName(block) {
|
||||
// Ищем name: value с учетом отступов (4 пробела)
|
||||
const match = block.match(/^ {4}name:\s*(?:["']?)(.*?)(?:["']?)\s*(?:#.*)?$/m);
|
||||
return match ? match[1].trim() : null;
|
||||
}
|
||||
|
||||
// Извлечение ключа из map (0 или 2 пробела)
|
||||
function extractKey(block, indentLevel) {
|
||||
const spaceStr = " ".repeat(indentLevel);
|
||||
const re = new RegExp(`^${spaceStr}([^#\\s][^:]+):`);
|
||||
const match = block.match(re);
|
||||
return match ? match[1].trim().replace(/['"]/g, "") : null;
|
||||
}
|
||||
|
||||
// Разбивка текста на логические блоки
|
||||
function splitBlocks(text, type) {
|
||||
const lines = cleanText(text).split("\n");
|
||||
const blocks = [];
|
||||
let currentBuf = [];
|
||||
|
||||
// Детекторы начала блока
|
||||
const isListStart = (l) => l.match(/^ {2}-\s/); // " - "
|
||||
const isMapStart2 = (l) => l.match(/^ {2}[^ \-#][^:]*:/); // " key:"
|
||||
const isMapStart0 = (l) => l.match(/^[^ \-#][^:]*:/); // "key:" (root)
|
||||
|
||||
const isStart = (l) => {
|
||||
if (type === 'list') return isListStart(l);
|
||||
if (type === 'map2') return isMapStart2(l);
|
||||
if (type === 'map0') return isMapStart0(l);
|
||||
return false;
|
||||
};
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Если строка начинается с # @file:, пропускаем ее, чтобы не мусорить в конфиге
|
||||
if (line.match(/^#\s*@file:/)) continue;
|
||||
|
||||
if (isStart(line)) {
|
||||
if (currentBuf.length > 0) {
|
||||
blocks.push(currentBuf.join("\n"));
|
||||
currentBuf = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Пропуск пустых строк в начале, если буфер пуст
|
||||
if (currentBuf.length === 0 && !line.trim()) continue;
|
||||
|
||||
currentBuf.push(line);
|
||||
}
|
||||
|
||||
if (currentBuf.length > 0) {
|
||||
blocks.push(currentBuf.join("\n"));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// --- MERGE LOGIC ---
|
||||
|
||||
function processSection(sectionName, manifestEntries, fileMap) {
|
||||
let sectionOutput = [];
|
||||
const seenKeys = new Set();
|
||||
|
||||
// Добавляем заголовок секции (кроме root, x-substore и rules)
|
||||
if (!['root', 'x-substore', 'rules'].includes(sectionName)) {
|
||||
sectionOutput.push(`${sectionName}:`);
|
||||
}
|
||||
if (sectionName === 'x-substore') {
|
||||
sectionOutput.push(`x-substore:`);
|
||||
}
|
||||
|
||||
for (const entry of manifestEntries) {
|
||||
const content = fileMap.get(entry.file);
|
||||
|
||||
if (!content) {
|
||||
// FAIL-FAST: Если файл из манифеста не найден (нет тега или не загружен)
|
||||
throw new Error(`CRITICAL: File "${entry.file}" not found inside input bundle. Did you add '# @file: ${entry.file}' header?`);
|
||||
}
|
||||
|
||||
const mode = entry.mode || "concat";
|
||||
sectionOutput.push(`\n# --- source: ${entry.file} | mode: ${mode} ---`);
|
||||
|
||||
if (mode === 'concat') {
|
||||
// Просто чистим от тега @file при вставке
|
||||
const lines = cleanText(content).split('\n').filter(l => !l.match(/^#\s*@file:/));
|
||||
sectionOutput.push(lines.join('\n'));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mode === 'first_wins') {
|
||||
let blockType = 'map2';
|
||||
if (sectionName === 'root') blockType = 'map0';
|
||||
if (['proxies', 'proxy-groups'].includes(sectionName)) blockType = 'list';
|
||||
|
||||
const blocks = splitBlocks(content, blockType);
|
||||
|
||||
if (blocks.length === 0) {
|
||||
sectionOutput.push(`# WARNING: No valid blocks found in ${entry.file}`);
|
||||
}
|
||||
|
||||
for (const block of blocks) {
|
||||
let id = null;
|
||||
|
||||
if (blockType === 'list') {
|
||||
id = extractName(block);
|
||||
} else if (blockType === 'map0') {
|
||||
id = extractKey(block, 0);
|
||||
} else {
|
||||
id = extractKey(block, 2);
|
||||
}
|
||||
|
||||
if (id) {
|
||||
if (seenKeys.has(id)) {
|
||||
sectionOutput.push(`# [SKIP] Duplicate "${id}" ignored`);
|
||||
continue;
|
||||
}
|
||||
seenKeys.add(id);
|
||||
}
|
||||
|
||||
sectionOutput.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sectionOutput.join("\n");
|
||||
}
|
||||
|
||||
// --- MAIN EXECUTION ---
|
||||
|
||||
try {
|
||||
// 1. Создаем карту файлов на основе тегов # @file:
|
||||
const fileMap = normalizeFiles($files);
|
||||
|
||||
// 2. Ищем Манифест
|
||||
let manifestKey = null;
|
||||
// Ищем файл, чье имя (из тега) начинается с 00-manifest
|
||||
for (const k of fileMap.keys()) {
|
||||
if (k.startsWith("00-manifest")) {
|
||||
manifestKey = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!manifestKey) {
|
||||
const foundFiles = Array.from(fileMap.keys()).join(", ");
|
||||
throw new Error(`Manifest file (00-manifest-...) not found in headers. Found files: [${foundFiles}]`);
|
||||
}
|
||||
|
||||
// 3. Парсим Манифест
|
||||
const manifestRaw = fileMap.get(manifestKey);
|
||||
const manifestObj = ProxyUtils.yaml.safeLoad(manifestRaw);
|
||||
|
||||
if (!manifestObj?.['x-substore']?.manifest) {
|
||||
throw new Error("Invalid Manifest: missing x-substore.manifest structure.");
|
||||
}
|
||||
|
||||
const manifestList = manifestObj['x-substore'].manifest;
|
||||
const replacements = manifestObj['x-substore'].replacements || [];
|
||||
|
||||
// 4. Планирование порядка секций
|
||||
const sectionOrder = [
|
||||
"x-substore", "root", "hosts", "sniffer", "tun", "dns",
|
||||
"proxies", "proxy-providers", "proxy-groups", "rule-providers", "rules"
|
||||
];
|
||||
|
||||
const plan = {};
|
||||
sectionOrder.forEach(s => plan[s] = []);
|
||||
|
||||
manifestList.forEach(entry => {
|
||||
// Если секция в манифесте есть, а в нашем плане нет - добавляем динамически (на всякий случай)
|
||||
if (!plan[entry.section]) plan[entry.section] = [];
|
||||
plan[entry.section].push(entry);
|
||||
});
|
||||
|
||||
// 5. Сборка
|
||||
const finalChunks = [];
|
||||
|
||||
for (const sec of sectionOrder) {
|
||||
if (!plan[sec] || plan[sec].length === 0) continue;
|
||||
|
||||
// Обработка секции
|
||||
const secStr = processSection(sec, plan[sec], fileMap);
|
||||
finalChunks.push(secStr);
|
||||
}
|
||||
|
||||
let result = finalChunks.join("\n\n");
|
||||
|
||||
// 6. Replacements (Literal)
|
||||
if (Array.isArray(replacements)) {
|
||||
replacements.forEach(rep => {
|
||||
if (rep.from && rep.to) {
|
||||
result = result.split(rep.from).join(rep.to);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$content = result;
|
||||
|
||||
} catch (err) {
|
||||
$content = `# CRITICAL ERROR in Assembler:\n# ${err.message}\n\n# Stack:\n${err.stack}`;
|
||||
}
|
||||
235
config-sub-converter/scripts/convert-awg-to-clash.js
Normal file
@@ -0,0 +1,235 @@
|
||||
/**********************
|
||||
* Defaults (AmneziaWG)
|
||||
* Если в исходнике нет параметра, берём отсюда.
|
||||
* Если итоговое значение == 0, параметр пропускаем в amnezia-wg-option.
|
||||
**********************/
|
||||
const AMZ_DEFAULTS = {
|
||||
Jc: 4,
|
||||
Jmin: 10,
|
||||
Jmax: 50,
|
||||
S1: 110,
|
||||
S2: 120,
|
||||
H1: 0,
|
||||
H2: 0,
|
||||
H3: 0,
|
||||
H4: 0,
|
||||
};
|
||||
|
||||
/**********************
|
||||
* Options from Sub Store
|
||||
* Example URL:
|
||||
* .../convert-awg-to-clash.js#dns=false&ipv6=false#noCache
|
||||
*
|
||||
* Требования:
|
||||
* - dns=false => remote-dns-resolve: false (вне зависимости от входа)
|
||||
* - ipv6=false => удалить IPv6 из allowed-ips (и вообще не добавлять ipv6-части)
|
||||
**********************/
|
||||
function normalizeOptions() {
|
||||
const args = (typeof $arguments !== "undefined" && $arguments) ? $arguments : {};
|
||||
|
||||
const asBool = (v, def = true) => {
|
||||
if (v === undefined || v === null || v === "") return def;
|
||||
if (typeof v === "boolean") return v;
|
||||
const s = String(v).toLowerCase().trim();
|
||||
if (["1", "true", "yes", "y", "on"].includes(s)) return true;
|
||||
if (["0", "false", "no", "n", "off"].includes(s)) return false;
|
||||
return def;
|
||||
};
|
||||
|
||||
return {
|
||||
dns: asBool(args.dns, true),
|
||||
ipv6: asBool(args.ipv6, true),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**********************
|
||||
* Parsing WG INI blocks
|
||||
**********************/
|
||||
function cleanLines(text) {
|
||||
return String(text ?? "")
|
||||
.replace(/\r\n/g, "\n")
|
||||
.split("\n");
|
||||
}
|
||||
|
||||
// Парсим один INI-фрагмент с [Interface] и [Peer] (один peer)
|
||||
function parseIniOne(text) {
|
||||
const lines = cleanLines(text)
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0 && !l.startsWith("#") && !l.startsWith(";"));
|
||||
|
||||
let section = null;
|
||||
const data = { Interface: {}, Peer: {} };
|
||||
|
||||
for (const line of lines) {
|
||||
const mSec = line.match(/^\[(.+?)\]$/);
|
||||
if (mSec) {
|
||||
section = mSec[1];
|
||||
continue;
|
||||
}
|
||||
|
||||
const mKV = line.match(/^([^=]+?)\s*=\s*(.+)$/);
|
||||
if (!mKV || !section) continue;
|
||||
|
||||
const key = mKV[1].trim();
|
||||
const value = mKV[2].trim();
|
||||
|
||||
if (section === "Interface") data.Interface[key] = value;
|
||||
else if (section === "Peer") data.Peer[key] = value;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Разбиваем весь файл на блоки по заголовкам "##### ..."
|
||||
function splitByHeaders(fullText) {
|
||||
const lines = cleanLines(fullText);
|
||||
|
||||
const blocks = [];
|
||||
let current = { name: "amz-wg", buf: [] };
|
||||
|
||||
const headerRe = /^#{5}\s*(.+)\s*$/;
|
||||
|
||||
for (const line of lines) {
|
||||
const mh = line.match(headerRe);
|
||||
if (mh) {
|
||||
// закрываем предыдущий блок, если там что-то есть
|
||||
if (current.buf.join("\n").trim().length > 0) blocks.push(current);
|
||||
current = { name: mh[1].trim(), buf: [] };
|
||||
continue;
|
||||
}
|
||||
current.buf.push(line);
|
||||
}
|
||||
|
||||
if (current.buf.join("\n").trim().length > 0) blocks.push(current);
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function splitList(val) {
|
||||
return String(val || "")
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function parseEndpoint(endpoint) {
|
||||
// Поддержка:
|
||||
// - host:port
|
||||
// - [ipv6]:port
|
||||
const s = String(endpoint || "").trim();
|
||||
const v6 = s.match(/^\[(.+?)\]:(\d+)$/);
|
||||
if (v6) return { host: v6[1], port: Number(v6[2]) };
|
||||
|
||||
const v4 = s.match(/^(.+?):(\d+)$/);
|
||||
if (v4) return { host: v4[1], port: Number(v4[2]) };
|
||||
|
||||
return { host: "", port: 0 };
|
||||
}
|
||||
|
||||
function toNumberOrNull(v) {
|
||||
const s = String(v ?? "").trim();
|
||||
if (s === "") return null;
|
||||
if (/^-?\d+$/.test(s)) return Number(s);
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildAmzOptions(interfaceObj) {
|
||||
// Правило:
|
||||
// - если в файле есть параметр => используем его
|
||||
// - иначе берём из AMZ_DEFAULTS
|
||||
// - если итог == 0 => пропускаем
|
||||
const out = {};
|
||||
const keys = Object.keys(AMZ_DEFAULTS);
|
||||
|
||||
for (const K of keys) {
|
||||
const fromFile = interfaceObj[K];
|
||||
const fileNum = toNumberOrNull(fromFile);
|
||||
const fallback = AMZ_DEFAULTS[K];
|
||||
|
||||
const finalVal =
|
||||
fileNum !== null ? fileNum : (fallback ?? 0);
|
||||
|
||||
if (Number(finalVal) !== 0) {
|
||||
out[K.toLowerCase()] = Number(finalVal);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function filterAllowedIPs(allowed, enableIPv6) {
|
||||
if (enableIPv6) return allowed;
|
||||
// выкидываем всё, что похоже на IPv6
|
||||
return allowed.filter((cidr) => !cidr.includes(":"));
|
||||
}
|
||||
|
||||
function buildProxy(blockName, wg, options) {
|
||||
const i = wg.Interface || {};
|
||||
const p = wg.Peer || {};
|
||||
|
||||
const address = i.Address || "";
|
||||
const dnsList = splitList(i.DNS);
|
||||
|
||||
const ep = parseEndpoint(p.Endpoint);
|
||||
let allowed = splitList(p.AllowedIPs);
|
||||
allowed = filterAllowedIPs(allowed, options.ipv6);
|
||||
|
||||
const proxy = {
|
||||
name: blockName || "amz-wg",
|
||||
type: "wireguard",
|
||||
ip: address,
|
||||
// ipv6 поле в твоём примере закомментировано, так что не добавляем вообще
|
||||
"private-key": i.PrivateKey || "",
|
||||
peers: [
|
||||
{
|
||||
server: ep.host,
|
||||
port: ep.port,
|
||||
"public-key": p.PublicKey || "",
|
||||
...(p.PresharedKey ? { "pre-shared-key": p.PresharedKey } : {}),
|
||||
"allowed-ips": allowed,
|
||||
},
|
||||
],
|
||||
udp: true,
|
||||
// dns=false => принудительно false
|
||||
"remote-dns-resolve": options.dns ? true : false,
|
||||
...(dnsList.length ? { dns: dnsList } : {}),
|
||||
};
|
||||
|
||||
const amz = buildAmzOptions(i);
|
||||
if (Object.keys(amz).length) {
|
||||
proxy["amnezia-wg-option"] = amz;
|
||||
}
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
/**********************
|
||||
* ENTRYPOINT
|
||||
**********************/
|
||||
const opts = normalizeOptions();
|
||||
|
||||
// Вход: чаще всего $content, но на всякий пожарный берём $files[0]
|
||||
const input = String($content ?? ($files && $files[0]) ?? "");
|
||||
|
||||
// Разбиваем по заголовкам ##### ...
|
||||
const blocks = splitByHeaders(input);
|
||||
|
||||
// Для каждого блока парсим INI и строим proxy
|
||||
const proxies = [];
|
||||
for (const b of blocks) {
|
||||
const iniText = b.buf.join("\n").trim();
|
||||
if (!iniText) continue;
|
||||
|
||||
const wg = parseIniOne(iniText);
|
||||
|
||||
// минимальная валидация: нужны ключи
|
||||
if (!wg.Interface?.PrivateKey || !wg.Peer?.PublicKey || !wg.Peer?.Endpoint) {
|
||||
// пропускаем мусорные блоки, чтобы не ронять весь конвертер
|
||||
continue;
|
||||
}
|
||||
|
||||
proxies.push(buildProxy(b.name, wg, opts));
|
||||
}
|
||||
|
||||
// Финальный YAML
|
||||
$content = ProxyUtils.yaml.safeDump({ proxies });
|
||||
328
config-sub-converter/scripts/demo/gemeni-tried-orginal.js
Normal file
@@ -0,0 +1,328 @@
|
||||
/**
|
||||
* Sub-Store operator: Normalize + tag + country detect + per-country numbering
|
||||
*
|
||||
* Output format (default):
|
||||
* 🇺🇸 USA-01 🔒 📺 ❻ ▫️ws/vless/443
|
||||
*
|
||||
* Notes:
|
||||
* - Numbering is computed per-country AFTER grouping the full list.
|
||||
* - Tags (icons) do NOT affect numbering order.
|
||||
* - GeoIP is optional and only used when server is an IP and utils.geoip.lookup exists.
|
||||
*/
|
||||
|
||||
///////////////////////
|
||||
// CONFIG (EDIT ME)
|
||||
///////////////////////
|
||||
|
||||
// 1) Remove these patterns (marketing noise, brackets, separators, etc.)
|
||||
const NOISE_PATTERNS = [
|
||||
/\[[^\]]*]/g, // [ ... ]
|
||||
/\([^)]*\)/g, // ( ... )
|
||||
/\{[^}]*}/g, // { ... }
|
||||
/\btraffic\b/gi,
|
||||
/\bfree\b/gi,
|
||||
/\bwebsite\b/gi,
|
||||
/\bexpire\b/gi,
|
||||
/\blow\s*ping\b/gi,
|
||||
/\bai\s*studio\b/gi,
|
||||
/\bno\s*p2p\b/gi,
|
||||
/\b10\s*gbit\b/gi,
|
||||
/\bvless\b/gi, // you said you don't want it in the visible name
|
||||
/\bvmess\b/gi,
|
||||
/\bssr?\b/gi,
|
||||
/\btrojan\b/gi,
|
||||
/\bhysteria2?\b/gi,
|
||||
/\btuic\b/gi,
|
||||
/[|]/g,
|
||||
/[_]+/g,
|
||||
/[-]{2,}/g
|
||||
];
|
||||
|
||||
// 2) Keyword -> icon tags (if found in original name, icon is added; the keyword is removed from base name)
|
||||
const ICON_RULES = [
|
||||
{ regex: /\bYT\b/gi, icon: "📺" },
|
||||
{ regex: /\bIPv6\b/gi, icon: "❻" },
|
||||
{ regex: /\bNetflix\b|\bNF\b/gi, icon: "🎬" },
|
||||
{ regex: /\bDisney\+?\b|\bDSNY\b/gi, icon: "🏰" },
|
||||
{ regex: /\bHBO\b/gi, icon: "📼" },
|
||||
{ regex: /\bPrime\b|\bAmazon\b/gi, icon: "📦" },
|
||||
{ regex: /\bChatGPT\b|\bOpenAI\b/gi, icon: "🤖" },
|
||||
{ regex: /\bSteam\b/gi, icon: "🎮" },
|
||||
];
|
||||
|
||||
// 3) Optional “network” tag rules based on NAME text (not $server.network)
|
||||
// (Useful if providers shove "BGP/IPLC" into the node name)
|
||||
const NAME_NETWORK_TAGS = [
|
||||
{ regex: /\bIPLC\b/gi, tag: "🛰️" },
|
||||
{ regex: /\bBGP\b/gi, tag: "🧭" },
|
||||
{ regex: /\bAnycast\b/gi, tag: "🌍" }
|
||||
];
|
||||
|
||||
// 4) Country detection rules by NAME (regex). First match wins (priority = lower is earlier)
|
||||
const COUNTRY_RULES = [
|
||||
// USA
|
||||
{ regex: /\b(USA|US|UNITED\s*STATES|AMERICA|NEW\s*YORK|NYC|LOS\s*ANGELES|LA|DALLAS|CHI(CAGO)?)\b/i, iso3: "USA", flag: "🇺🇸", priority: 10 },
|
||||
|
||||
// Germany
|
||||
{ regex: /\b(DE|DEU|GER(MANY)?|FRANKFURT|BERLIN|MUNICH|MÜNCHEN)\b/i, iso3: "DEU", flag: "🇩🇪", priority: 20 },
|
||||
|
||||
// Netherlands
|
||||
{ regex: /\b(NL|NLD|NETHERLANDS|HOLLAND|AMSTERDAM)\b/i, iso3: "NLD", flag: "🇳🇱", priority: 30 },
|
||||
|
||||
// UK
|
||||
{ regex: /\b(UK|GB|GBR|UNITED\s*KINGDOM|LONDON|MANCHESTER)\b/i, iso3: "GBR", flag: "🇬🇧", priority: 40 },
|
||||
|
||||
// France
|
||||
{ regex: /\b(FR|FRA|FRANCE|PARIS|MARSEILLE)\b/i, iso3: "FRA", flag: "🇫🇷", priority: 50 },
|
||||
|
||||
// Poland
|
||||
{ regex: /\b(PL|POL|POLAND|WARSAW|WARSZAWA)\b/i, iso3: "POL", flag: "🇵🇱", priority: 60 },
|
||||
|
||||
// Finland
|
||||
{ regex: /\b(FI|FIN|FINLAND|HELSINKI)\b/i, iso3: "FIN", flag: "🇫🇮", priority: 70 },
|
||||
|
||||
// Sweden
|
||||
{ regex: /\b(SE|SWE|SWEDEN|STOCKHOLM)\b/i, iso3: "SWE", flag: "🇸🇪", priority: 80 },
|
||||
|
||||
// Norway
|
||||
{ regex: /\b(NO|NOR|NORWAY|OSLO)\b/i, iso3: "NOR", flag: "🇳🇴", priority: 90 },
|
||||
|
||||
// Switzerland
|
||||
{ regex: /\b(CH|CHE|SWITZERLAND|ZURICH|GENEVA)\b/i, iso3: "CHE", flag: "🇨🇭", priority: 100 },
|
||||
|
||||
// Estonia / Latvia / Lithuania
|
||||
{ regex: /\b(EE|EST|ESTONIA|TALLINN)\b/i, iso3: "EST", flag: "🇪🇪", priority: 110 },
|
||||
{ regex: /\b(LV|LVA|LATVIA|RIGA)\b/i, iso3: "LVA", flag: "🇱🇻", priority: 120 },
|
||||
{ regex: /\b(LT|LTU|LITHUANIA|VILNIUS)\b/i, iso3: "LTU", flag: "🇱🇹", priority: 130 },
|
||||
|
||||
// Turkey
|
||||
{ regex: /\b(TR|TUR|TURKEY|ISTANBUL)\b/i, iso3: "TUR", flag: "🇹🇷", priority: 140 },
|
||||
|
||||
// Singapore / Japan / Korea / Hong Kong
|
||||
{ regex: /\b(SG|SGP|SINGAPORE)\b/i, iso3: "SGP", flag: "🇸🇬", priority: 200 },
|
||||
{ regex: /\b(JP|JPN|JAPAN|TOKYO|OSAKA)\b/i, iso3: "JPN", flag: "🇯🇵", priority: 210 },
|
||||
{ regex: /\b(KR|KOR|KOREA|SEOUL)\b/i, iso3: "KOR", flag: "🇰🇷", priority: 220 },
|
||||
{ regex: /\b(HK|HKG|HONG\s*KONG)\b/i, iso3: "HKG", flag: "🇭🇰", priority: 230 },
|
||||
];
|
||||
|
||||
// 5) GeoIP mapping (ISO2 -> ISO3 + flag) used only if utils.geoip.lookup(ip) returns ISO2
|
||||
const ISO2_TO_ISO3 = {
|
||||
US: { iso3: "USA", flag: "🇺🇸" },
|
||||
DE: { iso3: "DEU", flag: "🇩🇪" },
|
||||
NL: { iso3: "NLD", flag: "🇳🇱" },
|
||||
GB: { iso3: "GBR", flag: "🇬🇧" },
|
||||
FR: { iso3: "FRA", flag: "🇫🇷" },
|
||||
PL: { iso3: "POL", flag: "🇵🇱" },
|
||||
FI: { iso3: "FIN", flag: "🇫🇮" },
|
||||
SE: { iso3: "SWE", flag: "🇸🇪" },
|
||||
NO: { iso3: "NOR", flag: "🇳🇴" },
|
||||
CH: { iso3: "CHE", flag: "🇨🇭" },
|
||||
EE: { iso3: "EST", flag: "🇪🇪" },
|
||||
LV: { iso3: "LVA", flag: "🇱🇻" },
|
||||
LT: { iso3: "LTU", flag: "🇱🇹" },
|
||||
TR: { iso3: "TUR", flag: "🇹🇷" },
|
||||
SG: { iso3: "SGP", flag: "🇸🇬" },
|
||||
JP: { iso3: "JPN", flag: "🇯🇵" },
|
||||
KR: { iso3: "KOR", flag: "🇰🇷" },
|
||||
HK: { iso3: "HKG", flag: "🇭🇰" },
|
||||
};
|
||||
|
||||
// 6) Protocol icons (based on proxy.type)
|
||||
const PROTOCOL_ICONS = {
|
||||
ss: "🔒",
|
||||
ssr: "☂️",
|
||||
vmess: "🪁",
|
||||
vless: "🌌",
|
||||
trojan: "🐎",
|
||||
http: "🌐",
|
||||
socks5: "🧦",
|
||||
snell: "🐌",
|
||||
wireguard: "🐲",
|
||||
hysteria: "🤪",
|
||||
hysteria2: "⚡",
|
||||
tuic: "🚅"
|
||||
};
|
||||
|
||||
///////////////////////
|
||||
// HELPERS
|
||||
///////////////////////
|
||||
|
||||
function isIPv4(str) {
|
||||
if (typeof str !== "string") return false;
|
||||
const m = str.match(/^(\d{1,3})(\.\d{1,3}){3}$/);
|
||||
if (!m) return false;
|
||||
return str.split(".").every(oct => {
|
||||
const n = Number(oct);
|
||||
return n >= 0 && n <= 255 && String(n) === oct.replace(/^0+(\d)/, "$1"); // avoids "001" weirdness
|
||||
});
|
||||
}
|
||||
|
||||
function uniq(arr) {
|
||||
return [...new Set(arr.filter(Boolean))];
|
||||
}
|
||||
|
||||
function sanitizeBaseName(name) {
|
||||
let s = String(name || "");
|
||||
|
||||
// Remove noise patterns
|
||||
for (const re of NOISE_PATTERNS) s = s.replace(re, " ");
|
||||
|
||||
// Collapse spaces
|
||||
s = s.replace(/\s+/g, " ").trim();
|
||||
return s;
|
||||
}
|
||||
|
||||
function extractIconTagsAndStrip(name) {
|
||||
let s = String(name || "");
|
||||
const tags = [];
|
||||
|
||||
for (const r of ICON_RULES) {
|
||||
if (r.regex.test(s)) {
|
||||
tags.push(r.icon);
|
||||
s = s.replace(r.regex, " ");
|
||||
}
|
||||
}
|
||||
|
||||
for (const t of NAME_NETWORK_TAGS) {
|
||||
if (t.regex.test(s)) {
|
||||
tags.push(t.tag);
|
||||
s = s.replace(t.regex, " ");
|
||||
}
|
||||
}
|
||||
|
||||
return { stripped: s.replace(/\s+/g, " ").trim(), tags: uniq(tags) };
|
||||
}
|
||||
|
||||
function detectCountryByName(name) {
|
||||
const n = String(name || "");
|
||||
// Order by priority, then first match wins
|
||||
const sorted = COUNTRY_RULES.slice().sort((a, b) => a.priority - b.priority);
|
||||
for (const c of sorted) {
|
||||
if (c.regex.test(n)) return { iso3: c.iso3, flag: c.flag, priority: c.priority, source: "name" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectCountryByGeoIP(server, utils) {
|
||||
if (!isIPv4(server)) return null;
|
||||
if (!utils || !utils.geoip || typeof utils.geoip.lookup !== "function") return null;
|
||||
|
||||
try {
|
||||
const geo = utils.geoip.lookup(server);
|
||||
const iso2 = geo && (geo.country || geo.country_code || geo.iso_code);
|
||||
if (!iso2 || typeof iso2 !== "string") return null;
|
||||
|
||||
const key = iso2.toUpperCase();
|
||||
const mapped = ISO2_TO_ISO3[key];
|
||||
if (mapped) return { iso3: mapped.iso3, flag: mapped.flag, priority: 900, source: "geoip" };
|
||||
|
||||
// Unknown ISO2: keep something sane
|
||||
return { iso3: key, flag: "🏳️", priority: 950, source: "geoip" };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function pad2(n) {
|
||||
const x = Number(n);
|
||||
return x < 10 ? `0${x}` : String(x);
|
||||
}
|
||||
|
||||
function safeStr(v) {
|
||||
return (v === undefined || v === null) ? "" : String(v);
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
// OPERATOR
|
||||
///////////////////////
|
||||
|
||||
function operator(proxies, targetPlatform, utils) {
|
||||
// Sub-Store sometimes passes utils as global $utils; sometimes as 3rd arg; sometimes not at all.
|
||||
// We'll accept any of them without whining.
|
||||
const U = utils || (typeof $utils !== "undefined" ? $utils : null);
|
||||
|
||||
const buckets = Object.create(null);
|
||||
|
||||
for (const proxy of proxies) {
|
||||
const originalName = safeStr(proxy && proxy.name);
|
||||
|
||||
// 1) Extract tags (icons) from ORIGINAL name, then strip those keywords out
|
||||
const iconStage = extractIconTagsAndStrip(originalName);
|
||||
|
||||
// 2) Sanitize remaining base name (remove marketing trash, brackets, etc.)
|
||||
const cleanBase = sanitizeBaseName(iconStage.stripped);
|
||||
|
||||
// 3) Detect country (name first, then GeoIP)
|
||||
const byName = detectCountryByName(originalName);
|
||||
const byGeo = detectCountryByGeoIP(proxy && proxy.server, U);
|
||||
const country = byName || byGeo || { iso3: "UNK", flag: "🏴☠️", priority: 9999, source: "fallback" };
|
||||
|
||||
// 4) Protocol icon (based on type)
|
||||
const proto = PROTOCOL_ICONS[(proxy && proxy.type) || ""] || "🔌";
|
||||
|
||||
// 5) Network/type/port tag (from proxy fields)
|
||||
const net = safeStr(proxy && proxy.network) || "net?";
|
||||
const typ = safeStr(proxy && proxy.type) || "type?";
|
||||
const port = safeStr(proxy && proxy.port) || "port?";
|
||||
const metaTag = `▫️${net}/${typ}/${port}`;
|
||||
|
||||
// 6) Prepare bucket key
|
||||
const key = country.iso3;
|
||||
|
||||
if (!buckets[key]) {
|
||||
buckets[key] = {
|
||||
country,
|
||||
list: []
|
||||
};
|
||||
}
|
||||
|
||||
// Keep meta used for sorting and final formatting
|
||||
buckets[key].list.push({
|
||||
proxy,
|
||||
_meta: {
|
||||
cleanBase,
|
||||
iconTags: iconStage.tags,
|
||||
proto,
|
||||
metaTag
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 7) Sort buckets by priority
|
||||
const bucketKeys = Object.keys(buckets).sort((a, b) => {
|
||||
return (buckets[a].country.priority || 9999) - (buckets[b].country.priority || 9999);
|
||||
});
|
||||
|
||||
// 8) Sort inside each country bucket and rename with per-country numbering
|
||||
const result = [];
|
||||
|
||||
for (const key of bucketKeys) {
|
||||
const group = buckets[key];
|
||||
|
||||
group.list.sort((A, B) => {
|
||||
// Tags do not affect numbering: sort only by sanitized base + server:port as tie-breaker
|
||||
const an = A._meta.cleanBase.toLowerCase();
|
||||
const bn = B._meta.cleanBase.toLowerCase();
|
||||
if (an !== bn) return an.localeCompare(bn);
|
||||
|
||||
const as = `${safeStr(A.proxy.server)}:${safeStr(A.proxy.port)}`;
|
||||
const bs = `${safeStr(B.proxy.server)}:${safeStr(B.proxy.port)}`;
|
||||
return as.localeCompare(bs);
|
||||
});
|
||||
|
||||
for (let i = 0; i < group.list.length; i++) {
|
||||
const item = group.list[i];
|
||||
const p = item.proxy;
|
||||
const num = pad2(i + 1);
|
||||
|
||||
const tagStr = item._meta.iconTags.length ? ` ${item._meta.iconTags.join(" ")}` : "";
|
||||
// Final name format:
|
||||
// 🇩🇪 DEU-03 🌌 📺 ❻ ▫️ws/vless/443
|
||||
p.name = `${group.country.flag} ${group.country.iso3}-${num} ${item._meta.proto}${tagStr} ${item._meta.metaTag}`.replace(/\s+/g, " ").trim();
|
||||
|
||||
result.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
499
config-sub-converter/scripts/external-proxies-sanitizer.js
Normal file
@@ -0,0 +1,499 @@
|
||||
/**
|
||||
* Sub-Store operator: Normalize + tag + country detect + per-country numbering
|
||||
*
|
||||
* Output format (default):
|
||||
* 🇺🇸 USA-01 🔒 📺 ❻ ▫️ws/vless/443
|
||||
*
|
||||
* Notes:
|
||||
* - Numbering is computed per-country AFTER grouping the full list.
|
||||
* - Tags (icons) do NOT affect numbering order.
|
||||
* - GeoIP is optional and only used when server is an IP and utils.geoip.lookup exists.
|
||||
*/
|
||||
|
||||
///////////////////////
|
||||
// CONFIG (EDIT ME)
|
||||
///////////////////////
|
||||
|
||||
const DEBUG_APPEND_ORIGINAL_NAME = false; // set true to enable debug mode (appends original name as comment)
|
||||
|
||||
// 1) Remove these patterns (marketing noise, brackets, separators, etc.)
|
||||
const NOISE_PATTERNS = [
|
||||
/\[[^\]]*]/g, // [ ... ]
|
||||
/\([^)]*\)/g, // ( ... )
|
||||
/\{[^}]*}/g, // { ... }
|
||||
/\btraffic\b/gi,
|
||||
/\bfree\b/gi,
|
||||
/\bwebsite\b/gi,
|
||||
/\bexpire\b/gi,
|
||||
/\blow\s*ping\b/gi,
|
||||
/\bai\s*studio\b/gi,
|
||||
/\bno\s*p2p\b/gi,
|
||||
/\b10\s*gbit\b/gi,
|
||||
/\bvless\b/gi,
|
||||
/\bvmess\b/gi,
|
||||
/\bssr?\b/gi,
|
||||
/\btrojan\b/gi,
|
||||
/\bhysteria2?\b/gi,
|
||||
/\btuic\b/gi,
|
||||
/[|]/g,
|
||||
/[_]+/g,
|
||||
/[-]{2,}/g
|
||||
];
|
||||
|
||||
// 2) Keyword -> icon tags (if found in original name, icon is added; the keyword is removed from base name)
|
||||
// 🇫🇿 🇺🇳 🇩🇻 🇻🇿 🇵🇷 🇦🇿 🇬🇺🇦🇷🇩
|
||||
const ICON_RULES = [
|
||||
{ regex: /TEST/gi, icon: "🧪" },
|
||||
{ regex: uWordBoundaryGroup("Low Ping"), icon: "⚡️" },
|
||||
{ regex: uWordBoundaryGroup("10 Gbit"), icon: "🛤️" },
|
||||
{ regex: uWordBoundaryGroup("YT|Russia|Россия"), icon: "📺" },
|
||||
{ regex: uWordBoundaryGroup("IPv6"), icon: "🎱" },
|
||||
{ regex: uWordBoundaryGroup("Gemini|AI Studio"), icon: "🤖" },
|
||||
{ regex: uWordBoundaryGroup("Torrent|P2P|P2P-Torrents"), icon: "🧲" },
|
||||
|
||||
{ regex: uWordBoundaryGroup("local"), icon: "🚪" },
|
||||
{ regex: uWordBoundaryGroup("neighbourhood"), icon: "🫂" },
|
||||
|
||||
{ regex: uWordBoundaryGroup("xfizz|x-fizz"), icon: "🇫" },
|
||||
{ regex: uWordBoundaryGroup("unicade|uncd"), icon: "🇺" },
|
||||
{ regex: uWordBoundaryGroup("vzdh|vezdehod"), icon: "🇻" },
|
||||
{ regex: uWordBoundaryGroup("dvpn|d-vpn"), icon: "🇩" },
|
||||
{ regex: uWordBoundaryGroup("proton"), icon: "🇵" },
|
||||
{ regex: uWordBoundaryGroup("amnezia"), icon: "🇦" },
|
||||
{ regex: uWordBoundaryGroup("adguard"), icon: "🇬" },
|
||||
];
|
||||
|
||||
// 3) Optional “network” tag rules based on NAME text (not $server.network)
|
||||
// (Useful if providers shove "BGP/IPLC" into the node name)
|
||||
const NAME_NETWORK_TAGS = [
|
||||
{ regex: uWordBoundaryGroup("IPLC"), tag: "🛰️" },
|
||||
{ regex: uWordBoundaryGroup("BGP"), tag: "🧭" },
|
||||
{ regex: uWordBoundaryGroup("Anycast"), tag: "🌍" }
|
||||
];
|
||||
|
||||
// 4) Country detection rules by NAME (regex). First match wins (priority = lower is earlier)
|
||||
const COUNTRY_RULES = [
|
||||
{ regex: uWordBoundaryGroup("(Аргентина|Argentina|AR|ARG|ARGENTINA|BUENOS\s*AIRES)"), iso3: "ARG", flag: "🇦🇷", priority: 100 }, // Argentina
|
||||
{ regex: uWordBoundaryGroup("(Australia|AU|AUS|AUSTRALIA|SYDNEY)"), iso3: "AUS", flag: "🇦🇺", priority: 110 }, // Australia
|
||||
{ regex: uWordBoundaryGroup("(Austria|AT|AUT|AUSTRIA|VIENNA)"), iso3: "AUT", flag: "🇦🇹", priority: 120 }, // Austria
|
||||
{ regex: uWordBoundaryGroup("(Беларусь|Белоруссия|BELARUS)"), iso3: "BLR", flag: "🇧🇾", priority: 130 }, // Belarus
|
||||
{ regex: uWordBoundaryGroup("(Brazil|BR|BRA|BRAZIL|SAO\s*PAULO)"), iso3: "BRA", flag: "🇧🇷", priority: 140 }, // Brazil
|
||||
{ regex: uWordBoundaryGroup("(Bulgaria|BG|BGR|BULGARIA|SOFIA)"), iso3: "BGR", flag: "🇧🇬", priority: 150 }, // Bulgaria
|
||||
{ regex: uWordBoundaryGroup("(Canada|CA|CAN|CANADA|TORONTO)"), iso3: "CAN", flag: "🇨🇦", priority: 160 }, // Canada
|
||||
{ regex: uWordBoundaryGroup("(КИТАЙ|China)"), iso3: "CHN", flag: "🇨🇳", priority: 170 }, // China
|
||||
{ regex: uWordBoundaryGroup("(Czech\s*Republic|CZ|CZE|CZECH|PRAGUE)"), iso3: "CZE", flag: "🇨🇿", priority: 180 }, // Czech Republic
|
||||
{ regex: uWordBoundaryGroup("(Denmark|DK|DNK|DENMARK|COPENHAGEN)"), iso3: "DNK", flag: "🇩🇰", priority: 190 }, // Denmark
|
||||
{ regex: uWordBoundaryGroup("(Egypt|EG|EGY|EGYPT|CAIRO)"), iso3: "EGY", flag: "🇪🇬", priority: 200 }, // Egypt
|
||||
{ regex: uWordBoundaryGroup("(Эстония|EE|EST|ESTONIA|TALLINN)"), iso3: "EST", flag: "🇪🇪", priority: 210 }, // Estonia
|
||||
{ regex: uWordBoundaryGroup("(Финляндия|FI|FIN|FINLAND|HELSINKI)"), iso3: "FIN", flag: "🇫🇮", priority: 220 }, // Finland
|
||||
{ regex: uWordBoundaryGroup("(Франция|FR|FRA|FRANCE|PARIS|MARSEILLE)"), iso3: "FRA", flag: "🇫🇷", priority: 230 }, // France
|
||||
{ regex: uWordBoundaryGroup("(Georgia|GE|GEO|GEORGIA|TBILISI)"), iso3: "GEO", flag: "🇬🇪", priority: 240 }, // Georgia
|
||||
{ regex: uWordBoundaryGroup("(Германия|DE|DEU|GER(MANY)?|FRANKFURT|BERLIN|MUNICH)"), iso3: "DEU", flag: "🇩🇪", priority: 250 }, // Germany
|
||||
{ regex: uWordBoundaryGroup("(Гонконг|HK|HKG|HONG\s*KONG)"), iso3: "HKG", flag: "🇭🇰", priority: 260 }, // Hong Kong
|
||||
{ regex: uWordBoundaryGroup("(India|IN|IND|INDIA|MUMBAI)"), iso3: "IND", flag: "🇮🇳", priority: 270 }, // India
|
||||
{ regex: uWordBoundaryGroup("(Ireland|IE|IRL|IRELAND|DUBLIN)"), iso3: "IRL", flag: "🇮🇪", priority: 280 }, // Ireland
|
||||
{ regex: uWordBoundaryGroup("(Israel|IL|ISR|ISRAEL|TEL\s*AVIV)"), iso3: "ISR", flag: "🇮🇱", priority: 290 }, // Israel
|
||||
{ regex: uWordBoundaryGroup("(Italy|IT|ITA|ITALY|ROME)"), iso3: "ITA", flag: "🇮🇹", priority: 300 }, // Italy
|
||||
{ regex: uWordBoundaryGroup("(Япония|JP|JPN|JAPAN|TOKYO|OSAKA)"), iso3: "JPN", flag: "🇯🇵", priority: 310 }, // Japan
|
||||
{ regex: uWordBoundaryGroup("(Kazakhstan|KZ|KAZ|KAZAKHSTAN|ALMATY)"), iso3: "KAZ", flag: "🇰🇿", priority: 320 }, // Kazakhstan
|
||||
{ regex: uWordBoundaryGroup("(Латвия|LV|LVA|LATVIA|RIGA)"), iso3: "LVA", flag: "🇱🇻", priority: 330 }, // Latvia
|
||||
{ regex: uWordBoundaryGroup("(Литва|LT|LTU|LITHUANIA|VILNIUS)"), iso3: "LTU", flag: "🇱🇹", priority: 340 }, // Lithuania
|
||||
{ regex: uWordBoundaryGroup("(Malaysia|MY|MYS|MALAYSIA|KUALA\s*LUMPUR)"), iso3: "MYS", flag: "🇲🇾", priority: 350 }, // Malaysia
|
||||
{ regex: uWordBoundaryGroup("(Moldova|MD|MDA|MOLDOVA|CHISINAU)"), iso3: "MDA", flag: "🇲🇩", priority: 360 }, // Moldova
|
||||
{ regex: uWordBoundaryGroup("(Нидерланды|NL|NLD|NETHERLANDS|HOLLAND|AMSTERDAM)"), iso3: "NLD", flag: "🇳🇱", priority: 370 }, // Netherlands
|
||||
{ regex: uWordBoundaryGroup("(Nigeria|NG|NGA|NIGERIA|LAGOS)"), iso3: "NGA", flag: "🇳🇬", priority: 380 }, // Nigeria
|
||||
{ regex: uWordBoundaryGroup("(Норвегия|NO|NOR|NORWAY|OSLO)"), iso3: "NOR", flag: "🇳🇴", priority: 390 }, // Norway
|
||||
{ regex: uWordBoundaryGroup("(Philippines|PH|PHL|PHILIPPINES|MANILA)"), iso3: "PHL", flag: "🇵🇭", priority: 400 }, // Philippines
|
||||
{ regex: uWordBoundaryGroup("(Польша|PL|POL|POLAND|WARSAW|WARSZAWA)"), iso3: "POL", flag: "🇵🇱", priority: 410 }, // Poland
|
||||
{ regex: uWordBoundaryGroup("(Portugal|PT|PRT|PORTUGAL|LISBON)"), iso3: "PRT", flag: "🇵🇹", priority: 420 }, // Portugal
|
||||
{ regex: uWordBoundaryGroup("(Romania|RO|ROU|ROMANIA|BUCHAREST)"), iso3: "ROU", flag: "🇷🇴", priority: 430 }, // Romania
|
||||
{ regex: uWordBoundaryGroup("(Russia|RU|RUS|RUSSIA|MOSCOW)"), iso3: "RUS", flag: "🇷🇺", priority: 440 }, // Russia
|
||||
{ regex: uWordBoundaryGroup("(Сингапур|SG|SGP|SINGAPORE)"), iso3: "SGP", flag: "🇸🇬", priority: 200 }, // Singapore
|
||||
{ regex: uWordBoundaryGroup("(South Korea|Корея|KR|KOR|KOREA|SEOUL)"), iso3: "KOR", flag: "🇰🇷", priority: 450 }, // South Korea
|
||||
{ regex: uWordBoundaryGroup("(Spain|ES|ESP|SPAIN|MADRID)"), iso3: "ESP", flag: "🇪🇸", priority: 460 }, // Spain
|
||||
{ regex: uWordBoundaryGroup("(Швеция|SE|SWE|SWEDEN|STOCKHOLM)"), iso3: "SWE", flag: "🇸🇪", priority: 470 }, // Sweden
|
||||
{ regex: uWordBoundaryGroup("(Швейцария|CH|CHE|SWITZERLAND|Switzerl)"), iso3: "CHE", flag: "🇨🇭", priority: 480 }, // Switzerland
|
||||
{ regex: uWordBoundaryGroup("(Taiwan|TW|TWN|TAIWAN|TAIPEI)"), iso3: "TWN", flag: "🇹🇼", priority: 490 }, // Taiwan
|
||||
{ regex: uWordBoundaryGroup("(Thailand|TH|THA|THAILAND|BANGKOK)"), iso3: "THA", flag: "🇹🇭", priority: 500 }, // Thailand
|
||||
{ regex: uWordBoundaryGroup("(Турция|TR|TUR|TURKEY|ISTANBUL)"), iso3: "TUR", flag: "🇹🇷", priority: 510 }, // Turkey
|
||||
{ regex: uWordBoundaryGroup("(UAE|United\s*Arab\s*Emirates|AE|ARE|DUBAI)"), iso3: "ARE", flag: "🇦🇪", priority: 520 }, // UAE
|
||||
{ regex: uWordBoundaryGroup("(Великобритания|Англия|England|UK|GB|GBR|UNITED\s*KINGDOM)"), iso3: "GBR", flag: "🇬🇧", priority: 530 }, // UK
|
||||
{ regex: uWordBoundaryGroup("(США|USA|US|UNITED\s*STATES|AMERICA|NEW\s*YORK|NYC)"), iso3: "USA", flag: "🇺🇸", priority: 540 }, // USA
|
||||
{ regex: uWordBoundaryGroup("(Vietnam|VN|VNM|VIETNAM|HANOI)"), iso3: "VNM", flag: "🇻🇳", priority: 500 } // Vietnam
|
||||
];
|
||||
|
||||
// 5) GeoIP mapping (ISO2 -> ISO3 + flag) used only if utils.geoip.lookup(ip) returns ISO2
|
||||
const ISO2_TO_ISO3 = {
|
||||
US: { iso3: "USA", flag: "🇺🇸" },
|
||||
DE: { iso3: "DEU", flag: "🇩🇪" },
|
||||
NL: { iso3: "NLD", flag: "🇳🇱" },
|
||||
GB: { iso3: "GBR", flag: "🇬🇧" },
|
||||
FR: { iso3: "FRA", flag: "🇫🇷" },
|
||||
PL: { iso3: "POL", flag: "🇵🇱" },
|
||||
FI: { iso3: "FIN", flag: "🇫🇮" },
|
||||
SE: { iso3: "SWE", flag: "🇸🇪" },
|
||||
NO: { iso3: "NOR", flag: "🇳🇴" },
|
||||
CH: { iso3: "CHE", flag: "🇨🇭" },
|
||||
EE: { iso3: "EST", flag: "🇪🇪" },
|
||||
LV: { iso3: "LVA", flag: "🇱🇻" },
|
||||
LT: { iso3: "LTU", flag: "🇱🇹" },
|
||||
TR: { iso3: "TUR", flag: "🇹🇷" },
|
||||
SG: { iso3: "SGP", flag: "🇸🇬" },
|
||||
JP: { iso3: "JPN", flag: "🇯🇵" },
|
||||
KR: { iso3: "KOR", flag: "🇰🇷" },
|
||||
HK: { iso3: "HKG", flag: "🇭🇰" },
|
||||
};
|
||||
|
||||
// 6) Protocol icons (based on proxy.type)
|
||||
const PROTOCOL_ICONS = {
|
||||
ss: "",
|
||||
ssr: "",
|
||||
vmess: "",
|
||||
vless: "",
|
||||
trojan: "",
|
||||
http: "",
|
||||
socks5: "",
|
||||
snell: "",
|
||||
wireguard: "",
|
||||
hysteria: "",
|
||||
hysteria2: "",
|
||||
tuic: ""
|
||||
};
|
||||
|
||||
const STANDARD_PORTS_BY_TYPE = {
|
||||
wireguard: new Set(["51820"]),
|
||||
vless: new Set(["443"]),
|
||||
trojan: new Set(["443"]),
|
||||
ss: new Set(["443"]),
|
||||
};
|
||||
|
||||
const PROTOCOL_ICON_DEFAULT = ""; // fallback icon if type is unknown
|
||||
|
||||
|
||||
const METATAG_RULES = {
|
||||
// Keys are "network/type" OR "/type" (network-agnostic) OR "network/" (type-agnostic)
|
||||
// Matching priority: exact "network/type" -> "/type" -> "network/" -> default
|
||||
// 🅶🆃 🆃🆂 🆃🆅 🆆🆅 🆇🆅 🆆🅶 🅽🅸
|
||||
pairMap: {
|
||||
"grpc/trojan": "🅶🆃",
|
||||
"tcp/ss": "🆃🆂",
|
||||
"tcp/vless": "🆃🆅",
|
||||
"ws/vless": "🆆🆅",
|
||||
"xhttp/vless": "🆇🆅",
|
||||
|
||||
"/wireguard": "🆆🅶",
|
||||
"/naive": "🅽🅸",
|
||||
},
|
||||
|
||||
defaultPair: "▫️", // fallback if nothing matches
|
||||
includeFallbackText: false, // if true, append "(net/type)" when defaultPair is used
|
||||
};
|
||||
|
||||
// Port formatting: superscript digits with left padding to 4 chars
|
||||
// 𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗
|
||||
const PORT_FORMAT = {
|
||||
padLeftTo: 3,
|
||||
padChar: "0",
|
||||
fancy: {
|
||||
"0": "𝟎", "1": "𝟏", "2": "𝟐", "3": "𝟑", "4": "𝟒", "5": "𝟓", "6": "𝟔", "7": "𝟕", "8": "𝟖", "9": "𝟗",
|
||||
},
|
||||
};
|
||||
|
||||
///////////////////////
|
||||
// HELPERS
|
||||
///////////////////////
|
||||
|
||||
function normalizeToken(s) {
|
||||
return String(s || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
function uWordBoundaryGroup(inner) {
|
||||
// Match if surrounded by non-letter/non-digit (Unicode-aware)
|
||||
// We don't use lookbehind for max compatibility.
|
||||
return new RegExp(`(?:^|[^\\p{L}\\p{N}])(?:${inner})(?=$|[^\\p{L}\\p{N}])`, "iu");
|
||||
}
|
||||
|
||||
function portToFancy(port, type) {
|
||||
let p = String(port ?? "").trim();
|
||||
p = p.replace(/[^\d]/g, "");
|
||||
if (!p) return "";
|
||||
|
||||
if (STANDARD_PORTS_BY_TYPE[type]?.has(p)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// left pad to fixed width
|
||||
if (PORT_FORMAT.padLeftTo && p.length < PORT_FORMAT.padLeftTo) {
|
||||
p = p.padStart(PORT_FORMAT.padLeftTo, PORT_FORMAT.padChar);
|
||||
}
|
||||
|
||||
// map digits
|
||||
let out = "";
|
||||
for (const ch of p) out += PORT_FORMAT.fancy[ch] ?? ch;
|
||||
return out;
|
||||
}
|
||||
|
||||
function buildMetaTag(proxy) {
|
||||
const net = safeStr(proxy && proxy.network) || "";
|
||||
const typ = safeStr(proxy && proxy.type) || "";
|
||||
const port = safeStr(proxy && proxy.port);
|
||||
|
||||
const { icon, matched } = metaPairIcon(net, typ);
|
||||
const portSup = portToFancy(port, typ);
|
||||
|
||||
if (icon === METATAG_RULES.defaultPair && METATAG_RULES.includeFallbackText) {
|
||||
return `${icon}${portSup}(${normalizeToken(net)}/${normalizeToken(typ)})`;
|
||||
}
|
||||
|
||||
return `${icon}${portSup}`;
|
||||
}
|
||||
|
||||
function metaPairIcon(network, type) {
|
||||
const net = normalizeToken(network);
|
||||
const typ = normalizeToken(type);
|
||||
|
||||
const exact = `${net}/${typ}`;
|
||||
const typeOnly = `/${typ}`;
|
||||
const netOnly = `${net}/`;
|
||||
|
||||
const m = METATAG_RULES.pairMap;
|
||||
|
||||
if (m[exact]) return { icon: m[exact], matched: exact };
|
||||
if (m[typeOnly]) return { icon: m[typeOnly], matched: typeOnly };
|
||||
if (m[netOnly]) return { icon: m[netOnly], matched: netOnly };
|
||||
|
||||
return { icon: METATAG_RULES.defaultPair, matched: null };
|
||||
}
|
||||
|
||||
function isIPv4(str) {
|
||||
if (typeof str !== "string") return false;
|
||||
const m = str.match(/^(\d{1,3})(\.\d{1,3}){3}$/);
|
||||
if (!m) return false;
|
||||
return str.split(".").every(oct => {
|
||||
const n = Number(oct);
|
||||
return n >= 0 && n <= 255 && String(n) === oct.replace(/^0+(\d)/, "$1"); // avoids "001" weirdness
|
||||
});
|
||||
}
|
||||
|
||||
function uniq(arr) {
|
||||
return [...new Set(arr.filter(Boolean))];
|
||||
}
|
||||
|
||||
function sanitizeBaseName(name) {
|
||||
let s = String(name || "");
|
||||
|
||||
// Remove noise patterns
|
||||
for (const re of NOISE_PATTERNS) s = s.replace(re, " ");
|
||||
|
||||
// Collapse spaces
|
||||
s = s.replace(/\s+/g, " ").trim();
|
||||
return s;
|
||||
}
|
||||
|
||||
function extractIconTagsAndStrip(name) {
|
||||
let s = String(name || "");
|
||||
const tags = [];
|
||||
|
||||
for (const r of ICON_RULES) {
|
||||
if (r.regex.test(s)) {
|
||||
tags.push(r.icon);
|
||||
s = s.replace(r.regex, " ");
|
||||
}
|
||||
}
|
||||
|
||||
for (const t of NAME_NETWORK_TAGS) {
|
||||
if (t.regex.test(s)) {
|
||||
tags.push(t.tag);
|
||||
s = s.replace(t.regex, " ");
|
||||
}
|
||||
}
|
||||
|
||||
return { stripped: s.replace(/\s+/g, " ").trim(), tags: uniq(tags) };
|
||||
}
|
||||
|
||||
function detectCountryByName(name) {
|
||||
const n = String(name || "");
|
||||
// Order by priority, then first match wins
|
||||
|
||||
// Fast path: flag emoji
|
||||
if (n.includes("🇦🇪")) return { iso3: "ARE", flag: "🇦🇪", priority: 1, source: "flag" };
|
||||
if (n.includes("🇦🇷")) return { iso3: "ARG", flag: "🇦🇷", priority: 2, source: "flag" };
|
||||
if (n.includes("🇦🇹")) return { iso3: "AUT", flag: "🇦🇹", priority: 3, source: "flag" };
|
||||
if (n.includes("🇦🇺")) return { iso3: "AUS", flag: "🇦🇺", priority: 4, source: "flag" };
|
||||
if (n.includes("🇧🇬")) return { iso3: "BGR", flag: "🇧🇬", priority: 5, source: "flag" };
|
||||
if (n.includes("🇧🇾")) return { iso3: "BLR", flag: "🇧🇾", priority: 6, source: "flag" };
|
||||
if (n.includes("🇧🇷")) return { iso3: "BRA", flag: "🇧🇷", priority: 7, source: "flag" };
|
||||
if (n.includes("🇨🇦")) return { iso3: "CAN", flag: "🇨🇦", priority: 8, source: "flag" };
|
||||
if (n.includes("🇨🇭")) return { iso3: "CHE", flag: "🇨🇭", priority: 9, source: "flag" };
|
||||
if (n.includes("🇨🇳")) return { iso3: "CHN", flag: "🇨🇳", priority: 10, source: "flag" };
|
||||
if (n.includes("🇨🇿")) return { iso3: "CZE", flag: "🇨🇿", priority: 11, source: "flag" };
|
||||
if (n.includes("🇩🇪")) return { iso3: "DEU", flag: "🇩🇪", priority: 12, source: "flag" };
|
||||
if (n.includes("🇩🇰")) return { iso3: "DNK", flag: "🇩🇰", priority: 13, source: "flag" };
|
||||
if (n.includes("🇪🇪")) return { iso3: "EST", flag: "🇪🇪", priority: 14, source: "flag" };
|
||||
if (n.includes("🇪🇬")) return { iso3: "EGY", flag: "🇪🇬", priority: 15, source: "flag" };
|
||||
if (n.includes("🇪🇸")) return { iso3: "ESP", flag: "🇪🇸", priority: 16, source: "flag" };
|
||||
if (n.includes("🇫🇮")) return { iso3: "FIN", flag: "🇫🇮", priority: 17, source: "flag" };
|
||||
if (n.includes("🇫🇷")) return { iso3: "FRA", flag: "🇫🇷", priority: 18, source: "flag" };
|
||||
if (n.includes("🇬🇧")) return { iso3: "GBR", flag: "🇬🇧", priority: 19, source: "flag" };
|
||||
if (n.includes("🇬🇪")) return { iso3: "GEO", flag: "🇬🇪", priority: 20, source: "flag" };
|
||||
if (n.includes("🇭🇰")) return { iso3: "HKG", flag: "🇭🇰", priority: 21, source: "flag" };
|
||||
if (n.includes("🇮🇪")) return { iso3: "IRL", flag: "🇮🇪", priority: 22, source: "flag" };
|
||||
if (n.includes("🇮🇱")) return { iso3: "ISR", flag: "🇮🇱", priority: 23, source: "flag" };
|
||||
if (n.includes("🇮🇳")) return { iso3: "IND", flag: "🇮🇳", priority: 24, source: "flag" };
|
||||
if (n.includes("🇮🇹")) return { iso3: "ITA", flag: "🇮🇹", priority: 25, source: "flag" };
|
||||
if (n.includes("🇯🇵")) return { iso3: "JPN", flag: "🇯🇵", priority: 26, source: "flag" };
|
||||
if (n.includes("🇰🇷")) return { iso3: "KOR", flag: "🇰🇷", priority: 27, source: "flag" };
|
||||
if (n.includes("🇰🇿")) return { iso3: "KAZ", flag: "🇰🇿", priority: 28, source: "flag" };
|
||||
if (n.includes("🇱🇹")) return { iso3: "LTU", flag: "🇱🇹", priority: 29, source: "flag" };
|
||||
if (n.includes("🇱🇻")) return { iso3: "LVA", flag: "🇱🇻", priority: 30, source: "flag" };
|
||||
if (n.includes("🇲🇩")) return { iso3: "MDA", flag: "🇲🇩", priority: 31, source: "flag" };
|
||||
if (n.includes("🇲🇾")) return { iso3: "MYS", flag: "🇲🇾", priority: 32, source: "flag" };
|
||||
if (n.includes("🇳🇬")) return { iso3: "NGA", flag: "🇳🇬", priority: 33, source: "flag" };
|
||||
if (n.includes("🇳🇱")) return { iso3: "NLD", flag: "🇳🇱", priority: 34, source: "flag" };
|
||||
if (n.includes("🇳🇴")) return { iso3: "NOR", flag: "🇳🇴", priority: 35, source: "flag" };
|
||||
if (n.includes("🇵🇭")) return { iso3: "PHL", flag: "🇵🇭", priority: 36, source: "flag" };
|
||||
if (n.includes("🇵🇱")) return { iso3: "POL", flag: "🇵🇱", priority: 37, source: "flag" };
|
||||
if (n.includes("🇵🇹")) return { iso3: "PRT", flag: "🇵🇹", priority: 38, source: "flag" };
|
||||
if (n.includes("🇷🇴")) return { iso3: "ROU", flag: "🇷🇴", priority: 39, source: "flag" };
|
||||
if (n.includes("🇷🇺")) return { iso3: "RUS", flag: "🇷🇺", priority: 40, source: "flag" };
|
||||
if (n.includes("🇸🇪")) return { iso3: "SWE", flag: "🇸🇪", priority: 41, source: "flag" };
|
||||
if (n.includes("🇸🇬")) return { iso3: "SGP", flag: "🇸🇬", priority: 42, source: "flag" };
|
||||
if (n.includes("🇹🇭")) return { iso3: "THA", flag: "🇹🇭", priority: 43, source: "flag" };
|
||||
if (n.includes("🇹🇷")) return { iso3: "TUR", flag: "🇹🇷", priority: 44, source: "flag" };
|
||||
if (n.includes("🇹🇼")) return { iso3: "TWN", flag: "🇹🇼", priority: 45, source: "flag" };
|
||||
if (n.includes("🇺🇸")) return { iso3: "USA", flag: "🇺🇸", priority: 46, source: "flag" };
|
||||
if (n.includes("🇻🇳")) return { iso3: "VNM", flag: "🇻🇳", priority: 47, source: "flag" };
|
||||
|
||||
const sorted = COUNTRY_RULES.slice().sort((a, b) => a.priority - b.priority);
|
||||
for (const c of sorted) {
|
||||
if (c.regex.test(n)) return { iso3: c.iso3, flag: c.flag, priority: c.priority, source: "name" };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function detectCountryByGeoIP(server, utils) {
|
||||
if (!isIPv4(server)) return null;
|
||||
if (!utils || !utils.geoip || typeof utils.geoip.lookup !== "function") return null;
|
||||
|
||||
try {
|
||||
const geo = utils.geoip.lookup(server);
|
||||
const iso2 = geo && (geo.country || geo.country_code || geo.iso_code);
|
||||
if (!iso2 || typeof iso2 !== "string") return null;
|
||||
|
||||
const key = iso2.toUpperCase();
|
||||
const mapped = ISO2_TO_ISO3[key];
|
||||
if (mapped) return { iso3: mapped.iso3, flag: mapped.flag, priority: 900, source: "geoip" };
|
||||
|
||||
// Unknown ISO2: keep something sane
|
||||
return { iso3: key, flag: "🏳️", priority: 950, source: "geoip" };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function pad2(n) {
|
||||
const x = Number(n);
|
||||
return x < 10 ? `0${x}` : String(x);
|
||||
}
|
||||
|
||||
function safeStr(v) {
|
||||
return (v === undefined || v === null) ? "" : String(v);
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
// OPERATOR
|
||||
///////////////////////
|
||||
|
||||
function operator(proxies, targetPlatform, utils) {
|
||||
// Sub-Store sometimes passes utils as global $utils; sometimes as 3rd arg; sometimes not at all.
|
||||
// We'll accept any of them without whining.
|
||||
const U = utils || (typeof $utils !== "undefined" ? $utils : null);
|
||||
|
||||
const buckets = Object.create(null);
|
||||
|
||||
for (const proxy of proxies) {
|
||||
const originalName = safeStr(proxy && proxy.name);
|
||||
|
||||
// 1) Extract tags (icons) from ORIGINAL name, then strip those keywords out
|
||||
const iconStage = extractIconTagsAndStrip(originalName);
|
||||
|
||||
// 2) Sanitize remaining base name (remove marketing trash, brackets, etc.)
|
||||
const cleanBase = sanitizeBaseName(iconStage.stripped);
|
||||
|
||||
// 3) Detect country (name first, then GeoIP)
|
||||
const byName = detectCountryByName(originalName);
|
||||
const byGeo = detectCountryByGeoIP(proxy && proxy.server, U);
|
||||
const country = byName || byGeo || { iso3: "UNK", flag: "🏴☠️", priority: 9999, source: "fallback" };
|
||||
|
||||
// 4) Protocol icon (based on type)
|
||||
const proto = PROTOCOL_ICONS[(proxy && proxy.type) || ""] || PROTOCOL_ICON_DEFAULT;
|
||||
|
||||
// 5) Network/type/port tag (from proxy fields)
|
||||
const metaTag = buildMetaTag(proxy);
|
||||
|
||||
// 6) Prepare bucket key
|
||||
const key = country.iso3;
|
||||
|
||||
if (!buckets[key]) {
|
||||
buckets[key] = {
|
||||
country,
|
||||
list: []
|
||||
};
|
||||
}
|
||||
|
||||
// Keep meta used for sorting and final formatting
|
||||
buckets[key].list.push({
|
||||
proxy,
|
||||
_meta: {
|
||||
originalName,
|
||||
cleanBase,
|
||||
iconTags: iconStage.tags,
|
||||
proto,
|
||||
metaTag
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 7) Sort buckets by priority
|
||||
const bucketKeys = Object.keys(buckets).sort((a, b) => {
|
||||
return (buckets[a].country.priority || 9999) - (buckets[b].country.priority || 9999);
|
||||
});
|
||||
|
||||
// 8) Sort inside each country bucket and rename with per-country numbering
|
||||
const result = [];
|
||||
|
||||
for (const key of bucketKeys) {
|
||||
const group = buckets[key];
|
||||
|
||||
group.list.sort((A, B) => {
|
||||
// Tags do not affect numbering: sort only by sanitized base + server:port as tie-breaker
|
||||
const an = A._meta.cleanBase.toLowerCase();
|
||||
const bn = B._meta.cleanBase.toLowerCase();
|
||||
if (an !== bn) return an.localeCompare(bn);
|
||||
|
||||
const as = `${safeStr(A.proxy.server)}:${safeStr(A.proxy.port)}`;
|
||||
const bs = `${safeStr(B.proxy.server)}:${safeStr(B.proxy.port)}`;
|
||||
return as.localeCompare(bs);
|
||||
});
|
||||
|
||||
for (let i = 0; i < group.list.length; i++) {
|
||||
const item = group.list[i];
|
||||
const p = item.proxy;
|
||||
const num = pad2(i + 1);
|
||||
|
||||
const debugSuffix = DEBUG_APPEND_ORIGINAL_NAME
|
||||
? ` ⟦${item._meta.originalName}⟧`
|
||||
: "";
|
||||
|
||||
const tagStr = item._meta.iconTags.length ? ` ${item._meta.iconTags.join(" ")}` : "";
|
||||
|
||||
p.name = `${group.country.flag}${item._meta.metaTag} ${group.country.iso3}-${num} ${item._meta.proto}${tagStr} ${debugSuffix}`
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
|
||||
result.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
110
config-sub-converter/scripts/test-options.js
Normal file
@@ -0,0 +1,110 @@
|
||||
function safeStringify(obj) {
|
||||
const seen = new WeakSet();
|
||||
return JSON.stringify(
|
||||
obj,
|
||||
(k, v) => {
|
||||
if (typeof v === "object" && v !== null) {
|
||||
if (seen.has(v)) return "[Circular]";
|
||||
seen.add(v);
|
||||
}
|
||||
if (typeof v === "function") return `[Function: ${v.name || "anonymous"}]`;
|
||||
if (typeof v === "bigint") return v.toString();
|
||||
return v;
|
||||
},
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
function pickEnvSample() {
|
||||
try {
|
||||
const env = (typeof process !== "undefined" && process && process.env) ? process.env : null;
|
||||
if (!env) return null;
|
||||
|
||||
// only show safe-ish keys, no full dump
|
||||
const keys = Object.keys(env).sort();
|
||||
const filtered = keys.filter(k =>
|
||||
k.toLowerCase().includes("sub") ||
|
||||
k.toLowerCase().includes("store") ||
|
||||
k.toLowerCase().includes("script") ||
|
||||
k.toLowerCase().includes("url") ||
|
||||
k.toLowerCase().includes("option") ||
|
||||
k.toLowerCase().includes("param")
|
||||
);
|
||||
|
||||
const sample = {};
|
||||
for (const k of filtered.slice(0, 50)) sample[k] = env[k];
|
||||
return { keysCount: keys.length, filteredKeys: filtered.slice(0, 100), sample };
|
||||
} catch (e) {
|
||||
return { error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
function getGlobalDollarKeys() {
|
||||
try {
|
||||
return Object.getOwnPropertyNames(globalThis).filter(k => k.startsWith("$")).sort();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Safe "typeof" probes: never throws even if variable doesn't exist
|
||||
const probes = {
|
||||
$content: typeof $content,
|
||||
$files: typeof $files,
|
||||
$options: typeof $options,
|
||||
|
||||
$params: typeof $params,
|
||||
$args: typeof $args,
|
||||
$arguments: typeof $arguments,
|
||||
$argument: typeof $argument,
|
||||
$argv: typeof $argv,
|
||||
|
||||
$ctx: typeof $ctx,
|
||||
$context: typeof $context,
|
||||
$request: typeof $request,
|
||||
$req: typeof $req,
|
||||
$url: typeof $url,
|
||||
$scriptUrl: typeof $scriptUrl,
|
||||
$script_url: typeof $script_url,
|
||||
|
||||
ProxyUtils: typeof ProxyUtils,
|
||||
produceArtifact: typeof produceArtifact,
|
||||
|
||||
process: typeof process,
|
||||
};
|
||||
|
||||
const values = {};
|
||||
function maybeSet(name, getter) {
|
||||
try {
|
||||
const v = getter();
|
||||
// Avoid huge outputs
|
||||
if (typeof v === "string") values[name] = v.length > 800 ? v.slice(0, 800) + "…(truncated)" : v;
|
||||
else values[name] = v;
|
||||
} catch (e) {
|
||||
values[name] = { error: String(e) };
|
||||
}
|
||||
}
|
||||
|
||||
maybeSet("$options", () => (typeof $options !== "undefined" ? $options : null));
|
||||
maybeSet("$params", () => (typeof $params !== "undefined" ? $params : null));
|
||||
maybeSet("$args", () => (typeof $args !== "undefined" ? $args : null));
|
||||
maybeSet("$arguments", () => (typeof $arguments !== "undefined" ? $arguments : null));
|
||||
maybeSet("$argument", () => (typeof $argument !== "undefined" ? $argument : null));
|
||||
maybeSet("$ctx", () => (typeof $ctx !== "undefined" ? $ctx : null));
|
||||
maybeSet("$request", () => (typeof $request !== "undefined" ? $request : null));
|
||||
maybeSet("$url", () => (typeof $url !== "undefined" ? $url : null));
|
||||
maybeSet("$scriptUrl", () => (typeof $scriptUrl !== "undefined" ? $scriptUrl : null));
|
||||
maybeSet("$script_url", () => (typeof $script_url !== "undefined" ? $script_url : null));
|
||||
|
||||
maybeSet("$contentPreview", () => (typeof $content === "string" ? $content.slice(0, 300) : $content));
|
||||
maybeSet("$contentLength", () => (typeof $content === "string" ? $content.length : null));
|
||||
maybeSet("$files", () => (typeof $files !== "undefined" ? $files : null));
|
||||
|
||||
const report = {
|
||||
probes,
|
||||
values,
|
||||
globalDollarKeys: getGlobalDollarKeys(),
|
||||
envSample: pickEnvSample(),
|
||||
};
|
||||
|
||||
$content = safeStringify(report);
|
||||
13
icons/svg/numbers/n01.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 496.158 496.158" xml:space="preserve">
|
||||
<path style="fill:#56B48C;" d="M248.082,0.003C111.07,0.003,0,111.061,0,248.085c0,137,111.07,248.07,248.082,248.07
|
||||
c137.006,0,248.076-111.07,248.076-248.07C496.158,111.061,385.088,0.003,248.082,0.003z"/>
|
||||
<path style="fill:#FFFFFF;" d="M278.767,145.419c-3.126-4.003-7.276-6.006-12.451-6.006c-4.591,0-7.716,0.879-9.375,2.637
|
||||
c-1.662,1.758-5.226,6.445-10.693,14.063c-5.47,7.617-11.744,14.502-18.823,20.654c-7.082,6.152-16.53,12.012-28.345,17.578
|
||||
c-7.91,3.712-13.429,6.738-16.553,9.082c-3.126,2.344-4.688,6.006-4.688,10.986c0,4.298,1.586,8.082,4.761,11.353
|
||||
c3.172,3.273,6.812,4.907,10.913,4.907c8.592,0,25.292-9.521,50.098-28.564V335.41c0,7.814,1.806,13.722,5.42,17.725
|
||||
c3.612,4.003,8.397,6.006,14.355,6.006c13.378,0,20.068-9.814,20.068-29.443V161.972
|
||||
C283.455,154.941,281.892,149.425,278.767,145.419z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
19
icons/svg/numbers/n02.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 496.158 496.158" xml:space="preserve">
|
||||
<path style="fill:#56B48C;" d="M248.082,0.003C111.07,0.003,0,111.061,0,248.085c0,137,111.07,248.07,248.082,248.07
|
||||
c137.006,0,248.076-111.07,248.076-248.07C496.158,111.061,385.088,0.003,248.082,0.003z"/>
|
||||
<path style="fill:#FFFFFF;" d="M319.783,325.595c-4.005-3.124-9.814-4.688-17.432-4.688h-76.465c2.44-3.71,4.834-6.885,7.178-9.521
|
||||
c5.468-6.64,15.55-15.967,30.249-27.979c14.696-12.012,25.17-20.824,31.421-26.44c6.249-5.614,12.378-13.378,18.384-23.291
|
||||
c6.006-9.911,9.009-20.922,9.009-33.032c0-7.713-1.442-15.161-4.321-22.339c-2.882-7.178-6.91-13.5-12.085-18.97
|
||||
c-5.177-5.468-11.183-9.764-18.018-12.891c-10.547-4.688-23.291-7.031-38.232-7.031c-12.403,0-23.218,1.831-32.446,5.493
|
||||
s-16.846,8.473-22.852,14.429c-6.006,5.958-10.524,12.598-13.55,19.922c-3.028,7.324-4.541,14.355-4.541,21.094
|
||||
c0,5.566,1.611,9.961,4.834,13.184s7.274,4.834,12.158,4.834c5.566,0,9.789-1.758,12.671-5.273
|
||||
c2.879-3.516,5.468-8.544,7.764-15.088c2.293-6.542,3.93-10.547,4.907-12.012c7.324-11.229,17.381-16.846,30.176-16.846
|
||||
c6.054,0,11.646,1.369,16.772,4.102c5.127,2.735,9.178,6.569,12.158,11.499c2.978,4.933,4.468,10.524,4.468,16.772
|
||||
c0,5.763-1.392,11.646-4.175,17.651s-6.837,11.865-12.158,17.578c-5.324,5.713-11.989,11.403-19.995,17.065
|
||||
c-4.493,3.028-11.964,9.352-22.412,18.97c-10.451,9.62-22.169,21.167-35.156,34.644c-3.126,3.321-6.006,7.887-8.643,13.696
|
||||
c-2.637,5.812-3.955,10.474-3.955,13.989c0,5.47,2.051,10.231,6.152,14.282c4.102,4.054,9.814,6.079,17.139,6.079H306.6
|
||||
c6.445,0,11.254-1.659,14.429-4.98c3.172-3.319,4.761-7.372,4.761-12.158C325.789,332.97,323.786,328.722,319.783,325.595z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
24
icons/svg/numbers/n03.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 496.158 496.158" xml:space="preserve">
|
||||
<path style="fill:#56B48C;" d="M248.082,0.003C111.07,0.003,0,111.061,0,248.085c0,137,111.07,248.07,248.082,248.07
|
||||
c137.006,0,248.076-111.07,248.076-248.07C496.158,111.061,385.088,0.003,248.082,0.003z"/>
|
||||
<path style="fill:#FFFFFF;" d="M319.637,269.711c-2.637-6.395-6.569-12.231-11.792-17.505c-5.226-5.273-11.646-9.961-19.263-14.063
|
||||
c7.91-6.64,13.989-13.451,18.237-20.435c4.248-6.981,6.372-15.355,6.372-25.122c0-7.42-1.465-14.355-4.395-20.801
|
||||
s-7.276-12.108-13.037-16.992c-5.763-4.882-12.55-8.617-20.361-11.206c-7.814-2.586-16.457-3.882-25.928-3.882
|
||||
c-10.84,0-20.654,1.538-29.443,4.614s-16.139,7.155-22.046,12.231c-5.91,5.079-10.4,10.426-13.477,16.04
|
||||
c-3.076,5.617-4.614,10.963-4.614,16.04c0,5.273,1.634,9.499,4.907,12.671c3.271,3.175,6.859,4.761,10.767,4.761
|
||||
c3.319,0,6.249-0.586,8.789-1.758c2.538-1.172,4.296-2.783,5.273-4.834c1.659-3.809,3.49-7.86,5.493-12.158
|
||||
c2-4.296,4.125-7.812,6.372-10.547c2.245-2.733,5.296-4.93,9.155-6.592c3.856-1.659,8.764-2.49,14.722-2.49
|
||||
c8.789,0,15.77,2.71,20.947,8.13c5.175,5.42,7.764,11.891,7.764,19.409c0,9.865-3.248,17.432-9.741,22.705
|
||||
c-6.496,5.273-14.234,7.91-23.218,7.91h-6.006c-6.935,0-12.158,1.442-15.674,4.321c-3.516,2.882-5.273,6.665-5.273,11.353
|
||||
c0,4.786,1.465,8.521,4.395,11.206c2.93,2.687,7.079,4.028,12.451,4.028c1.172,0,3.809-0.194,7.91-0.586
|
||||
c4.102-0.389,7.127-0.586,9.082-0.586c11.133,0,19.823,3.248,26.074,9.741c6.249,6.496,9.375,15.454,9.375,26.88
|
||||
c0,7.716-1.831,14.502-5.493,20.361s-8.302,10.279-13.916,13.257c-5.617,2.98-11.451,4.468-17.505,4.468
|
||||
c-10.547,0-18.727-3.296-24.536-9.888c-5.812-6.592-11.256-16.674-16.333-30.249c-0.783-2.245-2.442-4.175-4.98-5.786
|
||||
c-2.541-1.611-5.177-2.417-7.91-2.417c-5.47,0-10.034,1.735-13.696,5.2c-3.662,3.468-5.493,8.034-5.493,13.696
|
||||
c0,4.395,1.538,9.961,4.614,16.699s7.617,13.257,13.623,19.556s13.646,11.549,22.925,15.747c9.276,4.198,19.775,6.299,31.494,6.299
|
||||
c11.522,0,22.046-1.831,31.567-5.493s17.748-8.739,24.683-15.234c6.933-6.493,12.181-13.891,15.747-22.192
|
||||
c3.563-8.299,5.347-16.894,5.347-25.781C323.592,283.018,322.273,276.109,319.637,269.711z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
17
icons/svg/substore/amnezia-hat.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="si-glyph si-glyph-hat">
|
||||
|
||||
<title>629</title>
|
||||
|
||||
<defs>
|
||||
|
||||
</defs>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(0.000000, 3.000000)" fill="#434343">
|
||||
<path d="M12.708,5.211 C12.278,6.128 10.548,7.239 8,7.239 C5.468,7.239 3.667,6.142 3.288,5.226 C1.375,5.312 0,6.456 0,7.136 C0,8.264 3.581,9.864 8,9.864 C12.418,9.864 16,8.264 16,7.136 C16,6.447 14.625,5.569 12.708,5.211 L12.708,5.211 Z" class="si-glyph-fill">
|
||||
|
||||
</path>
|
||||
<path d="M10.077,0.197 C9.561,0.197 8.495,0.551 8.012,0.551 C7.529,0.551 6.463,0.197 5.948,0.197 C4.834,0.197 4.138,1.569 4.041,2.094 L4.021,4.521 C4.583,5.027 5.632,5.663 6.782,5.759 C7.182,5.792 7.604,5.812 8.043,5.812 C8.48,5.812 8.901,5.792 9.302,5.759 C10.452,5.662 11.376,5.105 11.97,4.538 L11.965,2.125 C11.879,1.579 11.181,0.197 10.077,0.197 L10.077,0.197 Z" class="si-glyph-fill">
|
||||
|
||||
</path>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
19
icons/svg/substore/external.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" height="800px" width="800px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 365.789 365.789" xml:space="preserve">
|
||||
<g id="XMLID_7_">
|
||||
<path id="XMLID_8_" d="M137.743,18.96v139.676c0,62.873,51.15,114.023,114.022,114.023c62.873,0,114.023-51.15,114.023-114.023
|
||||
V18.96H137.743z M225.575,98.13c0,14.77-12.017,26.787-26.787,26.787S172.001,112.9,172.001,98.13
|
||||
c0-14.771,12.017-26.787,26.787-26.787S225.575,83.359,225.575,98.13z M277.957,98.13c0-14.771,12.017-26.787,26.787-26.787
|
||||
s26.787,12.017,26.787,26.787c0,14.77-12.017,26.787-26.787,26.787S277.957,112.9,277.957,98.13z M219.562,172.001
|
||||
c3.773,8.898,17.209,15.894,32.204,15.894s28.432-6.996,32.205-15.894h33.146c-1.503,13.065-8.85,25.31-20.672,34.176
|
||||
c-12.122,9.092-27.989,14.1-44.679,14.1c-16.688,0-32.556-5.007-44.678-14.1c-11.823-8.867-19.17-21.111-20.673-34.176H219.562z"/>
|
||||
<path id="XMLID_13_" d="M177.154,284.553l-1.181-0.703h-29.747c-3.772-8.898-17.209-15.894-32.203-15.894
|
||||
c-14.995,0-28.432,6.996-32.205,15.894H48.673c1.503-13.065,8.85-25.31,20.672-34.176c12.124-9.093,27.991-14.1,44.679-14.1
|
||||
c1.293,0,2.678,0.041,4.356,0.129l8.923,0.469l-4.268-7.85c-11.563-21.271-17.675-45.369-17.675-69.687V93.13H0v139.676
|
||||
c0,62.873,51.15,114.023,114.023,114.023c34.758,0,67.232-15.701,89.099-43.077l4.313-5.402l-6.48-2.406
|
||||
C192.743,292.895,184.736,289.063,177.154,284.553z M61.045,145.512c14.771,0,26.787,12.017,26.787,26.787
|
||||
c0,14.771-12.017,26.787-26.787,26.787c-14.77,0-26.786-12.017-26.786-26.787C34.259,157.529,46.275,145.512,61.045,145.512z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
2
icons/svg/substore/lab.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" width="800px" height="800px" viewBox="-0.5 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m22.171 19.68-7.352-11.311v-5.407h1.708v-2.962h-10.429v2.965h1.722v5.407l-7.366 11.308c-.285.431-.455.96-.455 1.528 0 1.542 1.25 2.792 2.792 2.792h.003 17.031c1.544-.003 2.795-1.255 2.795-2.8 0-.565-.167-1.091-.455-1.531l.007.011zm-.905 2.302c-.282.513-.818.854-1.434.854-.002 0-.004 0-.006 0h-17.032c-.001 0-.002 0-.003 0-.904 0-1.636-.732-1.636-1.636 0-.33.098-.638.266-.895l-.004.006 7.549-11.6v-5.751h4.686v5.754l7.541 11.6c.17.251.272.561.272.895 0 .285-.074.553-.204.785l.004-.008z"/><path d="m14.412 12.351h-6.191l-5.655 8.698c-.03.045-.048.1-.048.159 0 .051.013.098.036.14l-.001-.001c.05.087.142.145.248.146h17.031.001c.106 0 .198-.058.247-.145l.001-.001c.022-.04.036-.088.036-.138 0-.059-.018-.115-.049-.16l.001.001z"/></svg>
|
||||
|
After Width: | Height: | Size: 969 B |
4
icons/svg/substore/magic.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.544 1.49857C16.8278 1.08878 16.0621 1.24503 15.3437 1.54072C14.6378 1.83126 13.769 2.3458 12.7076 2.97441L12.0804 3.34588C11.394 3.75237 11.2339 3.83012 11.0785 3.84959C10.9285 3.86837 10.7686 3.83515 10.0272 3.60702L9.34735 3.3978C8.20554 3.0464 7.26483 2.75689 6.52104 2.64685C5.74973 2.53274 4.98782 2.57927 4.38078 3.13487C3.77784 3.68672 3.65464 4.4423 3.68494 5.22709C3.71433 5.98815 3.89821 6.96778 4.12234 8.16182L4.25502 8.86874C4.3996 9.63911 4.41777 9.81347 4.38149 9.97264C4.34436 10.1356 4.24727 10.294 3.77952 10.9431L3.3514 11.5371C2.62441 12.5457 2.03065 13.3695 1.67313 14.0516C1.30926 14.7457 1.08259 15.4902 1.40213 16.2436C1.72676 17.009 2.42603 17.3422 3.17507 17.5274C3.90291 17.7074 4.88842 17.7975 6.08514 17.907L6.79813 17.9723C7.57839 18.0437 7.73636 18.0742 7.85944 18.1446C7.98151 18.2144 8.08585 18.3329 8.53855 18.9671L8.95248 19.5469C9.64616 20.5187 10.2182 21.32 10.74 21.8553C11.2791 22.4083 11.9187 22.8384 12.743 22.7351C13.5531 22.6337 14.0878 22.0723 14.5134 21.4115C14.9307 20.7636 15.3538 19.842 15.8718 18.7137L16.1769 18.0494C16.2869 17.8098 16.3703 17.6316 16.4373 17.4962C16.4477 17.5079 16.4585 17.5194 16.4697 17.5306L20.4697 21.5306C20.7626 21.8235 21.2374 21.8235 21.5303 21.5306C21.8232 21.2377 21.8232 20.7628 21.5303 20.4699L17.6859 16.6255C17.7093 16.6173 17.7333 16.6088 17.758 16.6002L18.441 16.361C19.5975 15.956 20.5446 15.6243 21.2233 15.2696C21.9207 14.9051 22.5191 14.4235 22.7004 13.628C22.8839 12.8228 22.5399 12.1402 22.0536 11.5343C21.5851 10.9503 20.8613 10.2874 19.9839 9.48369L19.4604 9.00421C18.8892 8.48099 18.7829 8.36232 18.7259 8.22792C18.6669 8.08891 18.6544 7.91704 18.6665 7.12305L18.6775 6.39777C18.6962 5.17362 18.7114 4.1698 18.6122 3.41751C18.511 2.65005 18.2629 1.90987 17.544 1.49857ZM13.4178 4.29716C14.5467 3.62858 15.3217 3.17189 15.9146 2.92782C16.5071 2.68397 16.7019 2.74496 16.7991 2.80054C16.8936 2.8546 17.0424 2.98631 17.1251 3.61361C17.2081 4.24305 17.1965 5.13504 17.1767 6.43877L17.1647 7.21994C17.1547 7.82701 17.1461 8.34464 17.345 8.81361C17.5457 9.2869 17.9243 9.63271 18.3612 10.0316L18.4472 10.1103L18.9225 10.5456C19.8602 11.4046 20.4962 11.9901 20.8837 12.473C21.2622 12.9446 21.2697 13.1551 21.2379 13.2947C21.2039 13.444 21.0914 13.646 20.5285 13.9402C19.9583 14.2382 19.1162 14.5352 17.8865 14.9659L17.1539 15.2223C16.5752 15.4242 16.0914 15.593 15.7122 15.94C15.3345 16.2857 15.1202 16.7539 14.8621 17.3179L14.5343 18.0319C13.9843 19.2298 13.6047 20.0522 13.2524 20.5992C12.9015 21.1439 12.6931 21.2297 12.5566 21.2468C12.4345 21.2621 12.2378 21.2428 11.8141 20.8082C11.3826 20.3655 10.8767 19.6606 10.1351 18.6219L9.75937 18.0955L9.69049 17.9988C9.34758 17.5169 9.04872 17.0968 8.60434 16.8426C8.16092 16.5889 7.64642 16.5426 7.05381 16.4893L6.93485 16.4785L6.28697 16.4192C5.00882 16.3022 4.1387 16.2205 3.53516 16.0712C2.93966 15.924 2.82806 15.764 2.78306 15.6579C2.73296 15.5398 2.7003 15.3229 3.00168 14.748C3.30352 14.1721 3.83249 13.435 4.60438 12.364L5.06497 11.7251C5.42825 11.2224 5.73046 10.8041 5.844 10.3059C5.95834 9.80418 5.86331 9.30141 5.75053 8.70473L5.60821 7.94698C5.36962 6.67572 5.20841 5.80603 5.18383 5.16922C5.15965 4.54303 5.27846 4.34669 5.39353 4.24137C5.50449 4.13981 5.69883 4.04154 6.30151 4.1307C6.91831 4.22195 7.7488 4.4753 8.96802 4.85048L9.6966 5.0748C10.2652 5.25066 10.7542 5.40192 11.2649 5.33796C11.7704 5.27465 12.2154 5.01032 12.7422 4.69734L13.4178 4.29716Z" fill="#1C274C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
5
icons/svg/substore/stack.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.04582 10.8715C8.52718 10.6007 8.6979 9.99103 8.42713 9.50967C8.15637 9.02831 7.54665 8.85759 7.0653 9.12836L3.50974 11.1284C3.18725 11.3098 2.99128 11.6544 3.0003 12.0243C3.00931 12.3942 3.22184 12.7289 3.55279 12.8944L5.63258 13.9343L3.50974 15.1284C3.18725 15.3098 2.99128 15.6544 3.0003 16.0243C3.00931 16.3942 3.22184 16.7289 3.55279 16.8944L11.5528 20.8944C11.8343 21.0351 12.1657 21.0351 12.4472 20.8944L20.4472 16.8944C20.7782 16.7289 20.9907 16.3942 20.9997 16.0243C21.0087 15.6544 20.8128 15.3098 20.4903 15.1284L18.3674 13.9343L20.4472 12.8944C20.7782 12.7289 20.9907 12.3942 20.9997 12.0243C21.0087 11.6544 20.8128 11.3098 20.4903 11.1284L16.9347 9.12836C16.4533 8.85759 15.8436 9.02831 15.5729 9.50967C15.3021 9.99103 15.4728 10.6007 15.9542 10.8715L17.8679 11.948L12 14.8819L6.13213 11.948L8.04582 10.8715ZM16.2077 15.0141L12.4472 16.8944C12.1657 17.0351 11.8343 17.0351 11.5528 16.8944L7.7923 15.0141L6.13213 15.948L12 18.8819L17.8679 15.948L16.2077 15.0141Z" fill="#152C70"/>
|
||||
<path d="M12.4472 3.10557C12.1657 2.96481 11.8343 2.96481 11.5528 3.10557L3.55279 7.10557C3.214 7.27496 3 7.62123 3 8C3 8.37877 3.214 8.72504 3.55279 8.89443L11.5528 12.8944C11.8343 13.0352 12.1657 13.0352 12.4472 12.8944L20.4472 8.89443C20.786 8.72504 21 8.37877 21 8C21 7.62123 20.786 7.27496 20.4472 7.10557L12.4472 3.10557Z" fill="#4296FF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -51,3 +51,15 @@ proxies:
|
||||
h2: 430880481
|
||||
h3: 1214405368
|
||||
h4: 1739253821
|
||||
|
||||
- name: "cadian-adguard"
|
||||
type: socks5
|
||||
server: 192.168.0.3
|
||||
port: 1080
|
||||
username: adguard
|
||||
password: adguard
|
||||
# tls: true
|
||||
# fingerprint: xxxx
|
||||
# skip-cert-verify: true
|
||||
udp: true
|
||||
# ip-version: ipv6
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated on: 2025-12-02 09:51:50
|
||||
# VPN Key: vpn://vpn://AAAA_3icdY1PC4IwGIe_iuxcHYwKu4VQIB4Uieo0xlw2dH-cm6Hid2-bRKdOL7zPw--ZAEeMgGMAToyTkaIgU4RRw8AqACXpsKJSU8H_GFjwJ61gT1S3SKF9IknhAuxjAh1RPcUE6kH6EFpm1vI381WkElpg0XjtXTlkLLNrhms12Fv6CWXA7EJGv2CJNPIdl63J4HgbX8Yy2qebq8riXCZJURcoqW67_pynbSPDcHtoHtEdgXn-AEBTVs8=
|
||||
# Keenetic: interface Wireguard0 wireguard asc 4 10 50 110 20 211644422 533572853 237534087 1314455475
|
||||
|
||||
[Interface]
|
||||
Address = 100.70.184.183/32
|
||||
DNS = 8.8.8.8, 8.8.4.4
|
||||
PrivateKey = ZiA7zQ9osP4nEoB5jryU99gpHFg6cTprfiaA1nV/pks=
|
||||
Jc = 4
|
||||
Jmin = 10
|
||||
Jmax = 50
|
||||
S1 = 110
|
||||
S2 = 20
|
||||
H1 = 211644422
|
||||
H2 = 533572853
|
||||
H3 = 237534087
|
||||
H4 = 1314455475
|
||||
|
||||
[Peer]
|
||||
PublicKey = 5uUhd8S8MjnULEEAiSQysBi+JO6zfrXC0QIIsqsjUkc=
|
||||
PresharedKey = aJ4XS6+QgVasEcGP+5DInldM5i/YSR6PPmmix0wMtFA=
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
Endpoint = 5.189.202.78:46358
|
||||
PersistentKeepalive = 25
|
||||
@@ -1,20 +0,0 @@
|
||||
[Interface]
|
||||
PrivateKey = mJd8aibZI0iuQqPxpTdbs1Enb9+Ury7ido20lu2NrWo=
|
||||
Address = 10.136.82.167/32
|
||||
DNS = 1.1.1.1, 8.8.4.4
|
||||
MTU = 1380
|
||||
Jc = 43
|
||||
Jmin = 50
|
||||
Jmax = 70
|
||||
S1 = 110
|
||||
S2 = 120
|
||||
H1 = 1593635057
|
||||
H2 = 430880481
|
||||
H3 = 1214405368
|
||||
H4 = 1739253821
|
||||
[Peer]
|
||||
PublicKey = gbUPMNfaxgRSGD3xcnnbAJSclxfnOyh4U1qqmYMWmCI=
|
||||
PresharedKey = SCz82d6cfj9bfdlUyHwEBYC3u4T3znL6wFpJ7dMQYbM=
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
PersistentKeepalive = 25
|
||||
Endpoint = nl02awg.kcufwfgnkr.net:60136
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated on: 2025-12-02 09:35:19
|
||||
# VPN Key: vpn://vpn://AAAA_3icdY1PC4IwGIe_iuxcHYwKu4VQIB4Uieo0xlw2dH-cm6Hid2-bRKdOL7zPw--ZAEeMgGMAToyTkaIgU4RRw8AqACXpsKJSU8H_GFjwJ61gT1S3SKF9IknhAuxjAh1RPcUE6kH6EFpm1vI381WkElpg0XjtXTlkLLNrhms12Fv6CWXA7EJGv2CJNPIdl63J4HgbX8Yy2qebq8riXCZJURcoqW67_pynbSPDcHtoHtEdgXn-AEBTVs8=
|
||||
# Keenetic: interface Wireguard0 wireguard asc 2 10 50 18 122 1512494805 1147470590 1658720028 1826833034
|
||||
|
||||
[Interface]
|
||||
Address = 100.71.64.86/32
|
||||
DNS = 8.8.8.8, 8.8.4.4
|
||||
PrivateKey = /K20yRTTw44Ged/fXjdf4hSLnr33igHMl0SAXy0aMCE=
|
||||
Jc = 2
|
||||
Jmin = 10
|
||||
Jmax = 50
|
||||
S1 = 18
|
||||
S2 = 122
|
||||
H1 = 1512494805
|
||||
H2 = 1147470590
|
||||
H3 = 1658720028
|
||||
H4 = 1826833034
|
||||
|
||||
[Peer]
|
||||
PublicKey = Z01PDi29KeHvdtThNoSmXdxG4zyT1yFbulcI7AA5nQg=
|
||||
PresharedKey = uExSfwmdNaUTiJmyNNMuoDavJEKdrdsAqNaVcZ/U00Y=
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
Endpoint = 212.23.222.12:37089
|
||||
PersistentKeepalive = 25
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated on: 2025-12-02 09:46:50
|
||||
# VPN Key: vpn://vpn://AAAA_3icdY1PC4IwGIe_iuxcHYwKu4VQIB4Uieo0xlw2dH-cm6Hid2-bRKdOL7zPw--ZAEeMgGMAToyTkaIgU4RRw8AqACXpsKJSU8H_GFjwJ61gT1S3SKF9IknhAuxjAh1RPcUE6kH6EFpm1vI381WkElpg0XjtXTlkLLNrhms12Fv6CWXA7EJGv2CJNPIdl63J4HgbX8Yy2qebq8riXCZJURcoqW67_pynbSPDcHtoHtEdgXn-AEBTVs8=
|
||||
# Keenetic: interface Wireguard0 wireguard asc 3 10 50 145 34 203715079 914012290 174842657 1514769902
|
||||
|
||||
[Interface]
|
||||
Address = 100.71.64.155/32
|
||||
DNS = 8.8.8.8, 8.8.4.4
|
||||
PrivateKey = Vuk0huRtjGpA7KtWuQN/kJE0CRFOFs3JysN71nDFGqg=
|
||||
Jc = 3
|
||||
Jmin = 10
|
||||
Jmax = 50
|
||||
S1 = 145
|
||||
S2 = 34
|
||||
H1 = 203715079
|
||||
H2 = 914012290
|
||||
H3 = 174842657
|
||||
H4 = 1514769902
|
||||
|
||||
[Peer]
|
||||
PublicKey = ZqTlR9tYsMacuawQQaU6UCoXdT1exYJD2tzFXTpN9zs=
|
||||
PresharedKey = oOJNRLL+5aOgimi28Lwq18w6xCrry4AFLt68fhbUSlk=
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
Endpoint = 5.8.93.236:42854
|
||||
PersistentKeepalive = 25
|
||||
@@ -1,12 +0,0 @@
|
||||
ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpER1E3THlXRHBSOVFrU2ZvZi1zcnN3@151.243.101.40:2060#%E2%9A%A1%D0%92%D0%B5%D0%BB%D0%B8%D0%BA%D0%BE%D0%B1%D1%80%D0%B8%D1%82%D0%B0%D0%BD%D0%B8%D1%8F%20-%20GB_T074055
|
||||
vmess://eyJhZGQiOiAiMTUxLjI0My4xMDEuNDAiLCAiYWlkIjogIjAiLCAiaG9zdCI6ICJnb29nbGUuY29tIiwgImlkIjogImEzZTc1OTM2LTBlNDQtNGJjOS1iOTdlLWExZmRiMjc2Y2NhNCIsICJuZXQiOiAidGNwIiwgInBhdGgiOiAiLyIsICJwb3J0IjogODA4MSwgInBzIjogIlx1MjZhMVx1MDQxMlx1MDQzNVx1MDQzYlx1MDQzOFx1MDQzYVx1MDQzZVx1MDQzMVx1MDQ0MFx1MDQzOFx1MDQ0Mlx1MDQzMFx1MDQzZFx1MDQzOFx1MDQ0ZiAtIEdCX1QwNzQwNTUiLCAic2N5IjogImF1dG8iLCAidGxzIjogIm5vbmUiLCAidHlwZSI6ICJodHRwIiwgInYiOiAiMiJ9
|
||||
vmess://eyJhZGQiOiAiMTUxLjI0My4xMDEuNDAiLCAiYWlkIjogIjAiLCAiaG9zdCI6ICJnb29nbGUuY29tIiwgImlkIjogImEzZTc1OTM2LTBlNDQtNGJjOS1iOTdlLWExZmRiMjc2Y2NhNCIsICJuZXQiOiAid3MiLCAicGF0aCI6ICIvIiwgInBvcnQiOiA4MDgwLCAicHMiOiAiXHUyNmExXHUwNDEyXHUwNDM1XHUwNDNiXHUwNDM4XHUwNDNhXHUwNDNlXHUwNDMxXHUwNDQwXHUwNDM4XHUwNDQyXHUwNDMwXHUwNDNkXHUwNDM4XHUwNDRmIC0gR0JfVDA3NDA1NSIsICJzY3kiOiAiYXV0byIsICJ0bHMiOiAibm9uZSIsICJ0eXBlIjogIiIsICJ2IjogIjIifQ==
|
||||
vless://ca1a269d-6409-4bbf-a4e2-1e9cb04490e2@151.243.101.40:8443?security=reality&type=tcp&headerType=&path=&host=&sni=discordapp.com&fp=chrome&pbk=SbVKOEMjK0sIlbwg4akyBg5mL5KZwwB-ed4eEE7YnRc&sid=6ba85179e30d4fc2#%E2%9A%A1%D0%92%D0%B5%D0%BB%D0%B8%D0%BA%D0%BE%D0%B1%D1%80%D0%B8%D1%82%D0%B0%D0%BD%D0%B8%D1%8F%20-%20GB_T074055
|
||||
vless://ca1a269d-6409-4bbf-a4e2-1e9cb04490e2@151.243.101.40:2053?security=reality&type=grpc&headerType=&serviceName=xyz&authority=&mode=gun&sni=discordapp.com&fp=chrome&pbk=SbVKOEMjK0sIlbwg4akyBg5mL5KZwwB-ed4eEE7YnRc&sid=#%E2%9A%A1%D0%92%D0%B5%D0%BB%D0%B8%D0%BA%D0%BE%D0%B1%D1%80%D0%B8%D1%82%D0%B0%D0%BD%D0%B8%D1%8F%20-%20GB_T074055
|
||||
trojan://TEx16RQixr5cJ3rCBz1Ddw@151.243.101.40:2058?security=tls&type=ws&headerType=&path=%2F&host=&sni=uk.hydranet.space&fp=#%E2%9A%A1%D0%92%D0%B5%D0%BB%D0%B8%D0%BA%D0%BE%D0%B1%D1%80%D0%B8%D1%82%D0%B0%D0%BD%D0%B8%D1%8F%20-%20GB_T074055
|
||||
ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTp2aVluTUlnYW83TjlIbVlCWHV5ZnRB@109.235.48.181:2060#%E2%9A%A1%D0%9D%D0%B8%D0%B4%D0%B5%D1%80%D0%BB%D0%B0%D0%BD%D0%B4%D1%8B%20-%20NL_T078756
|
||||
vmess://eyJhZGQiOiAiMTA5LjIzNS40OC4xODEiLCAiYWlkIjogIjAiLCAiaG9zdCI6ICJnb29nbGUuY29tIiwgImlkIjogIjM1Y2EyMDY5LWFmYmYtNDkwMi04ZWIwLWViYjUxZTUwYjNiMyIsICJuZXQiOiAidGNwIiwgInBhdGgiOiAiLyIsICJwb3J0IjogODA4MSwgInBzIjogIlx1MjZhMVx1MDQxZFx1MDQzOFx1MDQzNFx1MDQzNVx1MDQ0MFx1MDQzYlx1MDQzMFx1MDQzZFx1MDQzNFx1MDQ0YiAtIE5MX1QwNzg3NTYiLCAic2N5IjogImF1dG8iLCAidGxzIjogIm5vbmUiLCAidHlwZSI6ICJodHRwIiwgInYiOiAiMiJ9
|
||||
vmess://eyJhZGQiOiAiMTA5LjIzNS40OC4xODEiLCAiYWlkIjogIjAiLCAiaG9zdCI6ICJnb29nbGUuY29tIiwgImlkIjogIjM1Y2EyMDY5LWFmYmYtNDkwMi04ZWIwLWViYjUxZTUwYjNiMyIsICJuZXQiOiAid3MiLCAicGF0aCI6ICIvIiwgInBvcnQiOiA4MDgwLCAicHMiOiAiXHUyNmExXHUwNDFkXHUwNDM4XHUwNDM0XHUwNDM1XHUwNDQwXHUwNDNiXHUwNDMwXHUwNDNkXHUwNDM0XHUwNDRiIC0gTkxfVDA3ODc1NiIsICJzY3kiOiAiYXV0byIsICJ0bHMiOiAibm9uZSIsICJ0eXBlIjogIiIsICJ2IjogIjIifQ==
|
||||
vless://b9ae9a97-18da-4c5d-9ada-3b0a92d4813e@109.235.48.181:8443?security=reality&type=tcp&headerType=&path=&host=&sni=vkvideo.ru&fp=chrome&pbk=SbVKOEMjK0sIlbwg4akyBg5mL5KZwwB-ed4eEE7YnRc&sid=6ba85179e30d4fc2#%E2%9A%A1%D0%9D%D0%B8%D0%B4%D0%B5%D1%80%D0%BB%D0%B0%D0%BD%D0%B4%D1%8B%20-%20NL_T078756
|
||||
vless://b9ae9a97-18da-4c5d-9ada-3b0a92d4813e@109.235.48.181:2053?security=reality&type=grpc&headerType=&serviceName=xyz&authority=&mode=gun&sni=vkvideo.ru&fp=chrome&pbk=SbVKOEMjK0sIlbwg4akyBg5mL5KZwwB-ed4eEE7YnRc&sid=#%E2%9A%A1%D0%9D%D0%B8%D0%B4%D0%B5%D1%80%D0%BB%D0%B0%D0%BD%D0%B4%D1%8B%20-%20NL_T078756
|
||||
trojan://tbPsjmK8x6xSDW1vTO8V5g@109.235.48.181:2058?security=tls&type=ws&headerType=&path=%2F&host=&sni=nl2.hydranet.space&fp=#%E2%9A%A1%D0%9D%D0%B8%D0%B4%D0%B5%D1%80%D0%BB%D0%B0%D0%BD%D0%B4%D1%8B%20-%20NL_T078756
|
||||
@@ -0,0 +1,3 @@
|
||||
payload:
|
||||
- SRC-IP-CIDR,192.168.10.103/32
|
||||
- SRC-IP-CIDR,192.168.10.86/32
|
||||
@@ -1,4 +1,5 @@
|
||||
payload:
|
||||
- SRC-IP-CIDR,192.168.10.203/32
|
||||
- SRC-IP-CIDR,192.168.10.204/32
|
||||
|
||||
- SRC-IP-CIDR,100.98.138.18/32 # DTS-TAB-S7-NET
|
||||
- SRC-IP-CIDR,192.168.0.101/32 # DTS-TAB-S7-NET
|
||||
@@ -5,3 +5,7 @@ payload:
|
||||
- GEOSITE,category-novel
|
||||
|
||||
- DOMAIN-SUFFIX,libgen.li
|
||||
|
||||
- DOMAIN-SUFFIX,gaminik.net
|
||||
- DOMAIN-SUFFIX,gaminik.cn
|
||||
- DOMAIN-SUFFIX,langbag.com
|
||||
@@ -66,3 +66,8 @@ payload:
|
||||
|
||||
- DOMAIN-SUFFIX,pervertium.com
|
||||
- DOMAIN-SUFFIX,tubesafari.com
|
||||
|
||||
- DOMAIN-SUFFIX,growcdnssedge.com
|
||||
- DOMAIN-SUFFIX,flixcdn.com
|
||||
- DOMAIN-SUFFIX,noodlemagazine.com
|
||||
- DOMAIN-SUFFIX,pvvstream.pro
|
||||
@@ -1,7 +1,5 @@
|
||||
payload:
|
||||
- DOMAIN-SUFFIX,globus.ru
|
||||
- DOMAIN-SUFFIX,samokat.ru
|
||||
- DOMAIN-SUFFIX,smartmed.pro
|
||||
- DOMAIN-SUFFIX,megamarket.ru
|
||||
- DOMAIN-SUFFIX,anextel.ru
|
||||
- DOMAIN-SUFFIX,2gis.com
|
||||
- DOMAIN-SUFFIX,rg.ru
|
||||