1
0
Fork 0

refactor!: a lot of stuff

This commit is contained in:
Arthur K. 2026-03-02 21:14:20 +03:00
parent d6396e4050
commit 0af7179596
Signed by: wzray
GPG key ID: B97F30FDC4636357
15 changed files with 663 additions and 302 deletions

View file

@ -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()