zapret-kvn/xray_fluent/country_flags.py
2026-03-19 00:30:27 +03:00

402 lines
18 KiB
Python

"""Country detection from node name/server and flag icon generation."""
from __future__ import annotations
import json
import re
import socket
import urllib.request
from .http_utils import urlopen as _urlopen
from PyQt6.QtCore import QRectF, QThread, Qt, pyqtSignal
from PyQt6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPixmap
# ── Flag stripe data ────────────────────────────────────────────
# (orientation, [colors]) h = horizontal top→bottom, v = vertical left→right
_W, _H = 20, 14
_STRIPES: dict[str, tuple[str, list[str]]] = {
# Horizontal tri-stripes
"RU": ("h", ["#FFFFFF", "#0039A6", "#D52B1E"]),
"DE": ("h", ["#000000", "#DD0000", "#FFCC00"]),
"NL": ("h", ["#AE1C28", "#FFFFFF", "#21468B"]),
"LU": ("h", ["#ED2939", "#FFFFFF", "#00A1DE"]),
"AT": ("h", ["#ED2939", "#FFFFFF", "#ED2939"]),
"HU": ("h", ["#CE2939", "#FFFFFF", "#477050"]),
"BG": ("h", ["#FFFFFF", "#00966E", "#D62612"]),
"LT": ("h", ["#FDB913", "#006A44", "#C1272D"]),
"EE": ("h", ["#0072CE", "#000000", "#FFFFFF"]),
"LV": ("h", ["#9E3039", "#FFFFFF", "#9E3039"]),
"HR": ("h", ["#FF0000", "#FFFFFF", "#171796"]),
"RS": ("h", ["#C7363C", "#0C4076", "#FFFFFF"]),
"SI": ("h", ["#FFFFFF", "#003DA5", "#ED1C24"]),
"AM": ("h", ["#D90012", "#0033A0", "#F2A800"]),
"AZ": ("h", ["#00B5E2", "#DD0000", "#00B532"]),
"CO": ("h", ["#FCD116", "#003893", "#CE1126"]),
"AR": ("h", ["#74ACDF", "#FFFFFF", "#74ACDF"]),
"IN": ("h", ["#FF9933", "#FFFFFF", "#138808"]),
"EG": ("h", ["#CE1126", "#FFFFFF", "#000000"]),
"IR": ("h", ["#239F40", "#FFFFFF", "#DA0000"]),
"BO": ("h", ["#D52B1E", "#F9E300", "#007934"]),
"ET": ("h", ["#009A44", "#FCDD09", "#DA121A"]),
"YE": ("h", ["#CE1126", "#FFFFFF", "#000000"]),
"IQ": ("h", ["#CE1126", "#FFFFFF", "#000000"]),
"TJ": ("h", ["#CC0000", "#FFFFFF", "#006600"]),
"GH": ("h", ["#CF0921", "#FCD116", "#006B3F"]),
# Horizontal bi-stripes
"UA": ("h", ["#005BBB", "#FFD500"]),
"PL": ("h", ["#FFFFFF", "#DC143C"]),
"ID": ("h", ["#FF0000", "#FFFFFF"]),
"MC": ("h", ["#CE1126", "#FFFFFF"]),
"SG": ("h", ["#EF3340", "#FFFFFF"]),
# Vertical tri-stripes
"FR": ("v", ["#002395", "#FFFFFF", "#ED2939"]),
"IT": ("v", ["#009246", "#FFFFFF", "#CE2B37"]),
"IE": ("v", ["#009A49", "#FFFFFF", "#FF7900"]),
"BE": ("v", ["#000000", "#FAE042", "#ED2939"]),
"RO": ("v", ["#002B7F", "#FCD116", "#CE1126"]),
"MD": ("v", ["#003DA5", "#FCD116", "#CC0000"]),
"MX": ("v", ["#006847", "#FFFFFF", "#CE1126"]),
"NG": ("v", ["#008751", "#FFFFFF", "#008751"]),
"CI": ("v", ["#F77F00", "#FFFFFF", "#009E60"]),
"PE": ("v", ["#D91023", "#FFFFFF", "#D91023"]),
"CA": ("v", ["#FF0000", "#FFFFFF", "#FF0000"]),
"PT": ("v", ["#006600", "#FF0000", "#FF0000"]),
# Cross-flags approximated as 3 stripes
"SE": ("h", ["#006AA7", "#FECC02", "#006AA7"]),
"NO": ("h", ["#EF2B2D", "#002868", "#EF2B2D"]),
"FI": ("h", ["#FFFFFF", "#003580", "#FFFFFF"]),
"DK": ("h", ["#C60C30", "#FFFFFF", "#C60C30"]),
"IS": ("h", ["#003897", "#FFFFFF", "#D72828"]),
"CH": ("h", ["#FF0000", "#FFFFFF", "#FF0000"]),
"GR": ("h", ["#0D5EAF", "#FFFFFF", "#0D5EAF"]),
"GE": ("h", ["#FFFFFF", "#FF0000", "#FFFFFF"]),
# Complex flags simplified
"US": ("h", ["#3C3B6E", "#FFFFFF", "#B22234"]),
"GB": ("h", ["#012169", "#FFFFFF", "#C8102E"]),
"JP": ("h", ["#FFFFFF", "#BC002D", "#FFFFFF"]),
"CN": ("h", ["#DE2910", "#DE2910", "#FFDE00"]),
"TW": ("h", ["#000095", "#FE0000", "#FE0000"]),
"HK": ("h", ["#DE2910", "#FFFFFF", "#DE2910"]),
"KR": ("h", ["#FFFFFF", "#CD2E3A", "#003478"]),
"TR": ("h", ["#E30A17", "#FFFFFF", "#E30A17"]),
"IL": ("h", ["#FFFFFF", "#0038B8", "#FFFFFF"]),
"BR": ("h", ["#009C3B", "#FFDF00", "#009C3B"]),
"AU": ("h", ["#012169", "#FFFFFF", "#012169"]),
"NZ": ("h", ["#00247D", "#CC142B", "#00247D"]),
"ZA": ("h", ["#007749", "#FFB81C", "#DE3831"]),
"KE": ("h", ["#000000", "#BB0000", "#006600"]),
"KZ": ("h", ["#00AFCA", "#FFD700", "#00AFCA"]),
"UZ": ("h", ["#1EB53A", "#FFFFFF", "#0099B5"]),
"VN": ("h", ["#DA251D", "#FFCD00", "#DA251D"]),
"SA": ("h", ["#006C35", "#FFFFFF", "#006C35"]),
"AE": ("h", ["#00732F", "#FFFFFF", "#000000"]),
"QA": ("h", ["#8A1538", "#FFFFFF", "#8A1538"]),
"MY": ("h", ["#010066", "#CC0001", "#FFFFFF"]),
"TH": ("h", ["#A51931", "#F4F5F8", "#2D2A4A"]),
"PH": ("h", ["#0038A8", "#FFFFFF", "#CE1126"]),
"MM": ("h", ["#FECB00", "#34B233", "#EA2839"]),
"BD": ("h", ["#006A4E", "#F42A41", "#006A4E"]),
"PK": ("h", ["#01411C", "#FFFFFF", "#01411C"]),
"CL": ("h", ["#FFFFFF", "#0039A6", "#D52B1E"]),
"PA": ("h", ["#FFFFFF", "#DA121A", "#003DA5"]),
"CU": ("h", ["#002A8F", "#FFFFFF", "#CF142B"]),
"ES": ("h", ["#AA151B", "#F1BF00", "#AA151B"]),
"CZ": ("h", ["#FFFFFF", "#11457E", "#D7141A"]),
"SK": ("h", ["#FFFFFF", "#0B4EA2", "#EE1C25"]),
"BA": ("h", ["#002395", "#FECB00", "#002395"]),
"AL": ("h", ["#E41E20", "#000000", "#E41E20"]),
"MK": ("h", ["#D20000", "#FFE600", "#D20000"]),
"ME": ("h", ["#C40308", "#D4AF37", "#C40308"]),
"MA": ("h", ["#C1272D", "#006233", "#C1272D"]),
"TN": ("h", ["#E70013", "#FFFFFF", "#E70013"]),
"DZ": ("h", ["#006233", "#FFFFFF", "#D21034"]),
}
# ── Icon cache & rendering ──────────────────────────────────────
_icon_cache: dict[str, QIcon] = {}
def get_flag_icon(code: str) -> QIcon | None:
if not code:
return None
code = code.upper()
cached = _icon_cache.get(code)
if cached is not None:
return cached
pm = _draw_flag(code)
icon = QIcon(pm)
_icon_cache[code] = icon
return icon
def _draw_flag(code: str) -> QPixmap:
pm = QPixmap(_W, _H)
pm.fill(QColor(0, 0, 0, 0))
p = QPainter(pm)
p.setRenderHint(QPainter.RenderHint.Antialiasing)
path = QPainterPath()
path.addRoundedRect(QRectF(0, 0, _W, _H), 2, 2)
p.setClipPath(path)
data = _STRIPES.get(code)
if data:
orient, colors = data
if orient == "h":
sh = _H / len(colors)
for i, c in enumerate(colors):
p.fillRect(QRectF(0, i * sh, _W, sh + 0.5), QColor(c))
else:
sw = _W / len(colors)
for i, c in enumerate(colors):
p.fillRect(QRectF(i * sw, 0, sw + 0.5, _H), QColor(c))
else:
p.fillRect(QRectF(0, 0, _W, _H), QColor("#B0BEC5"))
p.setClipping(False)
p.setPen(QColor(0, 0, 0, 30))
p.setBrush(Qt.BrushStyle.NoBrush)
p.drawRoundedRect(QRectF(0.5, 0.5, _W - 1, _H - 1), 2, 2)
p.end()
return pm
# ── Country detection ───────────────────────────────────────────
def detect_country(name: str, server: str) -> str:
return (
_detect_emoji(name)
or _detect_name(name)
or _detect_code(name)
or _detect_server(server)
or ""
)
def _detect_emoji(text: str) -> str:
codepoints = [ord(c) for c in text]
for i in range(len(codepoints) - 1):
a, b = codepoints[i], codepoints[i + 1]
if 0x1F1E6 <= a <= 0x1F1FF and 0x1F1E6 <= b <= 0x1F1FF:
return chr(a - 0x1F1E6 + ord("A")) + chr(b - 0x1F1E6 + ord("A"))
return ""
# Sorted longest-first so multi-word names match before shorter ones
_NAMES: list[tuple[str, str]] = sorted(
[
# English country names
("united states", "US"), ("usa", "US"), ("america", "US"),
("russia", "RU"), ("germany", "DE"), ("france", "FR"),
("united kingdom", "GB"), ("great britain", "GB"), ("england", "GB"),
("japan", "JP"), ("china", "CN"), ("south korea", "KR"), ("korea", "KR"),
("taiwan", "TW"), ("hong kong", "HK"), ("singapore", "SG"),
("netherlands", "NL"), ("holland", "NL"),
("italy", "IT"), ("spain", "ES"), ("portugal", "PT"),
("ireland", "IE"), ("belgium", "BE"), ("austria", "AT"),
("switzerland", "CH"), ("poland", "PL"), ("czech", "CZ"), ("czechia", "CZ"),
("slovakia", "SK"), ("hungary", "HU"), ("romania", "RO"), ("bulgaria", "BG"),
("croatia", "HR"), ("serbia", "RS"), ("greece", "GR"), ("turkey", "TR"),
("turkiye", "TR"), ("ukraine", "UA"), ("moldova", "MD"), ("georgia", "GE"),
("armenia", "AM"), ("azerbaijan", "AZ"), ("kazakhstan", "KZ"),
("uzbekistan", "UZ"), ("sweden", "SE"), ("norway", "NO"),
("finland", "FI"), ("denmark", "DK"), ("iceland", "IS"),
("estonia", "EE"), ("latvia", "LV"), ("lithuania", "LT"),
("luxembourg", "LU"), ("albania", "AL"), ("north macedonia", "MK"),
("bosnia", "BA"), ("montenegro", "ME"), ("slovenia", "SI"),
("israel", "IL"), ("india", "IN"), ("thailand", "TH"),
("vietnam", "VN"), ("indonesia", "ID"), ("malaysia", "MY"),
("philippines", "PH"), ("brazil", "BR"), ("argentina", "AR"),
("chile", "CL"), ("colombia", "CO"), ("mexico", "MX"),
("canada", "CA"), ("australia", "AU"), ("new zealand", "NZ"),
("south africa", "ZA"), ("nigeria", "NG"), ("kenya", "KE"),
("egypt", "EG"), ("uae", "AE"), ("emirates", "AE"),
("saudi arabia", "SA"), ("panama", "PA"), ("iran", "IR"),
("iraq", "IQ"), ("pakistan", "PK"), ("bangladesh", "BD"),
("cambodia", "KH"), ("myanmar", "MM"), ("mongolia", "MN"),
# Russian country names
("россия", "RU"), ("германия", "DE"), ("франция", "FR"),
("великобритания", "GB"), ("англия", "GB"), ("япония", "JP"),
("китай", "CN"), ("корея", "KR"), ("тайвань", "TW"),
("гонконг", "HK"), ("сингапур", "SG"), ("нидерланды", "NL"),
("голландия", "NL"), ("италия", "IT"), ("испания", "ES"),
("португалия", "PT"), ("ирландия", "IE"), ("бельгия", "BE"),
("австрия", "AT"), ("швейцария", "CH"), ("польша", "PL"),
("чехия", "CZ"), ("словакия", "SK"), ("венгрия", "HU"),
("румыния", "RO"), ("болгария", "BG"), ("хорватия", "HR"),
("сербия", "RS"), ("греция", "GR"), ("турция", "TR"),
("украина", "UA"), ("молдова", "MD"), ("молдавия", "MD"),
("грузия", "GE"), ("армения", "AM"), ("азербайджан", "AZ"),
("казахстан", "KZ"), ("узбекистан", "UZ"), ("швеция", "SE"),
("норвегия", "NO"), ("финляндия", "FI"), ("дания", "DK"),
("исландия", "IS"), ("эстония", "EE"), ("латвия", "LV"),
("литва", "LT"), ("люксембург", "LU"), ("албания", "AL"),
("македония", "MK"), ("босния", "BA"), ("черногория", "ME"),
("словения", "SI"), ("израиль", "IL"), ("индия", "IN"),
("таиланд", "TH"), ("вьетнам", "VN"), ("индонезия", "ID"),
("малайзия", "MY"), ("филиппины", "PH"), ("бразилия", "BR"),
("аргентина", "AR"), ("чили", "CL"), ("колумбия", "CO"),
("мексика", "MX"), ("канада", "CA"), ("австралия", "AU"),
("новая зеландия", "NZ"), ("юар", "ZA"), ("нигерия", "NG"),
("кения", "KE"), ("египет", "EG"), ("оаэ", "AE"), ("эмираты", "AE"),
("саудовская аравия", "SA"), ("панама", "PA"), ("иран", "IR"),
("ирак", "IQ"), ("пакистан", "PK"), ("монголия", "MN"),
# Major cities
("moscow", "RU"), ("saint petersburg", "RU"), ("novosibirsk", "RU"),
("москва", "RU"), ("питер", "RU"), ("спб", "RU"), ("петербург", "RU"),
("new york", "US"), ("los angeles", "US"), ("chicago", "US"),
("san francisco", "US"), ("seattle", "US"), ("dallas", "US"),
("miami", "US"), ("atlanta", "US"), ("washington", "US"),
("silicon valley", "US"), ("ashburn", "US"), ("phoenix", "US"),
("las vegas", "US"), ("denver", "US"), ("boston", "US"),
("berlin", "DE"), ("frankfurt", "DE"), ("munich", "DE"), ("hamburg", "DE"),
("dusseldorf", "DE"), ("nuremberg", "DE"),
("paris", "FR"), ("marseille", "FR"), ("lyon", "FR"),
("london", "GB"), ("manchester", "GB"), ("edinburgh", "GB"),
("tokyo", "JP"), ("osaka", "JP"),
("beijing", "CN"), ("shanghai", "CN"), ("guangzhou", "CN"), ("shenzhen", "CN"),
("seoul", "KR"), ("busan", "KR"),
("taipei", "TW"),
("amsterdam", "NL"), ("rotterdam", "NL"),
("rome", "IT"), ("milan", "IT"),
("madrid", "ES"), ("barcelona", "ES"),
("lisbon", "PT"), ("dublin", "IE"), ("brussels", "BE"),
("vienna", "AT"), ("wien", "AT"),
("zurich", "CH"), ("geneva", "CH"), ("bern", "CH"),
("warsaw", "PL"), ("krakow", "PL"),
("prague", "CZ"), ("budapest", "HU"), ("bucharest", "RO"),
("sofia", "BG"), ("zagreb", "HR"), ("belgrade", "RS"),
("athens", "GR"),
("kyiv", "UA"), ("kiev", "UA"), ("odessa", "UA"),
("киев", "UA"), ("одесса", "UA"),
("tbilisi", "GE"), ("yerevan", "AM"), ("baku", "AZ"),
("astana", "KZ"), ("almaty", "KZ"), ("tashkent", "UZ"),
("stockholm", "SE"), ("oslo", "NO"), ("helsinki", "FI"),
("copenhagen", "DK"), ("reykjavik", "IS"),
("tallinn", "EE"), ("riga", "LV"), ("vilnius", "LT"),
("istanbul", "TR"), ("ankara", "TR"),
("tel aviv", "IL"), ("jerusalem", "IL"),
("dubai", "AE"), ("abu dhabi", "AE"),
("riyadh", "SA"), ("jeddah", "SA"),
("cairo", "EG"), ("johannesburg", "ZA"), ("cape town", "ZA"),
("lagos", "NG"), ("nairobi", "KE"),
("sao paulo", "BR"), ("rio de janeiro", "BR"),
("buenos aires", "AR"), ("santiago", "CL"), ("bogota", "CO"),
("mexico city", "MX"),
("toronto", "CA"), ("montreal", "CA"), ("vancouver", "CA"),
("sydney", "AU"), ("melbourne", "AU"), ("perth", "AU"),
("auckland", "NZ"),
("mumbai", "IN"), ("delhi", "IN"), ("bangalore", "IN"),
("bangkok", "TH"),
("ho chi minh", "VN"), ("hanoi", "VN"),
("jakarta", "ID"), ("manila", "PH"), ("kuala lumpur", "MY"),
],
key=lambda x: -len(x[0]),
)
_VALID_CODES: frozenset[str] = frozenset(_STRIPES.keys())
def _detect_name(name: str) -> str:
lower = name.lower()
for pattern, code in _NAMES:
if re.search(r"\b" + re.escape(pattern) + r"\b", lower):
return code
return ""
def _detect_code(name: str) -> str:
m = re.match(r"^([A-Za-z]{2})(?:\s*[-_.|#\s]|\d)", name)
if m:
cc = m.group(1).upper()
if cc in _VALID_CODES:
return cc
return ""
_COUNTRY_TLDS: frozenset[str] = frozenset({
"ru", "de", "fr", "jp", "kr", "cn", "ua", "pl", "cz", "nl",
"se", "no", "fi", "dk", "at", "ch", "it", "es", "pt", "ie",
"be", "hu", "ro", "bg", "hr", "rs", "gr", "tr", "il", "br",
"ar", "mx", "ca", "au", "nz", "in", "sg", "my", "th", "vn",
"id", "ph", "eg", "za", "ng", "ke", "kz", "uz", "ge", "am",
"az", "ee", "lv", "lt", "lu", "al", "mk", "ba", "sk", "md",
"tw", "hk", "pk", "bd", "ir", "iq", "sa", "ae", "qa", "us",
"uk",
})
_TLD_REMAP: dict[str, str] = {"uk": "GB"}
def _detect_server(server: str) -> str:
if not server:
return ""
server = server.lower().strip(".")
# TLD check
parts = server.rsplit(".", 1)
if len(parts) == 2 and len(parts[1]) == 2:
tld = parts[1]
if tld in _COUNTRY_TLDS:
return _TLD_REMAP.get(tld, tld.upper())
# First label: "de1.server.com" → "de"
first = server.split(".")[0]
m = re.match(r"^([a-z]{2})\d", first)
if m and m.group(1) in _COUNTRY_TLDS:
return _TLD_REMAP.get(m.group(1), m.group(1).upper())
return ""
# ── Async IP-based country resolution ───────────────────────────
class CountryResolver(QThread):
resolved = pyqtSignal(dict) # {node_id: country_code}
def __init__(self, nodes: list[tuple[str, str]], parent=None):
super().__init__(parent)
self._nodes = nodes # [(node_id, server_address), ...]
def run(self) -> None:
results: dict[str, str] = {}
ip_map: dict[str, list[str]] = {} # ip → [node_ids]
for node_id, server in self._nodes:
try:
infos = socket.getaddrinfo(server, None, socket.AF_INET, socket.SOCK_STREAM)
if infos:
ip = infos[0][4][0]
ip_map.setdefault(ip, []).append(node_id)
except Exception:
pass
if not ip_map:
self.resolved.emit(results)
return
ips = list(ip_map.keys())
for i in range(0, len(ips), 100):
batch = ips[i : i + 100]
try:
payload = json.dumps(
[{"query": ip, "fields": "countryCode,query"} for ip in batch]
).encode()
req = urllib.request.Request(
"http://ip-api.com/batch",
data=payload,
headers={"Content-Type": "application/json"},
)
with _urlopen(req, timeout=10) as resp:
data = json.loads(resp.read())
for item in data:
cc = item.get("countryCode", "")
ip = item.get("query", "")
if cc and ip in ip_map:
for nid in ip_map[ip]:
results[nid] = cc
except Exception:
pass
self.resolved.emit(results)