From b7f2adba40d6b634f4755952e4588d203026e6e0 Mon Sep 17 00:00:00 2001 From: DaTekShaman Date: Sun, 4 Jan 2026 19:00:21 +0300 Subject: [PATCH] Add Sub-Store Advanced Batch Processing Script for proxy normalization and tagging --- .../scripts/demo/gemeni-tried-orginal.js | 328 +++++++++++++++++ .../scripts/external-proxies-sanitizer.js | 338 ++++++++++++++++++ 2 files changed, 666 insertions(+) create mode 100644 config-sub-converter/scripts/demo/gemeni-tried-orginal.js create mode 100644 config-sub-converter/scripts/external-proxies-sanitizer.js diff --git a/config-sub-converter/scripts/demo/gemeni-tried-orginal.js b/config-sub-converter/scripts/demo/gemeni-tried-orginal.js new file mode 100644 index 0000000..ca168c9 --- /dev/null +++ b/config-sub-converter/scripts/demo/gemeni-tried-orginal.js @@ -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; +} diff --git a/config-sub-converter/scripts/external-proxies-sanitizer.js b/config-sub-converter/scripts/external-proxies-sanitizer.js new file mode 100644 index 0000000..a74a56e --- /dev/null +++ b/config-sub-converter/scripts/external-proxies-sanitizer.js @@ -0,0 +1,338 @@ +/** + * 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 = true; // 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, // 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: { + 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(" ")}` : ""; + + // 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}` + .replace(/\s+/g, " ") + .trim(); + + result.push(p); + } + } + + return result; +}