token_refresh.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. """Token refresh helpers for the zhuce6 ChatGPT platform."""
  2. from __future__ import annotations
  3. from dataclasses import dataclass
  4. from datetime import datetime, timedelta
  5. import logging
  6. from typing import Any
  7. from curl_cffi import requests as cffi_requests
  8. from .constants import (
  9. OAUTH_CLIENT_ID,
  10. OAUTH_REDIRECT_URI,
  11. OPENAI_IMPERSONATE,
  12. OPENAI_SEC_CH_UA,
  13. OPENAI_SEC_CH_UA_MOBILE,
  14. OPENAI_SEC_CH_UA_PLATFORM,
  15. OPENAI_USER_AGENT,
  16. )
  17. logger = logging.getLogger(__name__)
  18. @dataclass
  19. class TokenRefreshResult:
  20. success: bool
  21. access_token: str = ""
  22. refresh_token: str = ""
  23. account_id: str = ""
  24. email: str = ""
  25. session_token: str = ""
  26. expires_at: datetime | None = None
  27. error_message: str = ""
  28. class TokenRefreshManager:
  29. SESSION_URL = "https://chatgpt.com/api/auth/session"
  30. TOKEN_URL = "https://auth.openai.com/oauth/token"
  31. def __init__(self, proxy_url: str | None = None) -> None:
  32. self.proxy_url = proxy_url
  33. self._oauth_client_id = OAUTH_CLIENT_ID
  34. self._oauth_redirect_uri = OAUTH_REDIRECT_URI
  35. @property
  36. def _default_headers(self) -> dict[str, str]:
  37. return {
  38. "user-agent": OPENAI_USER_AGENT,
  39. "accept-language": "en-US,en;q=0.9",
  40. "sec-ch-ua": OPENAI_SEC_CH_UA,
  41. "sec-ch-ua-mobile": OPENAI_SEC_CH_UA_MOBILE,
  42. "sec-ch-ua-platform": OPENAI_SEC_CH_UA_PLATFORM,
  43. }
  44. def _create_session(self) -> cffi_requests.Session:
  45. return cffi_requests.Session(impersonate=OPENAI_IMPERSONATE, proxy=self.proxy_url)
  46. def refresh_by_session_token(self, session_token: str) -> TokenRefreshResult:
  47. result = TokenRefreshResult(success=False)
  48. try:
  49. session = self._create_session()
  50. session.cookies.set(
  51. "__Secure-next-auth.session-token",
  52. session_token,
  53. domain=".chatgpt.com",
  54. path="/",
  55. )
  56. response = session.get(
  57. self.SESSION_URL,
  58. headers={**self._default_headers, "accept": "application/json"},
  59. timeout=30,
  60. )
  61. if response.status_code != 200:
  62. result.error_message = f"Session token refresh failed: HTTP {response.status_code}"
  63. return result
  64. data = response.json()
  65. access_token = str(data.get("accessToken") or "").strip()
  66. if not access_token:
  67. result.error_message = "Session token refresh failed: missing accessToken"
  68. return result
  69. expires_at = None
  70. expires_str = str(data.get("expires") or "").strip()
  71. if expires_str:
  72. try:
  73. expires_at = datetime.fromisoformat(expires_str.replace("Z", "+00:00"))
  74. except ValueError:
  75. expires_at = None
  76. result.success = True
  77. result.access_token = access_token
  78. user = data.get("user") or {}
  79. result.account_id = str(data.get("account_id") or (user.get("id") if isinstance(user, dict) else "") or "").strip()
  80. result.email = str((user.get("email") if isinstance(user, dict) else "") or data.get("email") or "").strip()
  81. result.session_token = session_token
  82. result.expires_at = expires_at
  83. return result
  84. except Exception as exc:
  85. result.error_message = f"Session token refresh exception: {exc}"
  86. logger.error(result.error_message)
  87. return result
  88. def refresh_by_oauth_token(self, refresh_token: str, client_id: str | None = None) -> TokenRefreshResult:
  89. result = TokenRefreshResult(success=False)
  90. try:
  91. session = self._create_session()
  92. response = session.post(
  93. self.TOKEN_URL,
  94. headers={
  95. **self._default_headers,
  96. "content-type": "application/x-www-form-urlencoded",
  97. "accept": "application/json",
  98. },
  99. data={
  100. "client_id": client_id or self._oauth_client_id,
  101. "grant_type": "refresh_token",
  102. "refresh_token": refresh_token,
  103. "redirect_uri": self._oauth_redirect_uri,
  104. },
  105. timeout=30,
  106. )
  107. if response.status_code != 200:
  108. result.error_message = f"OAuth token refresh failed: HTTP {response.status_code}"
  109. return result
  110. data = response.json()
  111. access_token = str(data.get("access_token") or "").strip()
  112. if not access_token:
  113. result.error_message = "OAuth token refresh failed: missing access_token"
  114. return result
  115. result.success = True
  116. result.access_token = access_token
  117. result.refresh_token = str(data.get("refresh_token") or refresh_token).strip()
  118. result.expires_at = datetime.utcnow() + timedelta(seconds=int(data.get("expires_in", 3600)))
  119. return result
  120. except Exception as exc:
  121. result.error_message = f"OAuth token refresh exception: {exc}"
  122. logger.error(result.error_message)
  123. return result
  124. def refresh_account(self, account: Any) -> TokenRefreshResult:
  125. session_token = str(getattr(account, "session_token", "") or "").strip()
  126. if session_token:
  127. session_result = self.refresh_by_session_token(session_token)
  128. if session_result.success:
  129. return session_result
  130. refresh_token = str(getattr(account, "refresh_token", "") or "").strip()
  131. if refresh_token:
  132. return self.refresh_by_oauth_token(
  133. refresh_token=refresh_token,
  134. client_id=str(getattr(account, "client_id", "") or "").strip() or None,
  135. )
  136. return TokenRefreshResult(success=False, error_message="No session_token or refresh_token available")