KManager / spoofers /ip_timezone.py
StarrySkyWorld's picture
Initial commit
494c89b
"""
Определение timezone по IP адресу
Использует бесплатные API для определения геолокации по IP.
Поддерживает локальный режим без внешних запросов для приватности.
"""
import requests
import time
import datetime
from typing import Optional, Tuple, Dict
from dataclasses import dataclass
@dataclass
class IPGeoData:
"""Данные геолокации по IP"""
ip: str
timezone: str
timezone_offset: int # минуты от UTC (положительное = запад)
country: str
city: str
latitude: float
longitude: float
locale: str
# Маппинг timezone -> offset (минуты западнее UTC)
TIMEZONE_OFFSETS = {
# США
'America/New_York': 300, # UTC-5
'America/Chicago': 360, # UTC-6
'America/Denver': 420, # UTC-7
'America/Los_Angeles': 480, # UTC-8
'America/Phoenix': 420, # UTC-7 (no DST)
'America/Anchorage': 540, # UTC-9
'Pacific/Honolulu': 600, # UTC-10
# Европа
'Europe/London': 0, # UTC+0
'Europe/Paris': -60, # UTC+1
'Europe/Berlin': -60, # UTC+1
'Europe/Moscow': -180, # UTC+3
'Europe/Kiev': -120, # UTC+2
'Europe/Warsaw': -60, # UTC+1
'Europe/Amsterdam': -60, # UTC+1
'Europe/Rome': -60, # UTC+1
'Europe/Madrid': -60, # UTC+1
'Europe/Stockholm': -60, # UTC+1
'Europe/Helsinki': -120, # UTC+2
'Europe/Athens': -120, # UTC+2
'Europe/Istanbul': -180, # UTC+3
# Азия
'Asia/Tokyo': -540, # UTC+9
'Asia/Shanghai': -480, # UTC+8
'Asia/Singapore': -480, # UTC+8
'Asia/Dubai': -240, # UTC+4
'Asia/Kolkata': -330, # UTC+5:30
'Asia/Seoul': -540, # UTC+9
'Asia/Hong_Kong': -480, # UTC+8
'Asia/Bangkok': -420, # UTC+7
'Asia/Jakarta': -420, # UTC+7
'Asia/Manila': -480, # UTC+8
'Asia/Taipei': -480, # UTC+8
'Asia/Kuala_Lumpur': -480, # UTC+8
'Asia/Ho_Chi_Minh': -420, # UTC+7
'Asia/Karachi': -300, # UTC+5
'Asia/Tehran': -210, # UTC+3:30
'Asia/Jerusalem': -120, # UTC+2
# Другие
'Australia/Sydney': -660, # UTC+11
'Australia/Melbourne': -660, # UTC+11
'Australia/Perth': -480, # UTC+8
'Pacific/Auckland': -780, # UTC+13
# Южная Америка
'America/Sao_Paulo': 180, # UTC-3
'America/Buenos_Aires': 180, # UTC-3
'America/Santiago': 240, # UTC-4
'America/Lima': 300, # UTC-5
'America/Bogota': 300, # UTC-5
'America/Mexico_City': 360, # UTC-6
# Канада
'America/Toronto': 300, # UTC-5
'America/Vancouver': 480, # UTC-8
'America/Edmonton': 420, # UTC-7
# Африка
'Africa/Cairo': -120, # UTC+2
'Africa/Johannesburg': -120, # UTC+2
'Africa/Lagos': -60, # UTC+1
'Africa/Nairobi': -180, # UTC+3
}
# Маппинг timezone -> геоданные (country, city, lat, lon)
# Используется для локального режима без внешних API
TIMEZONE_GEO_DATA: Dict[str, Dict] = {
# США
'America/New_York': {'country': 'US', 'city': 'New York', 'lat': 40.7128, 'lon': -74.0060},
'America/Chicago': {'country': 'US', 'city': 'Chicago', 'lat': 41.8781, 'lon': -87.6298},
'America/Denver': {'country': 'US', 'city': 'Denver', 'lat': 39.7392, 'lon': -104.9903},
'America/Los_Angeles': {'country': 'US', 'city': 'Los Angeles', 'lat': 34.0522, 'lon': -118.2437},
'America/Phoenix': {'country': 'US', 'city': 'Phoenix', 'lat': 33.4484, 'lon': -112.0740},
'America/Anchorage': {'country': 'US', 'city': 'Anchorage', 'lat': 61.2181, 'lon': -149.9003},
'Pacific/Honolulu': {'country': 'US', 'city': 'Honolulu', 'lat': 21.3069, 'lon': -157.8583},
# Европа
'Europe/London': {'country': 'GB', 'city': 'London', 'lat': 51.5074, 'lon': -0.1278},
'Europe/Paris': {'country': 'FR', 'city': 'Paris', 'lat': 48.8566, 'lon': 2.3522},
'Europe/Berlin': {'country': 'DE', 'city': 'Berlin', 'lat': 52.5200, 'lon': 13.4050},
'Europe/Moscow': {'country': 'RU', 'city': 'Moscow', 'lat': 55.7558, 'lon': 37.6173},
'Europe/Kiev': {'country': 'UA', 'city': 'Kyiv', 'lat': 50.4501, 'lon': 30.5234},
'Europe/Warsaw': {'country': 'PL', 'city': 'Warsaw', 'lat': 52.2297, 'lon': 21.0122},
'Europe/Amsterdam': {'country': 'NL', 'city': 'Amsterdam', 'lat': 52.3676, 'lon': 4.9041},
'Europe/Rome': {'country': 'IT', 'city': 'Rome', 'lat': 41.9028, 'lon': 12.4964},
'Europe/Madrid': {'country': 'ES', 'city': 'Madrid', 'lat': 40.4168, 'lon': -3.7038},
'Europe/Stockholm': {'country': 'SE', 'city': 'Stockholm', 'lat': 59.3293, 'lon': 18.0686},
'Europe/Helsinki': {'country': 'FI', 'city': 'Helsinki', 'lat': 60.1699, 'lon': 24.9384},
'Europe/Athens': {'country': 'GR', 'city': 'Athens', 'lat': 37.9838, 'lon': 23.7275},
'Europe/Istanbul': {'country': 'TR', 'city': 'Istanbul', 'lat': 41.0082, 'lon': 28.9784},
# Азия
'Asia/Tokyo': {'country': 'JP', 'city': 'Tokyo', 'lat': 35.6762, 'lon': 139.6503},
'Asia/Shanghai': {'country': 'CN', 'city': 'Shanghai', 'lat': 31.2304, 'lon': 121.4737},
'Asia/Singapore': {'country': 'SG', 'city': 'Singapore', 'lat': 1.3521, 'lon': 103.8198},
'Asia/Dubai': {'country': 'AE', 'city': 'Dubai', 'lat': 25.2048, 'lon': 55.2708},
'Asia/Kolkata': {'country': 'IN', 'city': 'Mumbai', 'lat': 19.0760, 'lon': 72.8777},
'Asia/Seoul': {'country': 'KR', 'city': 'Seoul', 'lat': 37.5665, 'lon': 126.9780},
'Asia/Hong_Kong': {'country': 'HK', 'city': 'Hong Kong', 'lat': 22.3193, 'lon': 114.1694},
'Asia/Bangkok': {'country': 'TH', 'city': 'Bangkok', 'lat': 13.7563, 'lon': 100.5018},
'Asia/Jakarta': {'country': 'ID', 'city': 'Jakarta', 'lat': -6.2088, 'lon': 106.8456},
'Asia/Manila': {'country': 'PH', 'city': 'Manila', 'lat': 14.5995, 'lon': 120.9842},
'Asia/Taipei': {'country': 'TW', 'city': 'Taipei', 'lat': 25.0330, 'lon': 121.5654},
'Asia/Kuala_Lumpur': {'country': 'MY', 'city': 'Kuala Lumpur', 'lat': 3.1390, 'lon': 101.6869},
'Asia/Ho_Chi_Minh': {'country': 'VN', 'city': 'Ho Chi Minh City', 'lat': 10.8231, 'lon': 106.6297},
'Asia/Karachi': {'country': 'PK', 'city': 'Karachi', 'lat': 24.8607, 'lon': 67.0011},
'Asia/Tehran': {'country': 'IR', 'city': 'Tehran', 'lat': 35.6892, 'lon': 51.3890},
'Asia/Jerusalem': {'country': 'IL', 'city': 'Jerusalem', 'lat': 31.7683, 'lon': 35.2137},
# Австралия и Океания
'Australia/Sydney': {'country': 'AU', 'city': 'Sydney', 'lat': -33.8688, 'lon': 151.2093},
'Australia/Melbourne': {'country': 'AU', 'city': 'Melbourne', 'lat': -37.8136, 'lon': 144.9631},
'Australia/Perth': {'country': 'AU', 'city': 'Perth', 'lat': -31.9505, 'lon': 115.8605},
'Pacific/Auckland': {'country': 'NZ', 'city': 'Auckland', 'lat': -36.8485, 'lon': 174.7633},
# Южная Америка
'America/Sao_Paulo': {'country': 'BR', 'city': 'São Paulo', 'lat': -23.5505, 'lon': -46.6333},
'America/Buenos_Aires': {'country': 'AR', 'city': 'Buenos Aires', 'lat': -34.6037, 'lon': -58.3816},
'America/Santiago': {'country': 'CL', 'city': 'Santiago', 'lat': -33.4489, 'lon': -70.6693},
'America/Lima': {'country': 'PE', 'city': 'Lima', 'lat': -12.0464, 'lon': -77.0428},
'America/Bogota': {'country': 'CO', 'city': 'Bogotá', 'lat': 4.7110, 'lon': -74.0721},
'America/Mexico_City': {'country': 'MX', 'city': 'Mexico City', 'lat': 19.4326, 'lon': -99.1332},
# Канада
'America/Toronto': {'country': 'CA', 'city': 'Toronto', 'lat': 43.6532, 'lon': -79.3832},
'America/Vancouver': {'country': 'CA', 'city': 'Vancouver', 'lat': 49.2827, 'lon': -123.1207},
'America/Edmonton': {'country': 'CA', 'city': 'Edmonton', 'lat': 53.5461, 'lon': -113.4938},
# Африка
'Africa/Cairo': {'country': 'EG', 'city': 'Cairo', 'lat': 30.0444, 'lon': 31.2357},
'Africa/Johannesburg': {'country': 'ZA', 'city': 'Johannesburg', 'lat': -26.2041, 'lon': 28.0473},
'Africa/Lagos': {'country': 'NG', 'city': 'Lagos', 'lat': 6.5244, 'lon': 3.3792},
'Africa/Nairobi': {'country': 'KE', 'city': 'Nairobi', 'lat': -1.2921, 'lon': 36.8219},
}
# Маппинг country -> locale
COUNTRY_LOCALES = {
'US': 'en-US',
'GB': 'en-GB',
'CA': 'en-CA',
'AU': 'en-AU',
'NZ': 'en-NZ',
'DE': 'de-DE',
'FR': 'fr-FR',
'ES': 'es-ES',
'IT': 'it-IT',
'NL': 'nl-NL',
'SE': 'sv-SE',
'FI': 'fi-FI',
'PL': 'pl-PL',
'GR': 'el-GR',
'TR': 'tr-TR',
'UA': 'uk-UA',
'JP': 'ja-JP',
'CN': 'zh-CN',
'TW': 'zh-TW',
'HK': 'zh-HK',
'KR': 'ko-KR',
'RU': 'ru-RU',
'BR': 'pt-BR',
'IN': 'hi-IN',
'SG': 'en-SG',
'MY': 'ms-MY',
'TH': 'th-TH',
'VN': 'vi-VN',
'ID': 'id-ID',
'PH': 'en-PH',
'PK': 'ur-PK',
'IR': 'fa-IR',
'IL': 'he-IL',
'AE': 'ar-AE',
'EG': 'ar-EG',
'ZA': 'en-ZA',
'NG': 'en-NG',
'KE': 'en-KE',
'AR': 'es-AR',
'CL': 'es-CL',
'CO': 'es-CO',
'PE': 'es-PE',
'MX': 'es-MX',
}
def get_timezone_offset(timezone: str) -> int:
"""Получает offset для timezone"""
return TIMEZONE_OFFSETS.get(timezone, 0)
def get_locale_for_country(country_code: str) -> str:
"""Получает locale для страны"""
return COUNTRY_LOCALES.get(country_code, 'en-US')
def get_local_geo_data() -> IPGeoData:
"""
Получает геоданные на основе системного timezone.
Не делает внешних запросов - полностью локальная функция.
Используется для приватности (не раскрывает реальный IP).
Returns:
IPGeoData с примерными данными на основе системного timezone
"""
tz_name, offset_minutes = get_system_timezone()
# Ищем геоданные для timezone
geo_data = TIMEZONE_GEO_DATA.get(tz_name)
if not geo_data:
# Если timezone не найден, пробуем найти по offset
for tz, data in TIMEZONE_GEO_DATA.items():
if TIMEZONE_OFFSETS.get(tz) == offset_minutes:
geo_data = data
tz_name = tz
break
# Fallback на New York если ничего не найдено
if not geo_data:
geo_data = TIMEZONE_GEO_DATA['America/New_York']
tz_name = 'America/New_York'
offset_minutes = TIMEZONE_OFFSETS['America/New_York']
country = geo_data['country']
return IPGeoData(
ip='127.0.0.1', # Локальный IP - не раскрываем реальный
timezone=tz_name,
timezone_offset=offset_minutes,
country=country,
city=geo_data['city'],
latitude=geo_data['lat'],
longitude=geo_data['lon'],
locale=get_locale_for_country(country)
)
def detect_ip_geo(use_external_api: bool = True) -> Optional[IPGeoData]:
"""
Определяет геолокацию по текущему IP.
Args:
use_external_api: Если True, использует внешние API (ip-api.com и др.)
Если False, возвращает данные на основе системного timezone
(не раскрывает реальный IP - для приватности)
Returns:
IPGeoData или None если не удалось определить
Note:
При use_external_api=False IP будет '127.0.0.1', а геоданные
будут примерными на основе системного timezone.
При use_external_api=True, если все API недоступны,
автоматически используется fallback на локальные данные.
"""
# Если не используем внешние API - возвращаем локальные данные
if not use_external_api:
print("[IP-GEO] Using local timezone data (no external API)")
return get_local_geo_data()
# Попытка 1: ip-api.com (бесплатный, без ключа)
try:
resp = requests.get(
'http://ip-api.com/json/?fields=status,country,countryCode,city,lat,lon,timezone,query',
timeout=5
)
if resp.status_code == 200:
data = resp.json()
if data.get('status') == 'success':
tz = data.get('timezone', 'America/New_York')
country = data.get('countryCode', 'US')
return IPGeoData(
ip=data.get('query', ''),
timezone=tz,
timezone_offset=get_timezone_offset(tz),
country=country,
city=data.get('city', ''),
latitude=data.get('lat', 0),
longitude=data.get('lon', 0),
locale=get_locale_for_country(country)
)
except Exception as e:
print(f"[IP-GEO] ip-api.com failed: {e}")
# Попытка 2: ipapi.co (бесплатный лимит)
try:
resp = requests.get('https://ipapi.co/json/', timeout=5)
if resp.status_code == 200:
data = resp.json()
tz = data.get('timezone', 'America/New_York')
country = data.get('country_code', 'US')
return IPGeoData(
ip=data.get('ip', ''),
timezone=tz,
timezone_offset=get_timezone_offset(tz),
country=country,
city=data.get('city', ''),
latitude=data.get('latitude', 0),
longitude=data.get('longitude', 0),
locale=get_locale_for_country(country)
)
except Exception as e:
print(f"[IP-GEO] ipapi.co failed: {e}")
# Попытка 3: ipinfo.io (бесплатный лимит)
try:
resp = requests.get('https://ipinfo.io/json', timeout=5)
if resp.status_code == 200:
data = resp.json()
tz = data.get('timezone', 'America/New_York')
country = data.get('country', 'US')
loc = data.get('loc', '0,0').split(',')
return IPGeoData(
ip=data.get('ip', ''),
timezone=tz,
timezone_offset=get_timezone_offset(tz),
country=country,
city=data.get('city', ''),
latitude=float(loc[0]) if len(loc) > 0 else 0,
longitude=float(loc[1]) if len(loc) > 1 else 0,
locale=get_locale_for_country(country)
)
except Exception as e:
print(f"[IP-GEO] ipinfo.io failed: {e}")
# Fallback на локальные данные если все API недоступны
print("[IP-GEO] All external APIs failed, using local timezone fallback")
return get_local_geo_data()
def get_system_timezone() -> Tuple[str, int]:
"""
Получает timezone из системы.
Returns:
(timezone_name, offset_minutes)
"""
# Получаем offset в секундах
if time.daylight:
offset_seconds = -time.altzone
else:
offset_seconds = -time.timezone
# Конвертируем в минуты (положительное = запад от UTC)
offset_minutes = -offset_seconds // 60
# Пытаемся получить имя timezone
tz_name = None
# Метод 1: через datetime.astimezone()
try:
local_tz = datetime.datetime.now().astimezone().tzinfo
if local_tz:
tz_str = str(local_tz)
# Проверяем что это валидное имя timezone (не просто offset)
if '/' in tz_str and tz_str in TIMEZONE_GEO_DATA:
tz_name = tz_str
except Exception:
pass
# Метод 2: через переменную окружения TZ
if not tz_name:
import os
env_tz = os.environ.get('TZ')
if env_tz and env_tz in TIMEZONE_GEO_DATA:
tz_name = env_tz
# Метод 3: через zoneinfo (Python 3.9+)
if not tz_name:
try:
import zoneinfo
# Пробуем получить системный timezone
local_tz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzinfo
if hasattr(local_tz, 'key'):
tz_name = local_tz.key
except Exception:
pass
# Fallback - угадываем по offset
if not tz_name:
tz_name = _guess_timezone_by_offset(offset_minutes)
return tz_name, offset_minutes
def _guess_timezone_by_offset(offset: int) -> str:
"""Угадывает timezone по offset"""
for tz, off in TIMEZONE_OFFSETS.items():
if off == offset:
return tz
return 'America/New_York'
if __name__ == '__main__':
print("=" * 50)
print("Testing IP geolocation")
print("=" * 50)
# Тест 1: Локальные данные (без внешних API)
print("\n[TEST 1] Local timezone data (use_external_api=False)")
print("-" * 50)
geo_local = detect_ip_geo(use_external_api=False)
if geo_local:
print(f" IP: {geo_local.ip}")
print(f" Country: {geo_local.country}")
print(f" City: {geo_local.city}")
print(f" Timezone: {geo_local.timezone}")
print(f" TZ Offset: {geo_local.timezone_offset} min")
print(f" Coords: {geo_local.latitude}, {geo_local.longitude}")
print(f" Locale: {geo_local.locale}")
# Тест 2: Внешние API (по умолчанию)
print("\n[TEST 2] External API data (use_external_api=True)")
print("-" * 50)
geo_external = detect_ip_geo(use_external_api=True)
if geo_external:
print(f" IP: {geo_external.ip}")
print(f" Country: {geo_external.country}")
print(f" City: {geo_external.city}")
print(f" Timezone: {geo_external.timezone}")
print(f" TZ Offset: {geo_external.timezone_offset} min")
print(f" Coords: {geo_external.latitude}, {geo_external.longitude}")
print(f" Locale: {geo_external.locale}")
# Тест 3: Системный timezone
print("\n[TEST 3] System timezone detection")
print("-" * 50)
tz, offset = get_system_timezone()
print(f" Timezone: {tz}")
print(f" Offset: {offset} min")
print("\n" + "=" * 50)
print("Tests completed!")
print("=" * 50)