Add Sub-Store Advanced Batch Processing Script for proxy normalization and tagging

This commit is contained in:
2026-01-04 19:00:21 +03:00
parent 5c8851a292
commit b7f2adba40
2 changed files with 666 additions and 0 deletions

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