/** * 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; }