From 18214ed03dcd5b0a28f687ed79aac4d54f068d2a Mon Sep 17 00:00:00 2001 From: DaTekShaman Date: Wed, 18 Feb 2026 22:22:39 +0300 Subject: [PATCH] Add external proxies sanitizer script for proxy normalization and tagging --- .../scripts/external-proxies-sanitizer-dev.js | 517 ++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 config-sub-converter/scripts/external-proxies-sanitizer-dev.js diff --git a/config-sub-converter/scripts/external-proxies-sanitizer-dev.js b/config-sub-converter/scripts/external-proxies-sanitizer-dev.js new file mode 100644 index 0000000..7adb7d8 --- /dev/null +++ b/config-sub-converter/scripts/external-proxies-sanitizer-dev.js @@ -0,0 +1,517 @@ +/** + * 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. + */ + +// --- 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 { + appendOriginalName: asBool(args['append-original'], false), + }; +} + +/////////////////////// +// 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 opts = normalizeOptions(); + + 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 = opts.appendOriginalName + ? ` โŸฆ${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; +}