1
0
Fork 0

refactor!: change the entire purpose of this script

This commit is contained in:
Arthur K. 2026-03-01 19:32:10 +03:00
parent 217e176975
commit 71d1050adb
Signed by: wzray
GPG key ID: B97F30FDC4636357
20 changed files with 1124 additions and 872 deletions

View file

@ -0,0 +1,477 @@
import asyncio
import base64
import hashlib
import logging
import random
import re
import secrets
import string
import time
from datetime import datetime
from pathlib import Path
import os
from typing import Callable
from urllib.parse import parse_qs, urlencode, urlparse
import aiohttp
from playwright.async_api import async_playwright, 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
logger = logging.getLogger(__name__)
DATA_DIR = Path(os.environ.get("DATA_DIR", "./data"))
AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize"
TOKEN_URL = "https://auth.openai.com/oauth/token"
REDIRECT_URI = "http://localhost:1455/auth/callback"
SCOPE = "openid profile email offline_access"
class AutomationError(Exception):
def __init__(self, step: str, message: str, page: Page | None = None):
self.step = step
self.message = message
self.page = page
super().__init__(f"[{step}] {message}")
async def save_error_screenshot(page: Page | None, step: str):
if page:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
screenshots_dir = DATA_DIR / "screenshots"
screenshots_dir.mkdir(parents=True, exist_ok=True)
filename = screenshots_dir / f"error_{step}_{timestamp}.png"
try:
await page.screenshot(path=str(filename))
logger.error(f"Screenshot saved: {filename}")
except:
pass
def generate_password(length: int = 20) -> str:
alphabet = string.ascii_letters + string.digits
return "".join(random.choice(alphabet) for _ in range(length))
def generate_name() -> str:
first_names = [
"James",
"John",
"Robert",
"Michael",
"William",
"David",
"Richard",
"Joseph",
"Thomas",
"Charles",
"Christopher",
"Daniel",
"Matthew",
"Anthony",
"Mark",
"Donald",
"Steven",
"Paul",
"Andrew",
"Joshua",
]
last_names = [
"Smith",
"Johnson",
"Williams",
"Brown",
"Jones",
"Garcia",
"Miller",
"Davis",
"Rodriguez",
"Martinez",
"Hernandez",
"Lopez",
"Gonzalez",
"Wilson",
"Anderson",
"Thomas",
"Taylor",
"Moore",
"Jackson",
"Martin",
]
return f"{random.choice(first_names)} {random.choice(last_names)}"
def extract_verification_code(message: str) -> str | None:
normalized = re.sub(r"\s+", " ", message)
preferred = re.search(
r"Your\s+ChatGPT\s+code\s+is\s*(\d{6})",
normalized,
re.IGNORECASE,
)
if preferred:
return preferred.group(1)
openai_otp = re.search(r"OpenAI\s+otp.*?(\d{6})", normalized, re.IGNORECASE)
if openai_otp:
return openai_otp.group(1)
all_codes = re.findall(r"\b(\d{6})\b", normalized)
if all_codes:
return all_codes[-1]
return None
def generate_pkce_pair() -> tuple[str, str]:
verifier = secrets.token_urlsafe(64)
digest = hashlib.sha256(verifier.encode("utf-8")).digest()
challenge = base64.urlsafe_b64encode(digest).decode("utf-8").rstrip("=")
return verifier, challenge
def generate_state() -> str:
return secrets.token_urlsafe(32)
def build_authorize_url(verifier: str, challenge: str, state: str) -> str:
del verifier
params = {
"response_type": "code",
"client_id": CLIENT_ID,
"redirect_uri": REDIRECT_URI,
"scope": SCOPE,
"code_challenge": challenge,
"code_challenge_method": "S256",
"id_token_add_organizations": "true",
"codex_cli_simplified_flow": "true",
"state": state,
"originator": "opencode",
}
return f"{AUTHORIZE_URL}?{urlencode(params)}"
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()
expires_in = int(body["expires_in"])
return ProviderTokens(
access_token=body["access_token"],
refresh_token=body["refresh_token"],
expires_at=time.time() + expires_in,
)
async def get_new_verification_code(
email_provider: BaseProvider,
email: str,
used_codes: set[str],
timeout_seconds: int = 240,
) -> str | None:
attempts = max(1, timeout_seconds // 5)
for _ in range(attempts):
message = await email_provider.get_latest_message(email)
if message:
all_codes = re.findall(r"\b(\d{6})\b", message)
for candidate in all_codes:
if candidate not in used_codes:
return candidate
await asyncio.sleep(5)
return None
async def get_latest_code(email_provider: BaseProvider, email: str) -> str | None:
message = await email_provider.get_latest_message(email)
if not message:
return None
return extract_verification_code(message)
async def fill_date_field(page: Page, month: str, day: str, year: str):
month_field = page.locator('[data-type="month"]').first
if await month_field.count() == 0:
raise AutomationError("profile", "Missing birthday month field", page)
await month_field.scroll_into_view_if_needed()
await month_field.click()
await page.wait_for_timeout(120)
await page.keyboard.type(f"{month}{day}{year}")
await page.wait_for_timeout(200)
async def wait_for_signup_stabilization(page: Page):
try:
await page.wait_for_load_state("networkidle", timeout=15000)
except Exception:
logger.warning(
"Signup page did not reach networkidle quickly; continuing with fallback"
)
try:
await page.wait_for_load_state("domcontentloaded", timeout=10000)
except Exception:
pass
await page.wait_for_timeout(3000)
async def register_chatgpt_account(
email_provider_factory: Callable[[BrowserContext], BaseProvider] | None = None,
) -> bool:
logger.info("=== Starting ChatGPT account registration ===")
if email_provider_factory is None:
logger.error("No email provider factory configured")
return False
birth_month, birth_day, birth_year = "01", "15", "1995"
current_page: Page | None = None
redirect_url_captured: str | None = None
managed = None
try:
async with async_playwright() as p:
managed = await launch_browser(p)
browser = managed.browser
context = (
browser.contexts[0] if browser.contexts else await browser.new_context()
)
email_provider = email_provider_factory(context)
logger.info("[1/6] Getting new email from configured provider...")
email = await email_provider.get_new_email()
if not email:
raise AutomationError(
"email_provider", "Email provider returned empty email"
)
password = generate_password()
full_name = generate_name()
verifier, challenge = generate_pkce_pair()
oauth_state = generate_state()
authorize_url = build_authorize_url(verifier, challenge, oauth_state)
logger.info("[2/6] Registering ChatGPT for %s", email)
chatgpt_page = await context.new_page()
current_page = chatgpt_page
await chatgpt_page.goto("https://chatgpt.com")
await chatgpt_page.wait_for_load_state("domcontentloaded")
await chatgpt_page.get_by_text("Sign up for free", exact=True).click()
await chatgpt_page.wait_for_timeout(2000)
await chatgpt_page.locator('input[type="email"]').fill(email)
await chatgpt_page.wait_for_timeout(500)
await chatgpt_page.get_by_role(
"button", name="Continue", exact=True
).click()
await chatgpt_page.wait_for_timeout(3000)
await chatgpt_page.locator('input[type="password"]').fill(password)
await chatgpt_page.wait_for_timeout(500)
await chatgpt_page.get_by_role(
"button", name="Continue", exact=True
).click()
await chatgpt_page.wait_for_timeout(5000)
logger.info("[3/6] Getting verification message from email provider...")
code = await get_latest_code(email_provider, email)
if not code:
raise AutomationError(
"email_provider", "Email provider returned no verification message"
)
logger.info("[3/6] Verification code extracted: %s", code)
used_codes = {code}
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 chatgpt_page.wait_for_timeout(5000)
continue_btn = chatgpt_page.get_by_role(
"button", name="Continue", exact=True
)
if await continue_btn.count() > 0:
await continue_btn.click()
await chatgpt_page.wait_for_timeout(5000)
logger.info("[4/6] Setting profile...")
name_input = chatgpt_page.get_by_placeholder("Full name")
if await name_input.count() > 0:
await name_input.fill(full_name)
await chatgpt_page.wait_for_timeout(500)
await fill_date_field(chatgpt_page, birth_month, birth_day, birth_year)
await chatgpt_page.wait_for_timeout(1000)
continue_btn = chatgpt_page.get_by_role(
"button", name="Continue", exact=True
)
if await continue_btn.count() > 0:
await continue_btn.click()
logger.info("Account registered!")
await wait_for_signup_stabilization(chatgpt_page)
logger.info("[5/6] Skipping onboarding...")
for _ in range(5):
skip_btn = chatgpt_page.locator(
'button:has-text("Skip"):not(:has-text("Skip Tour"))'
)
if await skip_btn.count() > 0:
for i in range(await skip_btn.count()):
try:
btn = skip_btn.nth(i)
if await btn.is_visible():
await btn.click(timeout=5000)
logger.info("Clicked: Skip")
await chatgpt_page.wait_for_timeout(1500)
except:
pass
await chatgpt_page.wait_for_timeout(1000)
skip_tour = chatgpt_page.locator('button:has-text("Skip Tour")')
if await skip_tour.count() > 0:
try:
await skip_tour.first.wait_for(state="visible", timeout=5000)
await skip_tour.first.click(timeout=5000)
logger.info("Clicked: Skip Tour")
await chatgpt_page.wait_for_timeout(2000)
except:
pass
await chatgpt_page.wait_for_timeout(2000)
for _ in range(3):
continue_btn = chatgpt_page.locator('button:has-text("Continue")')
if await continue_btn.count() > 0:
try:
await continue_btn.first.wait_for(state="visible", timeout=5000)
await continue_btn.first.click(timeout=5000)
logger.info("Clicked: Continue")
await chatgpt_page.wait_for_timeout(2000)
except:
pass
await chatgpt_page.wait_for_timeout(2000)
okay_btn = chatgpt_page.locator('button:has-text("Okay, let")')
for _ in range(10):
try:
await okay_btn.first.wait_for(state="visible", timeout=3000)
await okay_btn.first.click(timeout=5000)
logger.info("Clicked: Okay, let's go")
await chatgpt_page.wait_for_timeout(3000)
break
except:
await chatgpt_page.wait_for_timeout(1000)
logger.info("Skipping subscription/card flow (disabled)")
await chatgpt_page.wait_for_timeout(2000)
logger.info("[6/6] Running OAuth flow to get tokens...")
oauth_page = await context.new_page()
current_page = oauth_page
def handle_request(request):
nonlocal redirect_url_captured
url = request.url
if "localhost:1455" in url and "code=" in url:
redirect_url_captured = url
logger.info("Captured OAuth redirect URL")
oauth_page.on("request", handle_request)
await oauth_page.goto(authorize_url, wait_until="domcontentloaded")
await oauth_page.wait_for_timeout(2000)
email_input = oauth_page.locator('input[type="email"], input[name="email"]')
if await email_input.count() > 0:
await email_input.first.fill(email)
await oauth_page.wait_for_timeout(400)
continue_button = oauth_page.get_by_role("button", name="Continue")
if await continue_button.count() > 0:
await continue_button.first.click()
await oauth_page.wait_for_timeout(2500)
password_input = oauth_page.locator('input[type="password"]')
if await password_input.count() > 0:
await password_input.first.fill(password)
await oauth_page.wait_for_timeout(400)
continue_button = oauth_page.get_by_role("button", name="Continue")
if await continue_button.count() > 0:
await continue_button.first.click()
await oauth_page.wait_for_timeout(2500)
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(2000)
except Exception:
pass
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:
pass
if not redirect_url_captured:
raise AutomationError(
"oauth", "OAuth redirect with code was not captured", oauth_page
)
parsed = urlparse(redirect_url_captured)
params = parse_qs(parsed.query)
auth_code = params.get("code", [None])[0]
returned_state = params.get("state", [None])[0]
if not auth_code:
raise AutomationError(
"oauth", "OAuth code missing in redirect", oauth_page
)
if returned_state != oauth_state:
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")
return True
except AutomationError as e:
logger.error(f"Error at step [{e.step}]: {e.message}")
await save_error_screenshot(e.page, e.step)
return False
except Exception as e:
logger.error(f"Unexpected error: {e}")
await save_error_screenshot(current_page, "unexpected")
return False
finally:
if managed:
await asyncio.sleep(2)
await managed.close()