register_otp.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. """OTP helpers for ChatGPT registration."""
  2. from __future__ import annotations
  3. import json
  4. import time
  5. from typing import Any
  6. from .constants import OTP_CODE_PATTERN, OPENAI_API_ENDPOINTS
  7. def _mailbox_context(self) -> tuple[Any | None, Any | None]:
  8. mailbox = getattr(self.email_service, "mailbox", None)
  9. account = getattr(self.email_service, "_account", None)
  10. if mailbox is None or account is None:
  11. return None, None
  12. return mailbox, account
  13. def _capture_mailbox_ids(self) -> set[str]:
  14. mailbox, account = self._mailbox_context()
  15. if mailbox is None or account is None:
  16. return set()
  17. try:
  18. return set(mailbox.get_current_ids(account) or set())
  19. except Exception as exc:
  20. self._log(f"mailbox snapshot failed: {exc}")
  21. return set()
  22. def _wait_for_mailbox_code(
  23. self,
  24. *,
  25. before_ids: set[str] | None = None,
  26. timeout: int = 180,
  27. keyword: str = "",
  28. not_before_timestamp: float | None = None,
  29. ) -> str:
  30. mailbox, account = self._mailbox_context()
  31. if mailbox is None or account is None:
  32. return ""
  33. try:
  34. wait_callable = getattr(mailbox, "wait_for_code")
  35. try:
  36. result = wait_callable(
  37. account,
  38. keyword=keyword,
  39. timeout=timeout,
  40. before_ids=before_ids,
  41. not_before_timestamp=not_before_timestamp,
  42. )
  43. except TypeError:
  44. result = wait_callable(
  45. account,
  46. keyword=keyword,
  47. timeout=timeout,
  48. before_ids=before_ids,
  49. )
  50. return str(result or "").strip()
  51. except Exception as exc:
  52. self._log(f"mailbox wait_for_code failed: {exc}")
  53. return ""
  54. def _get_verification_code(self) -> str | None:
  55. if not self.email:
  56. return None
  57. self._last_otp_wait_failure_reason = ""
  58. self._last_otp_wait_diagnostics = {}
  59. try:
  60. mailbox, account = self._mailbox_context()
  61. if mailbox is not None and account is not None:
  62. started_at = time.time()
  63. baseline_ids = set(self._signup_otp_before_ids or set())
  64. self._log(
  65. "waiting for verification code via mailbox: "
  66. f"timeout={self._otp_wait_timeout_seconds}s baseline_ids={len(baseline_ids)}"
  67. )
  68. code = self._wait_for_mailbox_code(
  69. before_ids=baseline_ids,
  70. timeout=self._otp_wait_timeout_seconds,
  71. keyword="openai",
  72. )
  73. diagnostics = dict(getattr(mailbox, "last_wait_diagnostics", {}) or {})
  74. first_seen_at = diagnostics.get("first_message_seen_at")
  75. matched_at = diagnostics.get("matched_message_at")
  76. poll_count = diagnostics.get("poll_count") or 0
  77. message_scan_count = diagnostics.get("message_scan_count") or 0
  78. first_seen_delta = (
  79. round(float(first_seen_at) - float(self._otp_sent_at or started_at), 2)
  80. if first_seen_at is not None and self._otp_sent_at is not None
  81. else None
  82. )
  83. matched_delta = (
  84. round(float(matched_at) - float(self._otp_sent_at or started_at), 2)
  85. if matched_at is not None and self._otp_sent_at is not None
  86. else None
  87. )
  88. self._last_otp_wait_diagnostics = {
  89. "otp_mailbox_poll_count": int(poll_count),
  90. "otp_mailbox_message_scan_count": int(message_scan_count),
  91. "otp_mailbox_first_seen_after_seconds": first_seen_delta,
  92. "otp_mailbox_matched_after_seconds": matched_delta,
  93. }
  94. if diagnostics.get("aborted"):
  95. self._last_otp_wait_diagnostics["otp_mailbox_aborted"] = True
  96. self._last_otp_wait_diagnostics["otp_mailbox_abort_reason"] = str(
  97. diagnostics.get("abort_reason") or ""
  98. ).strip()
  99. self._log(
  100. "otp mailbox diagnostics: "
  101. f"polls={poll_count} scanned={message_scan_count} "
  102. f"first_seen_after={first_seen_delta if first_seen_delta is not None else '-'}s "
  103. f"matched_after={matched_delta if matched_delta is not None else '-'}s"
  104. )
  105. self._signup_otp_before_ids = set()
  106. if code:
  107. self._log(f"verification code received: {code}")
  108. return code
  109. if diagnostics.get("aborted"):
  110. self._last_otp_wait_failure_reason = "mailbox_aborted_rotation"
  111. self._log("verification code wait aborted due to cfmail rotation")
  112. return None
  113. if message_scan_count <= 0:
  114. self._last_otp_wait_failure_reason = "mailbox_timeout_no_message"
  115. else:
  116. self._last_otp_wait_failure_reason = "mailbox_timeout_no_match"
  117. self._log(
  118. "verification code timed out "
  119. f"after {round(time.time() - started_at, 2)}s"
  120. )
  121. return None
  122. email_id = (self.email_info or {}).get("service_id")
  123. code = self.email_service.get_verification_code(
  124. email=self.email,
  125. email_id=email_id,
  126. timeout=self._otp_wait_timeout_seconds,
  127. pattern=OTP_CODE_PATTERN,
  128. otp_sent_at=self._otp_sent_at,
  129. )
  130. if code:
  131. self._log(f"verification code received: {code}")
  132. return code
  133. self._log(f"verification code timed out after {self._otp_wait_timeout_seconds}s")
  134. return None
  135. except Exception as exc:
  136. self._log(f"get_verification_code failed: {exc}")
  137. return None
  138. def _validate_verification_code(self, code: str) -> bool:
  139. if self.session is None:
  140. return False
  141. try:
  142. response, session = self._session_request(
  143. session=self.session,
  144. method="POST",
  145. url=OPENAI_API_ENDPOINTS["validate_otp"],
  146. label="validate otp",
  147. refresh_session=self._refresh_registration_session,
  148. headers={
  149. "referer": "https://auth.openai.com/email-verification",
  150. "accept": "application/json",
  151. "content-type": "application/json",
  152. },
  153. data=json.dumps({"code": code}),
  154. )
  155. self.session = session
  156. self._log(f"validate otp status: {response.status_code}")
  157. return response.status_code == 200
  158. except Exception as exc:
  159. self._log(f"validate_verification_code failed: {exc}")
  160. return False