Add external proxies sanitizer script for proxy normalization and tagging

This commit is contained in:
2026-02-18 22:22:39 +03:00
parent d5faeb5cee
commit 18214ed03d

View File

@@ -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;
}