telegram_webapp.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. from __future__ import annotations
  2. import hashlib
  3. import hmac
  4. import json
  5. from datetime import UTC, datetime
  6. from typing import Any
  7. from urllib.parse import parse_qsl
  8. class TelegramWebAppAuthError(ValueError):
  9. pass
  10. def verify_telegram_webapp_init_data(
  11. init_data: str,
  12. bot_token: str,
  13. *,
  14. max_age_seconds: int = 3600,
  15. now: datetime | None = None,
  16. ) -> dict[str, Any]:
  17. """Validate Telegram Mini App init data and return the signed user."""
  18. if not init_data or not bot_token:
  19. raise TelegramWebAppAuthError("缺少 Telegram Mini App 鉴权信息。")
  20. try:
  21. values = dict(parse_qsl(init_data, keep_blank_values=True, strict_parsing=True))
  22. except ValueError as exc:
  23. raise TelegramWebAppAuthError("Telegram Mini App 鉴权信息格式无效。") from exc
  24. supplied_hash = values.pop("hash", "")
  25. if len(supplied_hash) != 64:
  26. raise TelegramWebAppAuthError("Telegram Mini App 签名无效。")
  27. data_check_string = "\n".join(
  28. f"{key}={value}" for key, value in sorted(values.items())
  29. )
  30. secret_key = hmac.new(
  31. b"WebAppData",
  32. bot_token.encode("utf-8"),
  33. hashlib.sha256,
  34. ).digest()
  35. expected_hash = hmac.new(
  36. secret_key,
  37. data_check_string.encode("utf-8"),
  38. hashlib.sha256,
  39. ).hexdigest()
  40. if not hmac.compare_digest(expected_hash, supplied_hash):
  41. raise TelegramWebAppAuthError("Telegram Mini App 签名无效。")
  42. try:
  43. auth_date = int(values.get("auth_date") or 0)
  44. except (TypeError, ValueError) as exc:
  45. raise TelegramWebAppAuthError("Telegram Mini App 鉴权时间无效。") from exc
  46. current = now or datetime.now(UTC)
  47. age_seconds = int(current.timestamp()) - auth_date
  48. if auth_date <= 0 or age_seconds < -30 or age_seconds > max_age_seconds:
  49. raise TelegramWebAppAuthError("Telegram Mini App 登录已过期,请从 Bot 重新打开。")
  50. try:
  51. user = json.loads(values.get("user") or "")
  52. except (TypeError, json.JSONDecodeError) as exc:
  53. raise TelegramWebAppAuthError("Telegram 用户信息无效。") from exc
  54. if not isinstance(user, dict):
  55. raise TelegramWebAppAuthError("Telegram 用户信息无效。")
  56. try:
  57. user_id = int(user.get("id"))
  58. except (TypeError, ValueError) as exc:
  59. raise TelegramWebAppAuthError("Telegram 用户编号无效。") from exc
  60. if user_id <= 0 or user.get("is_bot"):
  61. raise TelegramWebAppAuthError("Telegram 用户身份无效。")
  62. return {
  63. "user_id": user_id,
  64. "username": str(user.get("username") or ""),
  65. "display_name": " ".join(
  66. str(user.get(key) or "").strip()
  67. for key in ("first_name", "last_name")
  68. if str(user.get(key) or "").strip()
  69. )
  70. or f"技师 {user_id}",
  71. "language_code": str(user.get("language_code") or ""),
  72. "auth_date": auth_date,
  73. "query_id": str(values.get("query_id") or ""),
  74. }