refactor!: a lot of stuff
This commit is contained in:
parent
d6396e4050
commit
0af7179596
15 changed files with 663 additions and 302 deletions
|
|
@ -14,12 +14,17 @@ from typing import Callable
|
|||
from urllib.parse import parse_qs, urlencode, urlparse
|
||||
|
||||
import aiohttp
|
||||
from playwright.async_api import async_playwright, Page, BrowserContext
|
||||
from playwright.async_api import (
|
||||
async_playwright,
|
||||
Error as PlaywrightError,
|
||||
Page,
|
||||
BrowserContext,
|
||||
)
|
||||
|
||||
from browser import launch as launch_browser
|
||||
from email_providers import BaseProvider
|
||||
from providers.base import ProviderTokens
|
||||
from .tokens import CLIENT_ID, save_tokens
|
||||
from .tokens import CLIENT_ID
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -46,9 +51,9 @@ async def save_error_screenshot(page: Page | None, step: str):
|
|||
filename = screenshots_dir / f"error_{step}_{timestamp}.png"
|
||||
try:
|
||||
await page.screenshot(path=str(filename))
|
||||
logger.error(f"Screenshot saved: {filename}")
|
||||
except:
|
||||
pass
|
||||
logger.error("Screenshot saved: %s", filename)
|
||||
except PlaywrightError as e:
|
||||
logger.warning("Failed to save screenshot at step %s: %s", step, e)
|
||||
|
||||
|
||||
def generate_password(length: int = 20) -> str:
|
||||
|
|
@ -204,8 +209,7 @@ def generate_state() -> str:
|
|||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def build_authorize_url(verifier: str, challenge: str, state: str) -> str:
|
||||
del verifier
|
||||
def build_authorize_url(challenge: str, state: str) -> str:
|
||||
params = {
|
||||
"response_type": "code",
|
||||
"client_id": CLIENT_ID,
|
||||
|
|
@ -222,26 +226,33 @@ def build_authorize_url(verifier: str, challenge: str, state: str) -> str:
|
|||
|
||||
|
||||
async def exchange_code_for_tokens(code: str, verifier: str) -> ProviderTokens:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
payload = {
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": CLIENT_ID,
|
||||
"code": code,
|
||||
"code_verifier": verifier,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
}
|
||||
async with session.post(TOKEN_URL, data=payload) as resp:
|
||||
if not resp.ok:
|
||||
text = await resp.text()
|
||||
raise RuntimeError(f"Token exchange failed: {resp.status} {text}")
|
||||
body = await resp.json()
|
||||
payload = {
|
||||
"grant_type": "authorization_code",
|
||||
"client_id": CLIENT_ID,
|
||||
"code": code,
|
||||
"code_verifier": verifier,
|
||||
"redirect_uri": REDIRECT_URI,
|
||||
}
|
||||
timeout = aiohttp.ClientTimeout(total=20)
|
||||
try:
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.post(TOKEN_URL, data=payload) as resp:
|
||||
if not resp.ok:
|
||||
text = await resp.text()
|
||||
raise RuntimeError(f"Token exchange failed: {resp.status} {text}")
|
||||
body = await resp.json()
|
||||
except (aiohttp.ClientError, TimeoutError) as e:
|
||||
raise RuntimeError(f"Token exchange request error: {e}") from e
|
||||
|
||||
expires_in = int(body["expires_in"])
|
||||
return ProviderTokens(
|
||||
access_token=body["access_token"],
|
||||
refresh_token=body["refresh_token"],
|
||||
expires_at=time.time() + expires_in,
|
||||
)
|
||||
try:
|
||||
expires_in = int(body["expires_in"])
|
||||
return ProviderTokens(
|
||||
access_token=body["access_token"],
|
||||
refresh_token=body["refresh_token"],
|
||||
expires_at=time.time() + expires_in,
|
||||
)
|
||||
except (KeyError, TypeError, ValueError) as e:
|
||||
raise RuntimeError(f"Token exchange response parse error: {e}") from e
|
||||
|
||||
|
||||
async def get_latest_code(email_provider: BaseProvider, email: str) -> str | None:
|
||||
|
|
@ -270,6 +281,24 @@ async def click_continue(page: Page, timeout_ms: int = 10000):
|
|||
await btn.click()
|
||||
|
||||
|
||||
async def click_any_visible_button(
|
||||
page: Page,
|
||||
labels: list[str],
|
||||
timeout_ms: int = 2000,
|
||||
) -> bool:
|
||||
for label in labels:
|
||||
button = page.get_by_role("button", name=label)
|
||||
if await button.count() == 0:
|
||||
continue
|
||||
try:
|
||||
await button.first.wait_for(state="visible", timeout=timeout_ms)
|
||||
await button.first.click(timeout=timeout_ms)
|
||||
return True
|
||||
except PlaywrightError:
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
async def wait_for_signup_stabilization(
|
||||
page: Page,
|
||||
source_url: str,
|
||||
|
|
@ -288,12 +317,12 @@ async def wait_for_signup_stabilization(
|
|||
|
||||
async def register_chatgpt_account(
|
||||
email_provider_factory: Callable[[BrowserContext], BaseProvider] | None = None,
|
||||
) -> bool:
|
||||
) -> ProviderTokens | None:
|
||||
logger.info("=== Starting ChatGPT account registration ===")
|
||||
|
||||
if email_provider_factory is None:
|
||||
logger.error("No email provider factory configured")
|
||||
return False
|
||||
return None
|
||||
|
||||
birth_month, birth_day, birth_year = generate_birthdate_90s()
|
||||
|
||||
|
|
@ -321,7 +350,7 @@ async def register_chatgpt_account(
|
|||
full_name = generate_name()
|
||||
verifier, challenge = generate_pkce_pair()
|
||||
oauth_state = generate_state()
|
||||
authorize_url = build_authorize_url(verifier, challenge, oauth_state)
|
||||
authorize_url = build_authorize_url(challenge, oauth_state)
|
||||
|
||||
logger.info("[2/5] Registering ChatGPT for %s", email)
|
||||
chatgpt_page = await context.new_page()
|
||||
|
|
@ -352,19 +381,18 @@ async def register_chatgpt_account(
|
|||
raise AutomationError(
|
||||
"email_provider", "Email provider returned no verification message"
|
||||
)
|
||||
logger.info("[3/5] Verification code extracted: %s", code)
|
||||
logger.info("[3/5] Verification code extracted")
|
||||
|
||||
await chatgpt_page.bring_to_front()
|
||||
code_input = chatgpt_page.get_by_placeholder("Code")
|
||||
if await code_input.count() > 0:
|
||||
await code_input.fill(code)
|
||||
await code_input.first.wait_for(state="visible", timeout=10000)
|
||||
await code_input.first.fill(code)
|
||||
await click_continue(chatgpt_page)
|
||||
|
||||
logger.info("[4/5] Setting profile...")
|
||||
name_input = chatgpt_page.get_by_placeholder("Full name")
|
||||
await name_input.first.wait_for(state="visible", timeout=20000)
|
||||
if await name_input.count() > 0:
|
||||
await name_input.fill(full_name)
|
||||
await name_input.first.fill(full_name)
|
||||
|
||||
await fill_date_field(chatgpt_page, birth_month, birth_day, birth_year)
|
||||
profile_url = chatgpt_page.url
|
||||
|
|
@ -387,45 +415,42 @@ async def register_chatgpt_account(
|
|||
oauth_page.on("request", handle_request)
|
||||
|
||||
await oauth_page.goto(authorize_url, wait_until="domcontentloaded")
|
||||
await oauth_page.locator(
|
||||
'input[type="email"], input[name="email"]'
|
||||
).first.wait_for(state="visible", timeout=20000)
|
||||
|
||||
email_input = oauth_page.locator('input[type="email"], input[name="email"]')
|
||||
if await email_input.count() > 0:
|
||||
await email_input.first.wait_for(state="visible", timeout=10000)
|
||||
await email_input.first.fill(email)
|
||||
|
||||
continue_button = oauth_page.get_by_role("button", name="Continue")
|
||||
if await continue_button.count() > 0:
|
||||
await continue_button.first.click()
|
||||
await oauth_page.locator('input[type="password"]').first.wait_for(
|
||||
state="visible", timeout=20000
|
||||
await click_any_visible_button(
|
||||
oauth_page, ["Continue"], timeout_ms=4000
|
||||
)
|
||||
|
||||
password_input = oauth_page.locator('input[type="password"]')
|
||||
if await password_input.count() > 0:
|
||||
await password_input.first.wait_for(state="visible", timeout=10000)
|
||||
await password_input.first.fill(password)
|
||||
continue_button = oauth_page.get_by_role("button", name="Continue")
|
||||
if await continue_button.count() > 0:
|
||||
await continue_button.first.click()
|
||||
await click_any_visible_button(
|
||||
oauth_page, ["Continue"], timeout_ms=4000
|
||||
)
|
||||
|
||||
for label in ["Continue", "Allow", "Authorize"]:
|
||||
button = oauth_page.get_by_role("button", name=label)
|
||||
if await button.count() > 0:
|
||||
try:
|
||||
await button.first.click(timeout=5000)
|
||||
await oauth_page.wait_for_timeout(500)
|
||||
except Exception:
|
||||
pass
|
||||
for _ in range(6):
|
||||
if redirect_url_captured:
|
||||
break
|
||||
clicked = await click_any_visible_button(
|
||||
oauth_page,
|
||||
["Continue", "Allow", "Authorize"],
|
||||
timeout_ms=2000,
|
||||
)
|
||||
if clicked:
|
||||
await asyncio.sleep(0.4)
|
||||
else:
|
||||
await asyncio.sleep(0.4)
|
||||
|
||||
if not redirect_url_captured:
|
||||
try:
|
||||
await oauth_page.wait_for_timeout(4000)
|
||||
current_url = oauth_page.url
|
||||
if "localhost:1455" in current_url and "code=" in current_url:
|
||||
redirect_url_captured = current_url
|
||||
logger.info("Captured OAuth redirect from page URL")
|
||||
except Exception:
|
||||
except PlaywrightError:
|
||||
pass
|
||||
|
||||
if not redirect_url_captured:
|
||||
|
|
@ -446,20 +471,18 @@ async def register_chatgpt_account(
|
|||
raise AutomationError("oauth", "OAuth state mismatch", oauth_page)
|
||||
|
||||
tokens = await exchange_code_for_tokens(auth_code, verifier)
|
||||
save_tokens(tokens)
|
||||
logger.info("OAuth tokens saved successfully")
|
||||
logger.info("OAuth tokens fetched successfully")
|
||||
|
||||
return True
|
||||
return tokens
|
||||
|
||||
except AutomationError as e:
|
||||
logger.error(f"Error at step [{e.step}]: {e.message}")
|
||||
await save_error_screenshot(e.page, e.step)
|
||||
return False
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error: {e}")
|
||||
await save_error_screenshot(current_page, "unexpected")
|
||||
return False
|
||||
return None
|
||||
finally:
|
||||
if managed:
|
||||
await asyncio.sleep(2)
|
||||
await managed.close()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue