Add Sub-Store Advanced Batch Processing Script for proxy normalization and tagging
This commit is contained in:
328
config-sub-converter/scripts/demo/gemeni-tried-orginal.js
Normal file
328
config-sub-converter/scripts/demo/gemeni-tried-orginal.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
338
config-sub-converter/scripts/external-proxies-sanitizer.js
Normal file
338
config-sub-converter/scripts/external-proxies-sanitizer.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user