494 lines
24 KiB
JavaScript
494 lines
24 KiB
JavaScript
/**
|
||
* 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,
|
||
/\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: 1, source: "flag" };
|
||
if (n.includes("🇦🇹")) return { iso3: "AUT", flag: "🇦🇹", priority: 1, source: "flag" };
|
||
if (n.includes("🇦🇺")) return { iso3: "AUS", flag: "🇦🇺", priority: 1, source: "flag" };
|
||
if (n.includes("🇧🇬")) return { iso3: "BGR", flag: "🇧🇬", priority: 1, source: "flag" };
|
||
if (n.includes("🇧🇾")) return { iso3: "BLR", flag: "🇧🇾", priority: 1, source: "flag" };
|
||
if (n.includes("🇧🇷")) return { iso3: "BRA", flag: "🇧🇷", priority: 1, source: "flag" };
|
||
if (n.includes("🇨🇦")) return { iso3: "CAN", flag: "🇨🇦", priority: 1, source: "flag" };
|
||
if (n.includes("🇨🇭")) return { iso3: "CHE", flag: "🇨🇭", priority: 1, source: "flag" };
|
||
if (n.includes("🇨🇳")) return { iso3: "CHN", flag: "🇨🇳", priority: 1, source: "flag" };
|
||
if (n.includes("🇨🇿")) return { iso3: "CZE", flag: "🇨🇿", priority: 1, source: "flag" };
|
||
if (n.includes("🇩🇪")) return { iso3: "DEU", flag: "🇩🇪", priority: 1, source: "flag" };
|
||
if (n.includes("🇩🇰")) return { iso3: "DNK", flag: "🇩🇰", priority: 1, source: "flag" };
|
||
if (n.includes("🇪🇪")) return { iso3: "EST", flag: "🇪🇪", priority: 1, source: "flag" };
|
||
if (n.includes("🇪🇬")) return { iso3: "EGY", flag: "🇪🇬", priority: 1, source: "flag" };
|
||
if (n.includes("🇪🇸")) return { iso3: "ESP", flag: "🇪🇸", priority: 1, source: "flag" };
|
||
if (n.includes("🇫🇮")) return { iso3: "FIN", flag: "🇫🇮", priority: 1, source: "flag" };
|
||
if (n.includes("🇫🇷")) return { iso3: "FRA", flag: "🇫🇷", priority: 1, source: "flag" };
|
||
if (n.includes("🇬🇧")) return { iso3: "GBR", flag: "🇬🇧", priority: 1, source: "flag" };
|
||
if (n.includes("🇬🇪")) return { iso3: "GEO", flag: "🇬🇪", priority: 1, source: "flag" };
|
||
if (n.includes("🇭🇰")) return { iso3: "HKG", flag: "🇭🇰", priority: 1, source: "flag" };
|
||
if (n.includes("🇮🇪")) return { iso3: "IRL", flag: "🇮🇪", priority: 1, source: "flag" };
|
||
if (n.includes("🇮🇱")) return { iso3: "ISR", flag: "🇮🇱", priority: 1, source: "flag" };
|
||
if (n.includes("🇮🇳")) return { iso3: "IND", flag: "🇮🇳", priority: 1, source: "flag" };
|
||
if (n.includes("🇮🇹")) return { iso3: "ITA", flag: "🇮🇹", priority: 1, source: "flag" };
|
||
if (n.includes("🇯🇵")) return { iso3: "JPN", flag: "🇯🇵", priority: 1, source: "flag" };
|
||
if (n.includes("🇰🇷")) return { iso3: "KOR", flag: "🇰🇷", priority: 1, source: "flag" };
|
||
if (n.includes("🇰🇿")) return { iso3: "KAZ", flag: "🇰🇿", priority: 1, source: "flag" };
|
||
if (n.includes("🇱🇹")) return { iso3: "LTU", flag: "🇱🇹", priority: 1, source: "flag" };
|
||
if (n.includes("🇱🇻")) return { iso3: "LVA", flag: "🇱🇻", priority: 1, source: "flag" };
|
||
if (n.includes("🇲🇩")) return { iso3: "MDA", flag: "🇲🇩", priority: 1, source: "flag" };
|
||
if (n.includes("🇲🇾")) return { iso3: "MYS", flag: "🇲🇾", priority: 1, source: "flag" };
|
||
if (n.includes("🇳🇬")) return { iso3: "NGA", flag: "🇳🇬", priority: 1, source: "flag" };
|
||
if (n.includes("🇳🇱")) return { iso3: "NLD", flag: "🇳🇱", priority: 1, source: "flag" };
|
||
if (n.includes("🇳🇴")) return { iso3: "NOR", flag: "🇳🇴", priority: 1, source: "flag" };
|
||
if (n.includes("🇵🇭")) return { iso3: "PHL", flag: "🇵🇭", priority: 1, source: "flag" };
|
||
if (n.includes("🇵🇱")) return { iso3: "POL", flag: "🇵🇱", priority: 1, source: "flag" };
|
||
if (n.includes("🇵🇹")) return { iso3: "PRT", flag: "🇵🇹", priority: 1, source: "flag" };
|
||
if (n.includes("🇷🇴")) return { iso3: "ROU", flag: "🇷🇴", priority: 1, source: "flag" };
|
||
if (n.includes("🇷🇺")) return { iso3: "RUS", flag: "🇷🇺", priority: 1, source: "flag" };
|
||
if (n.includes("🇸🇪")) return { iso3: "SWE", flag: "🇸🇪", priority: 1, source: "flag" };
|
||
if (n.includes("🇸🇬")) return { iso3: "SGP", flag: "🇸🇬", priority: 1, source: "flag" };
|
||
if (n.includes("🇹🇭")) return { iso3: "THA", flag: "🇹🇭", priority: 1, source: "flag" };
|
||
if (n.includes("🇹🇷")) return { iso3: "TUR", flag: "🇹🇷", priority: 1, source: "flag" };
|
||
if (n.includes("🇹🇼")) return { iso3: "TWN", flag: "🇹🇼", priority: 1, source: "flag" };
|
||
if (n.includes("🇺🇸")) return { iso3: "USA", flag: "🇺🇸", priority: 1, source: "flag" };
|
||
if (n.includes("🇻🇳")) return { iso3: "VNM", flag: "🇻🇳", priority: 1, 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;
|
||
}
|