/** * 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: "6๏ธโƒฃ" }, { regex: uWordBoundaryGroup("Gemini|AI Studio"), icon: "๐Ÿค–" }, { regex: uWordBoundaryGroup("Torrent|P2P|P2P-Torrents"), icon: "๐Ÿงฒ" }, { regex: uWordBoundaryGroup("xfizz|x-fizz"), icon: "โ“•" }, { regex: uWordBoundaryGroup("unicade|uncd"), icon: "โ“ค" }, { regex: uWordBoundaryGroup("vzdh|vezdehod"), icon: "โ““" }, { regex: uWordBoundaryGroup("dvpn|d-vpn"), 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; }