"""Validate Telegram Web App initData and extract user username.""" import hashlib import hmac import json from urllib.parse import unquote # Telegram algorithm: https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app # Data-check string must use the same key=value pairs as received (sorted by key); we preserve raw values. def validate_init_data(init_data: str, bot_token: str) -> str | None: """ Validate initData signature and return the Telegram username (lowercase, no @). Returns None if data is invalid, forged, or user has no username. """ if not init_data or not bot_token: return None init_data = init_data.strip() # Parse preserving raw values for HMAC (key -> raw value) params = {} for part in init_data.split("&"): if "=" not in part: continue key, _, value = part.partition("=") if not key: continue params[key] = value hash_val = params.pop("hash", None) if not hash_val: return None # Build data-check string: keys sorted, format key=value per line (raw values) data_pairs = sorted(params.items()) data_string = "\n".join(f"{k}={v}" for k, v in data_pairs) # secret_key = HMAC-SHA256(key=b"WebAppData", msg=bot_token.encode()).digest() secret_key = hmac.new( b"WebAppData", msg=bot_token.encode(), digestmod=hashlib.sha256, ).digest() # computed = HMAC-SHA256(key=secret_key, msg=data_string.encode()).hexdigest() computed = hmac.new( secret_key, msg=data_string.encode(), digestmod=hashlib.sha256, ).hexdigest() if not hmac.compare_digest(computed.lower(), hash_val.lower()): return None # Parse user JSON (value may be URL-encoded in the raw string) user_raw = params.get("user") if not user_raw: return None try: user = json.loads(unquote(user_raw)) except (json.JSONDecodeError, TypeError): return None if not isinstance(user, dict): return None username = user.get("username") if not username or not isinstance(username, str): return None return username.strip().lstrip("@").lower()