register_http.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. """HTTP/session helpers for ChatGPT registration."""
  2. from __future__ import annotations
  3. import json
  4. import os
  5. import secrets
  6. import time
  7. import urllib.parse
  8. from typing import Any, Callable
  9. from .constants import (
  10. CHATGPT_CSRF_URL,
  11. CHATGPT_SIGNIN_URL,
  12. DEFAULT_PASSWORD_LENGTH,
  13. OPENAI_API_ENDPOINTS,
  14. OPENAI_PAGE_TYPES,
  15. OAUTH_REDIRECT_URI,
  16. PASSWORD_CHARSET,
  17. generate_random_user_info,
  18. )
  19. DEFAULT_ADD_PHONE_OAUTH_OTP_TIMEOUT_SECONDS = 90
  20. def _deduplicate_cross_domain_cookies(session: Any, target_domain: str) -> None:
  21. """Remove cookies from non-target domains that conflict with target domain cookies.
  22. curl_cffi raises an error when multiple cookies with the same name exist
  23. on different domains (e.g. __cf_bm on .auth.openai.com vs .sentinel.openai.com).
  24. This helper keeps only the cookie scoped to *target_domain*.
  25. """
  26. try:
  27. jar = session.cookies
  28. target_suffix = target_domain if target_domain.startswith(".") else f".{target_domain}"
  29. names_for_target: set[str] = set()
  30. for cookie in jar:
  31. domain = str(getattr(cookie, "domain", "") or "")
  32. if domain == target_suffix or domain == target_domain:
  33. names_for_target.add(cookie.name)
  34. to_remove: list[Any] = []
  35. for cookie in jar:
  36. domain = str(getattr(cookie, "domain", "") or "")
  37. if cookie.name in names_for_target and domain != target_suffix and domain != target_domain:
  38. to_remove.append(cookie)
  39. for cookie in to_remove:
  40. jar.clear(domain=getattr(cookie, "domain", ""), path=getattr(cookie, "path", "/"), name=cookie.name)
  41. except Exception:
  42. pass
  43. def _build_sentinel_header(
  44. self,
  45. sentinel: str,
  46. device_id: str,
  47. flow: str,
  48. *,
  49. client: Any | None = None,
  50. ) -> str:
  51. sentinel_client = client or self.http_client
  52. build_header = getattr(sentinel_client, "build_sentinel_header", None)
  53. if callable(build_header):
  54. try:
  55. return str(build_header(device_id=device_id, flow=flow, token=sentinel))
  56. except Exception:
  57. pass
  58. return json.dumps(
  59. {
  60. "p": "",
  61. "t": "",
  62. "c": sentinel,
  63. "id": device_id,
  64. "flow": flow,
  65. },
  66. separators=(",", ":"),
  67. )
  68. def _oauth_json_headers(self, *, referer: str, device_id: str) -> dict[str, str]:
  69. return {
  70. "accept": "application/json",
  71. "content-type": "application/json",
  72. "origin": "https://auth.openai.com",
  73. "referer": referer,
  74. "oai-device-id": device_id,
  75. "user-agent": self.http_client.default_headers.get("User-Agent", "Mozilla/5.0"),
  76. "sec-ch-ua": self.http_client.default_headers.get("sec-ch-ua", ""),
  77. "sec-ch-ua-mobile": self.http_client.default_headers.get("sec-ch-ua-mobile", ""),
  78. "sec-ch-ua-platform": self.http_client.default_headers.get("sec-ch-ua-platform", ""),
  79. }
  80. def _is_transient_transport_error(self, exc: Exception) -> bool:
  81. message = str(exc or "").lower()
  82. markers = (
  83. "connection closed abruptly",
  84. "connection timed out",
  85. "connection reset",
  86. "connection refused",
  87. "tls connect error",
  88. "recv failure",
  89. "send failure",
  90. "http/2 stream",
  91. "operation timed out",
  92. "curl: (7)",
  93. "curl: (28)",
  94. "curl: (35)",
  95. "curl: (52)",
  96. "curl: (55)",
  97. "curl: (56)",
  98. )
  99. return any(marker in message for marker in markers)
  100. def _session_request(
  101. self,
  102. *,
  103. session: Any,
  104. method: str,
  105. url: str,
  106. label: str,
  107. refresh_session: Callable[[], Any] | None = None,
  108. max_attempts: int = 3,
  109. retry_delay: float = 1.0,
  110. **kwargs: Any,
  111. ) -> tuple[Any, Any]:
  112. current_session = session
  113. last_exc: Exception | None = None
  114. for attempt in range(1, max_attempts + 1):
  115. try:
  116. _deduplicate_cross_domain_cookies(current_session, "auth.openai.com")
  117. response = getattr(current_session, method.lower())(url, **kwargs)
  118. return response, current_session
  119. except Exception as exc:
  120. last_exc = exc
  121. if attempt >= max_attempts or not self._is_transient_transport_error(exc):
  122. raise
  123. self._log(f"{label}: transient transport error, retry {attempt}/{max_attempts}: {exc}")
  124. if refresh_session is not None:
  125. current_session = refresh_session()
  126. time.sleep(retry_delay * attempt)
  127. if last_exc is not None:
  128. raise last_exc
  129. raise RuntimeError(f"{label}: request failed without exception")
  130. def _refresh_registration_session(self) -> Any:
  131. cookie_pairs: dict[str, str] = {}
  132. current_session = getattr(self, 'session', None)
  133. try:
  134. cookies = getattr(current_session, 'cookies', None)
  135. if cookies is not None:
  136. jar = getattr(cookies, 'jar', None)
  137. if jar is not None:
  138. for item in list(jar):
  139. name = str(getattr(item, 'name', '') or '').strip()
  140. value = str(getattr(item, 'value', '') or '').strip()
  141. if name:
  142. cookie_pairs[name] = value
  143. for key, value in dict(cookies).items():
  144. if key:
  145. cookie_pairs[str(key)] = str(value)
  146. except Exception:
  147. pass
  148. try:
  149. self.http_client.close()
  150. except Exception:
  151. pass
  152. new_session = self.http_client.session
  153. try:
  154. new_session.cookies.update(cookie_pairs)
  155. except Exception:
  156. pass
  157. self.session = new_session
  158. return new_session
  159. def _check_ip_location(self) -> tuple[bool, str | None]:
  160. try:
  161. return self.http_client.check_ip_location()
  162. except Exception as exc:
  163. self._log(f"check_ip_location failed: {exc}")
  164. return False, None
  165. def _create_email(self) -> bool:
  166. self._last_mailbox_error_kind = ""
  167. self._last_mailbox_error_stage = ""
  168. self._last_mailbox_error_message = ""
  169. if self.email:
  170. self.email_info = {"email": self.email}
  171. self._log(f"using provided mailbox: {self.email}")
  172. return True
  173. for attempt in range(1, self.create_email_max_attempts + 1):
  174. try:
  175. candidate_info = self.email_service.create_email()
  176. except Exception as exc:
  177. self._last_mailbox_error_stage = "create_email"
  178. self._last_mailbox_error_message = str(exc or "").strip()
  179. self._last_mailbox_error_kind = "transport_error" if self._is_transient_transport_error(exc) else "provider_error"
  180. self._log(f"create_email failed: {exc}")
  181. return False
  182. candidate_email = str((candidate_info or {}).get("email") or "").strip()
  183. if not candidate_email:
  184. self._last_mailbox_error_stage = "create_email"
  185. self._last_mailbox_error_kind = "provider_error"
  186. self._last_mailbox_error_message = "create_email returned no email address"
  187. self._log("create_email returned no email address")
  188. return False
  189. if self.mailbox_dedupe_store is not None and not self.mailbox_dedupe_store.reserve(candidate_email):
  190. self._log(
  191. f"duplicate mailbox discarded ({attempt}/{self.create_email_max_attempts}): {candidate_email}"
  192. )
  193. continue
  194. self.email_info = candidate_info
  195. self.email = candidate_email
  196. self._reserved_email = candidate_email
  197. self._log(f"created mailbox: {self.email}")
  198. return True
  199. self._log("create_email exhausted unique mailbox retries")
  200. self._last_mailbox_error_stage = "create_email"
  201. self._last_mailbox_error_kind = "provider_error"
  202. self._last_mailbox_error_message = "create_email exhausted unique mailbox retries"
  203. return False
  204. def _init_session(self) -> bool:
  205. try:
  206. self.session = self.http_client.session
  207. return True
  208. except Exception as exc:
  209. self._log(f"init_session failed: {exc}")
  210. return False
  211. def _start_oauth_via_chatgpt(self) -> bool:
  212. """Initiate OAuth flow via chatgpt.com (csrf + signin/openai) to avoid add_phone."""
  213. import uuid as _uuid
  214. from .oauth import OAuthStart
  215. if self.session is None:
  216. return False
  217. try:
  218. csrf_resp = self.session.get(CHATGPT_CSRF_URL, timeout=15)
  219. if csrf_resp.status_code != 200:
  220. self._log(f"chatgpt csrf failed: {csrf_resp.status_code}")
  221. return False
  222. csrf_token = csrf_resp.json().get("csrfToken", "")
  223. if not csrf_token:
  224. self._log("chatgpt csrf token empty")
  225. return False
  226. device_id = str(_uuid.uuid4())
  227. self.session.cookies.set("oai-did", device_id, domain=".openai.com")
  228. signin_url = f"{CHATGPT_SIGNIN_URL}?prompt=login&ext-oai-did={device_id}"
  229. signin_resp = self.session.post(
  230. signin_url,
  231. data=f"callbackUrl=https%3A%2F%2Fchatgpt.com%2F&csrfToken={csrf_token}&json=true",
  232. headers={"content-type": "application/x-www-form-urlencoded"},
  233. timeout=15,
  234. allow_redirects=False,
  235. )
  236. if signin_resp.status_code != 200:
  237. self._log(f"chatgpt signin failed: {signin_resp.status_code}")
  238. return False
  239. auth_url = signin_resp.json().get("url", "")
  240. if not auth_url:
  241. self._log("chatgpt signin returned no url")
  242. return False
  243. parsed = urllib.parse.urlparse(auth_url)
  244. params = dict(urllib.parse.parse_qsl(parsed.query))
  245. self.oauth_start = OAuthStart(
  246. auth_url=auth_url,
  247. state=params.get("state", ""),
  248. code_verifier="",
  249. redirect_uri=OAUTH_REDIRECT_URI,
  250. )
  251. self._log("oauth flow initialized via chatgpt.com")
  252. return True
  253. except Exception as exc:
  254. self._log(f"chatgpt oauth init failed: {exc}")
  255. return False
  256. def _start_oauth(self) -> bool:
  257. try:
  258. self.oauth_start = self.oauth_manager.start_oauth()
  259. self._log("oauth flow initialized")
  260. return True
  261. except Exception as exc:
  262. self._log(f"oauth init failed: {exc}")
  263. return False
  264. def _get_device_id(self) -> str | None:
  265. if not self.oauth_start or self.session is None:
  266. return None
  267. try:
  268. self.session.get(self.oauth_start.auth_url, timeout=15)
  269. device_id = str(self.session.cookies.get("oai-did") or "").strip()
  270. if device_id:
  271. self._log(f"device_id acquired: {device_id}")
  272. return device_id
  273. self._log("device_id missing from oauth bootstrap cookies")
  274. return None
  275. except Exception as exc:
  276. self._log(f"get_device_id failed: {exc}")
  277. return None
  278. def _check_sentinel(self, did: str, *, flow: str = "authorize_continue") -> str | None:
  279. try:
  280. token = self.http_client.check_sentinel(did, flow=flow)
  281. if token:
  282. self._log("sentinel token acquired")
  283. else:
  284. self._log("sentinel token unavailable")
  285. return token
  286. except Exception as exc:
  287. self._log(f"check_sentinel failed: {exc}")
  288. return None
  289. def _submit_signup_form(self, did: str, sen_token: str | None) -> SignupFormResult:
  290. if self.session is None or not self.email:
  291. return SignupFormResult(success=False, error_message="session or email missing")
  292. try:
  293. self._last_signup_http_status = None
  294. self._last_signup_error_code = ""
  295. self._last_signup_error_message = ""
  296. self._last_signup_error_body = ""
  297. signup_body = json.dumps(
  298. {
  299. "username": {"value": self.email, "kind": "email"},
  300. "screen_hint": "signup",
  301. }
  302. )
  303. headers = {
  304. "referer": "https://auth.openai.com/create-account",
  305. "accept": "application/json",
  306. "content-type": "application/json",
  307. }
  308. if sen_token:
  309. sentinel = self._build_sentinel_header(
  310. sen_token,
  311. did,
  312. "authorize_continue",
  313. )
  314. headers["openai-sentinel-token"] = sentinel
  315. response = self.session.post(
  316. OPENAI_API_ENDPOINTS["signup"],
  317. headers=headers,
  318. data=signup_body,
  319. )
  320. self._log(f"signup form status: {response.status_code}")
  321. self._last_signup_http_status = int(response.status_code)
  322. if response.status_code != 200:
  323. self._last_signup_error_body = str(response.text or "")[:240]
  324. try:
  325. error_payload = response.json()
  326. except Exception:
  327. error_payload = None
  328. if isinstance(error_payload, dict):
  329. error = error_payload.get("error")
  330. if isinstance(error, dict):
  331. self._last_signup_error_code = str(error.get("code") or "").strip()
  332. self._last_signup_error_message = str(error.get("message") or "").strip()
  333. return SignupFormResult(
  334. success=False,
  335. error_message=f"HTTP {response.status_code}: {response.text[:200]}",
  336. )
  337. try:
  338. response_data = response.json()
  339. except Exception as exc:
  340. return SignupFormResult(success=False, error_message=f"signup json parse failed: {exc}")
  341. page_type = str(((response_data.get("page") or {}).get("type")) or "").strip()
  342. is_existing = page_type == OPENAI_PAGE_TYPES["EMAIL_OTP_VERIFICATION"]
  343. self._is_existing_account = is_existing
  344. if is_existing:
  345. self._log("existing account detected; switching to login-like OTP flow")
  346. else:
  347. self._log(f"signup page type: {page_type or 'unknown'}")
  348. self._log(f"signup response data keys: {list(response_data.keys()) if isinstance(response_data, dict) else 'not dict'}")
  349. self._log(f"signup response data: {json.dumps(response_data, default=str)[:500]}")
  350. return SignupFormResult(
  351. success=True,
  352. page_type=page_type,
  353. is_existing_account=is_existing,
  354. response_data=response_data,
  355. )
  356. except Exception as exc:
  357. self._log(f"submit_signup_form failed: {exc}")
  358. return SignupFormResult(success=False, error_message=str(exc))
  359. def _register_password(self) -> bool:
  360. if self.session is None or not self.email:
  361. return False
  362. try:
  363. if not self.password:
  364. self.password = self._generate_password()
  365. _deduplicate_cross_domain_cookies(self.session, "auth.openai.com")
  366. device_id = getattr(self, "_last_device_id", "") or ""
  367. sentinel_token = getattr(self, "_last_sentinel_token", None)
  368. try:
  369. self.session.get(
  370. "https://auth.openai.com/create-account/password",
  371. headers={"referer": "https://auth.openai.com/create-account"},
  372. )
  373. except Exception:
  374. pass
  375. payload = json.dumps({"password": self.password, "username": self.email})
  376. headers: dict[str, str] = {
  377. "referer": "https://auth.openai.com/create-account/password",
  378. "accept": "application/json",
  379. "content-type": "application/json",
  380. }
  381. if device_id:
  382. headers["oai-device-id"] = device_id
  383. if sentinel_token:
  384. headers["openai-sentinel-token"] = self._build_sentinel_header(
  385. sentinel_token, device_id, "authorize_continue"
  386. )
  387. response = self.session.post(
  388. OPENAI_API_ENDPOINTS["register"],
  389. headers=headers,
  390. data=payload,
  391. )
  392. self._log(f"register password status: {response.status_code}")
  393. if response.status_code != 200:
  394. self._log(f"register password failed body: {response.text[:240]}")
  395. return False
  396. return True
  397. except Exception as exc:
  398. self._log(f"register_password failed: {exc}")
  399. return False
  400. def _send_verification_code(self) -> bool:
  401. if self.session is None:
  402. return False
  403. try:
  404. self._signup_otp_before_ids = self._capture_mailbox_ids()
  405. self._otp_sent_at = time.time()
  406. self._log(
  407. "send otp mailbox baseline captured: "
  408. f"{len(self._signup_otp_before_ids)} existing ids"
  409. )
  410. response, session = self._session_request(
  411. session=self.session,
  412. method="GET",
  413. url=OPENAI_API_ENDPOINTS["send_otp"],
  414. label="send otp",
  415. refresh_session=self._refresh_registration_session,
  416. headers={
  417. "referer": "https://auth.openai.com/create-account/password",
  418. "accept": "application/json",
  419. },
  420. )
  421. self.session = session
  422. self._log(f"send otp status: {response.status_code}")
  423. return response.status_code == 200
  424. except Exception as exc:
  425. self._log(f"send_verification_code failed: {exc}")
  426. return False
  427. def _create_user_account(self) -> bool:
  428. if self.session is None:
  429. return False
  430. try:
  431. self._last_create_account_http_status = None
  432. self._last_create_account_error_code = ""
  433. self._last_create_account_error_message = ""
  434. self._last_create_account_error_body = ""
  435. user_info = generate_random_user_info()
  436. self._log(f"generated profile: {user_info['name']} / {user_info['birthdate']}")
  437. _deduplicate_cross_domain_cookies(self.session, "auth.openai.com")
  438. device_id = getattr(self, "_last_device_id", "") or ""
  439. sentinel_token = getattr(self, "_last_sentinel_token", None)
  440. headers = {
  441. "referer": "https://auth.openai.com/about-you",
  442. "accept": "application/json",
  443. "content-type": "application/json",
  444. }
  445. if device_id:
  446. headers["oai-device-id"] = device_id
  447. if sentinel_token:
  448. headers["openai-sentinel-token"] = self._build_sentinel_header(
  449. sentinel_token, device_id, "authorize_continue"
  450. )
  451. response = self.session.post(
  452. OPENAI_API_ENDPOINTS["create_account"],
  453. headers=headers,
  454. data=json.dumps(user_info),
  455. )
  456. self._last_create_account_http_status = int(response.status_code)
  457. self._log(f"create account status: {response.status_code}")
  458. if response.status_code != 200:
  459. self._last_create_account_error_body = str(response.text or "")[:240]
  460. self._log(f"create account body: {self._last_create_account_error_body}")
  461. try:
  462. error_payload = response.json()
  463. except Exception:
  464. error_payload = {}
  465. error_info = error_payload.get("error") if isinstance(error_payload, dict) else {}
  466. if isinstance(error_info, dict):
  467. self._last_create_account_error_code = str(error_info.get("code") or "").strip()
  468. self._last_create_account_error_message = str(error_info.get("message") or "").strip()
  469. if self._last_create_account_error_code or self._last_create_account_error_message:
  470. self._log(
  471. "create account classified error: "
  472. f"code={self._last_create_account_error_code or '-'} "
  473. f"message={self._last_create_account_error_message or '-'}"
  474. )
  475. return False
  476. try:
  477. create_resp = response.json()
  478. self._log(f"create account response keys: {list(create_resp.keys())}")
  479. # Store continue_url from response (bypass workspace flow)
  480. curl = str(create_resp.get("continue_url") or "").strip()
  481. page_info = create_resp.get("page") or {}
  482. page_type = str(page_info.get("type") or "").strip() if isinstance(page_info, dict) else ""
  483. continue_host = ""
  484. continue_kind = "unknown"
  485. if curl:
  486. parsed_curl = urllib.parse.urlparse(curl)
  487. continue_host = parsed_curl.netloc
  488. if "callback/openai" in curl:
  489. continue_kind = "callback_openai"
  490. elif "add-phone" in curl:
  491. continue_kind = "add_phone"
  492. elif "workspace" in curl:
  493. continue_kind = "workspace"
  494. else:
  495. continue_kind = f"other:{parsed_curl.path[:40]}"
  496. self._log(
  497. f"create_account result: page_type={page_type}, "
  498. f"continue_kind={continue_kind}, continue_host={continue_host}"
  499. )
  500. if curl:
  501. self._create_account_continue_url = curl
  502. self._log(f"continue_url from create_account: {curl[:120]}")
  503. except Exception:
  504. pass
  505. return True
  506. except Exception as exc:
  507. self._log(f"create_user_account failed: {exc}")
  508. return False
  509. def _email_domain(self) -> str:
  510. email = str(self.email or "").strip()
  511. if "@" not in email:
  512. return ""
  513. return email.rsplit("@", 1)[-1].strip().lower()
  514. def _load_add_phone_oauth_max_attempts(self) -> int:
  515. raw = str(os.getenv("ZHUCE6_ADD_PHONE_OAUTH_MAX_ATTEMPTS", "2") or "2").strip()
  516. try:
  517. value = int(raw)
  518. except Exception:
  519. value = 2
  520. return max(1, min(value, 3))
  521. def _load_wait_otp_timeout_seconds(self) -> int:
  522. raw = str(os.getenv("ZHUCE6_WAIT_OTP_TIMEOUT_SECONDS", "180") or "180").strip()
  523. try:
  524. value = int(raw)
  525. except Exception:
  526. value = 180
  527. return max(60, min(value, 300))
  528. def _load_add_phone_oauth_otp_timeout_seconds(self) -> int:
  529. raw = str(
  530. os.getenv(
  531. "ZHUCE6_ADD_PHONE_OAUTH_OTP_TIMEOUT_SECONDS",
  532. str(DEFAULT_ADD_PHONE_OAUTH_OTP_TIMEOUT_SECONDS),
  533. )
  534. or str(DEFAULT_ADD_PHONE_OAUTH_OTP_TIMEOUT_SECONDS)
  535. ).strip()
  536. try:
  537. value = int(raw)
  538. except Exception:
  539. value = DEFAULT_ADD_PHONE_OAUTH_OTP_TIMEOUT_SECONDS
  540. return max(30, min(value, 180))
  541. def _load_post_create_login_delay_seconds(self) -> int:
  542. raw = str(os.getenv("ZHUCE6_POST_CREATE_LOGIN_DELAY_SECONDS", "8") or "8").strip()
  543. try:
  544. value = int(raw)
  545. except Exception:
  546. value = 8
  547. return max(0, min(value, 600))
  548. def _metadata(self, extra: dict[str, Any] | None = None) -> dict[str, Any]:
  549. payload: dict[str, Any] = {
  550. "email_domain": self._email_domain(),
  551. "signup_http_status": self._last_signup_http_status,
  552. "signup_error_code": self._last_signup_error_code,
  553. "signup_error_message": self._last_signup_error_message,
  554. "signup_auth_reset_count": getattr(self, "_signup_auth_reset_count", 0),
  555. "mailbox_error_kind": getattr(self, "_last_mailbox_error_kind", ""),
  556. "mailbox_error_stage": getattr(self, "_last_mailbox_error_stage", ""),
  557. "mailbox_error_message": getattr(self, "_last_mailbox_error_message", ""),
  558. "create_account_http_status": self._last_create_account_http_status,
  559. "create_account_error_code": self._last_create_account_error_code,
  560. "create_account_error_message": self._last_create_account_error_message,
  561. }
  562. if self._last_signup_error_body:
  563. payload["signup_error_body"] = self._last_signup_error_body
  564. if self._last_create_account_error_body:
  565. payload["create_account_error_body"] = self._last_create_account_error_body
  566. if self._last_otp_wait_failure_reason:
  567. payload["otp_wait_failure_reason"] = self._last_otp_wait_failure_reason
  568. if self._last_otp_wait_diagnostics:
  569. payload.update(self._last_otp_wait_diagnostics)
  570. if extra:
  571. payload.update(extra)
  572. return payload
  573. def _generate_password(self, length: int = DEFAULT_PASSWORD_LENGTH) -> str:
  574. required = [
  575. secrets.choice("abcdefghijklmnopqrstuvwxyz"),
  576. secrets.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
  577. secrets.choice("0123456789"),
  578. secrets.choice("!@#$%&*"),
  579. ]
  580. rest = [secrets.choice(PASSWORD_CHARSET) for _ in range(length - len(required))]
  581. combined = required + rest
  582. secrets.SystemRandom().shuffle(combined)
  583. return "".join(combined)
  584. def _auth_url(self, url: str) -> str:
  585. candidate = str(url or "").strip()
  586. if not candidate:
  587. return ""
  588. return urllib.parse.urljoin("https://auth.openai.com", candidate)