diff --git a/config-sub-converter/scripts/external-proxies-sanitizer.js b/config-sub-converter/scripts/external-proxies-sanitizer.js index 0a6f21c..1ef4775 100644 --- a/config-sub-converter/scripts/external-proxies-sanitizer.js +++ b/config-sub-converter/scripts/external-proxies-sanitizer.js @@ -43,20 +43,20 @@ const NOISE_PATTERNS = [ // 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: /\bโšก๏ธLow Ping\b/gi, icon: "โšก๏ธ" }, - { regex: /\bโšก๏ธ10 Gbit \b/gi, icon: "๐Ÿ›ค๏ธ" }, - { regex: /\bYT\b|\bRussia\b|\bะ ะพััะธั\b/gi, icon: "๐Ÿ“บ" }, - { regex: /\bIPv6\b/gi, icon: "6๏ธโƒฃ" }, - { regex: /\bGemini\b|\bAI Studio\b/gi, icon: "๐Ÿค–" }, - { regex: /\bTorrentโœ…\b|Torrent|\bP2P\b|\bP2P-Torrents\b/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: "๐Ÿงฒ" } ]; // 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: "๐ŸŒ" } + { 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) @@ -132,30 +132,114 @@ const ISO2_TO_ISO3 = { // 6) Protocol icons (based on proxy.type) const PROTOCOL_ICONS = { - ss: "๐Ÿ”’", - ssr: "โ˜‚๏ธ", - vmess: "๐Ÿช", - vless: "๐ŸŒŒ", - trojan: "๐ŸŽ", - http: "๐ŸŒ", - socks5: "๐Ÿงฆ", - snell: "๐ŸŒ", - wireguard: "๐Ÿฒ", - hysteria: "๐Ÿคช", - hysteria2: "โšก", - tuic: "๐Ÿš…" + ss: "", + ssr: "", + vmess: "", + vless: "", + trojan: "", + http: "", + socks5: "", + snell: "", + wireguard: "", + hysteria: "", + hysteria2: "", + tuic: "" +}; + +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: 5, + padChar: "0", + superscripts: { + "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 portToSuperscript(port) { + let p = String(port ?? "").trim(); + + // keep only digits, because providers love putting garbage here + p = p.replace(/[^\d]/g, ""); + if (!p) p = "0"; + + // 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.superscripts[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 = portToSuperscript(port); + + 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}$/); @@ -253,7 +337,6 @@ function detectCountryByName(name) { if (n.includes("๐Ÿ‡บ๐Ÿ‡ธ")) return { iso3: "USA", flag: "๐Ÿ‡บ๐Ÿ‡ธ", priority: 1, source: "flag" }; if (n.includes("๐Ÿ‡ป๐Ÿ‡ณ")) return { iso3: "VNM", flag: "๐Ÿ‡ป๐Ÿ‡ณ", priority: 1, 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" }; @@ -316,13 +399,10 @@ function operator(proxies, targetPlatform, utils) { const country = byName || byGeo || { iso3: "UNK", flag: "๐Ÿดโ€โ˜ ๏ธ", priority: 9999, source: "fallback" }; // 4) Protocol icon (based on type) - const proto = PROTOCOL_ICONS[(proxy && proxy.type) || ""] || "๐Ÿ”Œ"; + const proto = PROTOCOL_ICONS[(proxy && proxy.type) || ""] || PROTOCOL_ICON_DEFAULT; // 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}`; + const metaTag = buildMetaTag(proxy); // 6) Prepare bucket key const key = country.iso3; @@ -382,7 +462,7 @@ function operator(proxies, targetPlatform, utils) { // Final name format: // ๐Ÿ‡ฉ๐Ÿ‡ช DEU-03 ๐ŸŒŒ ๐Ÿ“บ โป โ–ซ๏ธws/vless/443 - p.name = `${group.country.flag} ${group.country.iso3}-${num} ${item._meta.proto}${tagStr} ${item._meta.metaTag}${debugSuffix}` + p.name = `${group.country.flag} ${group.country.iso3}-${num} ${item._meta.metaTag} ${item._meta.proto}${tagStr} ${debugSuffix}` .replace(/\s+/g, " ") .trim();