k1rl-quasar / visitor_tracker.py
RealComp's picture
Upload 32 files
047d482 verified
Raw
History Blame Contribute Delete
14 kB
#!/usr/bin/env python3
"""
K1RL QUASAR — Visitor Tracking Module (HuggingFace Spaces)
Tracks IPs, locations, devices with container-safe persistence
"""
import os
import json
import time
import threading
import requests
from datetime import datetime
from pathlib import Path
try:
from user_agents import parse as parse_ua
except ImportError:
# Fallback if user-agents not available
def parse_ua(ua_string):
return type('UA', (), {
'browser': type('Browser', (), {'family': 'Unknown', 'version_string': ''}),
'os': type('OS', (), {'family': 'Unknown', 'version_string': ''}),
'device': type('Device', (), {'family': 'Unknown'}),
'is_mobile': False,
'is_tablet': False,
'is_pc': True,
'is_bot': False
})
# ============================================================================
# CONFIGURATION (HuggingFace Spaces paths)
# ============================================================================
BASE_DIR = Path('/home/user/app')
VISITOR_FILE = BASE_DIR / 'data' / 'visitors.json'
VISITOR_TIMEOUT = 120 # seconds before considered inactive
# Ensure data directory exists
VISITOR_FILE.parent.mkdir(parents=True, exist_ok=True)
# ============================================================================
# STORAGE
# ============================================================================
# Active visitors: {ip: {'last_seen': timestamp, 'user_agent': str}}
active_visitors = {}
# All-time visitors: {ip: {'first_seen': str, 'last_seen': str, 'visits': int, 'location': dict, 'device': dict}}
all_time_visitors = {}
visitor_lock = threading.Lock()
# ============================================================================
# GEOLOCATION (Free API with fallback)
# ============================================================================
def get_location(ip):
"""Get location from IP using free API with fallbacks"""
try:
# Skip local/private IPs
if ip in ['127.0.0.1', 'localhost'] or ip.startswith(('192.168.', '10.', '172.')):
return {'country': 'Local', 'city': 'Local', 'region': '', 'country_code': 'LO', 'isp': 'Local'}
# Try ip-api.com (free, 1000 requests per month)
response = requests.get(
f'http://ip-api.com/json/{ip}?fields=status,country,countryCode,region,city,isp',
timeout=3
)
if response.status_code == 200:
data = response.json()
if data.get('status') == 'success':
return {
'country': data.get('country', 'Unknown'),
'country_code': data.get('countryCode', '??'),
'city': data.get('city', 'Unknown'),
'region': data.get('region', ''),
'isp': data.get('isp', 'Unknown ISP')
}
except Exception as e:
print(f"⚠️ Geolocation API failed for {ip}: {e}")
# Fallback location data
return {
'country': 'Unknown',
'city': 'Unknown',
'country_code': '??',
'region': '',
'isp': 'Unknown ISP'
}
def parse_device(user_agent_string):
"""Parse user agent to get device info with fallback"""
try:
ua = parse_ua(user_agent_string)
return {
'browser': f"{ua.browser.family} {ua.browser.version_string}".strip(),
'os': f"{ua.os.family} {ua.os.version_string}".strip(),
'device': ua.device.family,
'is_mobile': ua.is_mobile,
'is_tablet': ua.is_tablet,
'is_pc': ua.is_pc,
'is_bot': ua.is_bot
}
except Exception as e:
print(f"⚠️ User agent parsing failed: {e}")
# Fallback device info
return {
'browser': 'Unknown',
'os': 'Unknown',
'device': 'Unknown',
'is_mobile': False,
'is_tablet': False,
'is_pc': True,
'is_bot': False
}
# ============================================================================
# PERSISTENCE (Container-safe)
# ============================================================================
def load_visitors():
"""Load all-time visitors from file"""
global all_time_visitors
try:
if VISITOR_FILE.exists():
with open(VISITOR_FILE, 'r') as f:
data = json.load(f)
all_time_visitors = data.get('visitors', {})
count = len(all_time_visitors)
print(f"📊 Loaded {count} historical visitors from {VISITOR_FILE}")
else:
print(f"📊 No existing visitor data found at {VISITOR_FILE}")
all_time_visitors = {}
except Exception as e:
print(f"⚠️ Could not load visitor data: {e}")
all_time_visitors = {}
def save_visitors():
"""Save all-time visitors to file"""
try:
visitor_data = {
'visitors': all_time_visitors,
'total_count': len(all_time_visitors),
'last_updated': datetime.now().isoformat(),
'platform': 'huggingface-spaces'
}
# Atomic write (write to temp file then rename)
temp_file = VISITOR_FILE.with_suffix('.tmp')
with open(temp_file, 'w') as f:
json.dump(visitor_data, f, indent=2)
temp_file.rename(VISITOR_FILE)
print(f"💾 Saved {len(all_time_visitors)} visitors to {VISITOR_FILE}")
except Exception as e:
print(f"⚠️ Could not save visitor data: {e}")
# ============================================================================
# TRACKING FUNCTIONS
# ============================================================================
def track_visitor(ip, user_agent=''):
"""Track a visitor by IP with location and device"""
with visitor_lock:
now = time.time()
now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Update active visitors
active_visitors[ip] = {
'last_seen': now,
'user_agent': user_agent
}
# Check if new visitor
is_new = ip not in all_time_visitors
if is_new:
print(f"👤 New visitor detected: {ip}")
# Get location and device info (these can be slow, so do it in background)
def get_visitor_info():
try:
location = get_location(ip)
device = parse_device(user_agent)
with visitor_lock:
all_time_visitors[ip] = {
'first_seen': now_str,
'last_seen': now_str,
'location': location,
'device': device,
'visits': 1,
'user_agent_history': [user_agent] if user_agent else []
}
save_visitors()
count = len(all_time_visitors)
city = location.get('city', '?')
country = location.get('country', '?')
print(f"👤 Visitor #{count}: {ip} from {city}, {country}")
except Exception as e:
print(f"⚠️ Error processing new visitor {ip}: {e}")
# Run in background thread to not slow down the request
info_thread = threading.Thread(target=get_visitor_info, daemon=True)
info_thread.start()
else:
# Update existing visitor
visitor = all_time_visitors[ip]
visitor['last_seen'] = now_str
visitor['visits'] = visitor.get('visits', 0) + 1
# Update user agent history (keep last 5)
if user_agent:
ua_history = visitor.setdefault('user_agent_history', [])
if user_agent not in ua_history:
ua_history.append(user_agent)
visitor['user_agent_history'] = ua_history[-5:] # Keep last 5
# Update device info if we have better data now
if user_agent and visitor.get('device', {}).get('browser') == 'Unknown':
visitor['device'] = parse_device(user_agent)
# Save periodically (every 10 visits)
if visitor['visits'] % 10 == 0:
save_visitors()
def cleanup_inactive():
"""Remove inactive visitors from active list"""
with visitor_lock:
now = time.time()
stale = [ip for ip, data in active_visitors.items()
if now - data['last_seen'] > VISITOR_TIMEOUT]
for ip in stale:
del active_visitors[ip]
def get_stats():
"""Get basic visitor statistics (for API compatibility)"""
cleanup_inactive()
with visitor_lock:
return {
'active_now': len(active_visitors),
'active_ips': list(active_visitors.keys()),
'all_time_unique': len(all_time_visitors),
}
def get_detailed_stats():
"""Get detailed visitor statistics"""
cleanup_inactive()
with visitor_lock:
active_ips = list(active_visitors.keys())
# Country stats
countries = {}
cities = {}
devices = {'mobile': 0, 'tablet': 0, 'pc': 0, 'bot': 0}
browsers = {}
for ip, data in all_time_visitors.items():
# Country stats
country = data.get('location', {}).get('country', 'Unknown')
countries[country] = countries.get(country, 0) + 1
# City stats
city = data.get('location', {}).get('city', 'Unknown')
cities[city] = cities.get(city, 0) + 1
# Device stats
device_info = data.get('device', {})
if device_info.get('is_mobile'):
devices['mobile'] += 1
elif device_info.get('is_tablet'):
devices['tablet'] += 1
elif device_info.get('is_bot'):
devices['bot'] += 1
else:
devices['pc'] += 1
# Browser stats
browser = device_info.get('browser', 'Unknown').split()[0] # Get browser name only
browsers[browser] = browsers.get(browser, 0) + 1
return {
'total_unique': len(all_time_visitors),
'active_now': len(active_ips),
'countries': dict(sorted(countries.items(), key=lambda x: x[1], reverse=True)[:10]),
'cities': dict(sorted(cities.items(), key=lambda x: x[1], reverse=True)[:10]),
'devices': devices,
'browsers': dict(sorted(browsers.items(), key=lambda x: x[1], reverse=True)[:10]),
}
def get_all_visitors():
"""Get ALL visitor details with location and device"""
cleanup_inactive()
with visitor_lock:
active_ips = list(active_visitors.keys())
visitors_list = []
for ip, data in all_time_visitors.items():
visitors_list.append({
'ip': ip,
'is_active': ip in active_ips,
'first_seen': data.get('first_seen', ''),
'last_seen': data.get('last_seen', ''),
'visits': data.get('visits', 1),
'location': data.get('location', {}),
'device': data.get('device', {}),
'user_agent_count': len(data.get('user_agent_history', []))
})
# Sort: active first, then by visits, then by last_seen
visitors_list.sort(
key=lambda x: (not x['is_active'], -x['visits'], x['last_seen']),
reverse=False
)
return {
'total_unique': len(all_time_visitors),
'active_now': len(active_ips),
'visitors': visitors_list[:100], # Limit to first 100 for performance
'showing_count': min(100, len(visitors_list))
}
# ============================================================================
# BACKGROUND CLEANUP TASK
# ============================================================================
def background_cleanup():
"""Background task to periodically clean up and save data"""
while True:
try:
time.sleep(300) # Every 5 minutes
cleanup_inactive()
# Save data every hour
current_time = int(time.time())
if current_time % 3600 < 300: # Within 5 minutes of the hour
save_visitors()
except Exception as e:
print(f"⚠️ Background cleanup error: {e}")
# ============================================================================
# INITIALIZE
# ============================================================================
# Load existing visitor data
load_visitors()
# Start background cleanup task
cleanup_thread = threading.Thread(target=background_cleanup, daemon=True)
cleanup_thread.start()
print(f"✅ K1RL QUASAR Visitor Tracker ready")
print(f" Platform: HuggingFace Spaces")
print(f" Historical visitors: {len(all_time_visitors)}")
print(f" Data file: {VISITOR_FILE}")
print("=" * 50)
# Export main functions for use by other modules
__all__ = ['track_visitor', 'get_stats', 'get_detailed_stats', 'get_all_visitors', 'cleanup_inactive']