Overwrite previous grok-keygen contents with the local registration/CPA protocol codebase (secrets and runtime data excluded).
778 lines
31 KiB
Python
Executable File
778 lines
31 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
自动注册脚本 v2(Cloudflare catch-all + Gmail IMAP)
|
||
流程:生成随机别名 -> Chromium注册 -> 直接IMAP取验证码 -> 填码完成注册
|
||
"""
|
||
import argparse
|
||
import email as email_lib
|
||
import imaplib
|
||
import json
|
||
import os
|
||
import random
|
||
import re
|
||
import string
|
||
import sys
|
||
import time
|
||
from datetime import datetime, timezone
|
||
from email.header import decode_header as _decode_header
|
||
from email.utils import parsedate_to_datetime
|
||
from pathlib import Path
|
||
|
||
from DrissionPage import Chromium, ChromiumOptions
|
||
|
||
# ---- 配置 ----
|
||
BASE_URL = "https://accounts.x.ai"
|
||
SIGNUP_URL = "https://accounts.x.ai/sign-up?redirect=grok-com"
|
||
ACCOUNTS_FILE = "accounts_cli.txt"
|
||
CPA_DIR = "cpa_auths"
|
||
DOMAIN = "zhangyunuo.net"
|
||
CODE_TIMEOUT = 180
|
||
CODE_POLL = 3
|
||
IMAP_HOST = "imap.gmail.com"
|
||
IMAP_PORT = 993
|
||
EXTENSION_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "turnstilePatch")
|
||
|
||
CHROMIUM_SLIM_FLAGS = [
|
||
"--disable-gpu",
|
||
"--disable-software-rasterizer",
|
||
"--no-sandbox",
|
||
"--disable-dev-shm-usage",
|
||
"--disable-images",
|
||
"--mute-audio",
|
||
"--disable-background-networking",
|
||
"--no-first-run",
|
||
]
|
||
|
||
CODE_PATTERNS = [
|
||
re.compile(r"verification\s+code[:\s]+(\d{4,8})", re.I),
|
||
re.compile(r"your\s+code[:\s]+(\d{4,8})", re.I),
|
||
re.compile(r"confirm(?:ation)?\s+code[:\s]+(\d{4,8})", re.I),
|
||
re.compile(r"code[:\s]+(\d{4,8})", re.I),
|
||
re.compile(r"验证码[::\\s]+(\d{4,8})"),
|
||
]
|
||
|
||
def load_env(path=".env"):
|
||
data = {}
|
||
if not os.path.exists(path):
|
||
return data
|
||
with open(path) as f:
|
||
for line in f:
|
||
line = line.strip()
|
||
if not line or line.startswith("#"):
|
||
continue
|
||
if "=" in line:
|
||
k, v = line.split("=", 1)
|
||
data[k.strip()] = v.strip().strip('"').strip("'")
|
||
return data
|
||
|
||
def gen_alias(domain: str, length: int = 10) -> str:
|
||
alias = "".join(random.choices(string.ascii_lowercase + string.digits, k=length))
|
||
return f"{alias}@{domain}"
|
||
|
||
def gen_password(length: int = 16) -> str:
|
||
# Avoid form-hostile punctuation; keep strong enough for xAI signup rules.
|
||
lower = string.ascii_lowercase
|
||
upper = string.ascii_uppercase
|
||
digits = string.digits
|
||
special = "!@#$%^&*"
|
||
if length < 8:
|
||
length = 8
|
||
base = [
|
||
random.choice(lower),
|
||
random.choice(upper),
|
||
random.choice(digits),
|
||
random.choice(special),
|
||
]
|
||
pool = lower + upper + digits + special
|
||
base += random.choices(pool, k=length - len(base))
|
||
random.shuffle(base)
|
||
return "".join(base)
|
||
|
||
def decode_header(raw: str) -> str:
|
||
parts = _decode_header(raw or "")
|
||
out = []
|
||
for part, charset in parts:
|
||
if isinstance(part, bytes):
|
||
out.append(part.decode(charset or "utf-8", errors="replace"))
|
||
else:
|
||
out.append(part)
|
||
return "".join(out)
|
||
|
||
def message_body(msg) -> str:
|
||
body = ""
|
||
if msg.is_multipart():
|
||
for part in msg.walk():
|
||
ct = part.get_content_type()
|
||
if ct == "text/plain":
|
||
payload = part.get_payload(decode=True)
|
||
if payload:
|
||
body += payload.decode("utf-8", errors="replace")
|
||
else:
|
||
payload = msg.get_payload(decode=True)
|
||
if payload:
|
||
body += payload.decode("utf-8", errors="replace")
|
||
return body
|
||
|
||
def extract_code(text: str) -> str | None:
|
||
for pat in CODE_PATTERNS:
|
||
m = pat.search(text)
|
||
if m:
|
||
return m.group(1)
|
||
return None
|
||
|
||
def imap_login(user: str, password: str):
|
||
imap = imaplib.IMAP4_SSL(IMAP_HOST, IMAP_PORT, timeout=45)
|
||
imap.login(user, password)
|
||
imap.select("INBOX")
|
||
return imap
|
||
|
||
def fetch_code(imap, target_email: str, timeout: int = CODE_TIMEOUT, poll: int = CODE_POLL) -> str | None:
|
||
deadline = time.time() + timeout
|
||
target_lower = target_email.lower().strip()
|
||
while time.time() < deadline:
|
||
try:
|
||
status, data = imap.search(None, "ALL")
|
||
if status != "OK" or not data or not data[0]:
|
||
time.sleep(poll)
|
||
continue
|
||
msg_ids = data[0].split()[-30:]
|
||
filter_ts = int((time.time() - 900) * 1000)
|
||
for mid in reversed(msg_ids):
|
||
try:
|
||
_, msg_data = imap.fetch(mid, "(RFC822)")
|
||
except Exception:
|
||
continue
|
||
if not msg_data or not msg_data[0] or not isinstance(msg_data[0][1], bytes):
|
||
continue
|
||
msg = email_lib.message_from_bytes(msg_data[0][1])
|
||
date_str = msg.get("Date")
|
||
if date_str:
|
||
try:
|
||
dt = parsedate_to_datetime(date_str)
|
||
if dt.tzinfo is None:
|
||
dt = dt.replace(tzinfo=timezone.utc)
|
||
if int(dt.timestamp() * 1000) < filter_ts:
|
||
continue
|
||
except Exception:
|
||
pass
|
||
subject = decode_header(msg.get("Subject", ""))
|
||
sender = decode_header(msg.get("From", ""))
|
||
recipient_blob = " ".join(
|
||
decode_header(msg.get(h, ""))
|
||
for h in ("To", "Cc", "Delivered-To", "X-Original-To", "Original-Recipient", "Envelope-To")
|
||
).lower()
|
||
if target_lower not in recipient_blob:
|
||
continue
|
||
body = message_body(msg)
|
||
combined = f"{subject}\n{sender}\n{recipient_blob}\n{body}".lower()
|
||
if not any(kw in combined for kw in ["x.ai", "xai", "grok", "verification", "code", "confirm", "验证码", "确认"]):
|
||
continue
|
||
code = extract_code(combined)
|
||
if code:
|
||
print(f"[+] Code found for {target_email}: {code}")
|
||
return code
|
||
except Exception as e:
|
||
print(f"[!] IMAP poll error: {e}")
|
||
time.sleep(poll)
|
||
return None
|
||
|
||
def save_account(email: str, password: str, sso: str = ""):
|
||
Path(ACCOUNTS_FILE).parent.mkdir(parents=True, exist_ok=True)
|
||
with open(ACCOUNTS_FILE, "a") as f:
|
||
f.write(f"{email}----{password}----{sso}\n")
|
||
print(f"[+] Saved: {email}")
|
||
|
||
def create_browser_options(headless: bool = False, proxy: str = "") -> ChromiumOptions:
|
||
opts = ChromiumOptions()
|
||
opts.auto_port()
|
||
for flag in CHROMIUM_SLIM_FLAGS:
|
||
opts.set_argument(flag)
|
||
if os.path.exists(EXTENSION_PATH):
|
||
opts.add_extension(EXTENSION_PATH)
|
||
if headless:
|
||
opts.headless()
|
||
if proxy:
|
||
try:
|
||
from urllib.parse import urlparse
|
||
u = urlparse(proxy if "://" in proxy else f"http://{proxy}")
|
||
host = u.hostname or ""
|
||
if host:
|
||
port = u.port or (443 if (u.scheme or "http") == "https" else 80)
|
||
scheme = u.scheme or "http"
|
||
opts.set_argument(f"--proxy-server={scheme}://{host}:{port}")
|
||
except Exception as e:
|
||
print(f"[!] Proxy setup failed: {e}")
|
||
return opts
|
||
|
||
def wait_for_tab(browser: Chromium, timeout: int = 15) -> any:
|
||
"""Wait for browser tab to be ready and return it."""
|
||
deadline = time.time() + timeout
|
||
while time.time() < deadline:
|
||
try:
|
||
tab_ids = browser.tab_ids
|
||
if tab_ids:
|
||
tab = browser.get_tab(tab_ids[0])
|
||
if tab is not None:
|
||
return tab
|
||
except Exception:
|
||
pass
|
||
time.sleep(0.5)
|
||
# Fallback
|
||
return browser.latest_tab
|
||
|
||
def bypass_turnstile(page) -> None:
|
||
"""Try to bypass Cloudflare Turnstile using multiple methods."""
|
||
try:
|
||
# Method 1: Check if Turnstile token already exists
|
||
token = page.run_js("""
|
||
try {
|
||
const byInput = String((document.querySelector('input[name="cf-turnstile-response"]') || {}).value || '').trim();
|
||
if (byInput) return byInput;
|
||
if (window.turnstile && typeof turnstile.getResponse === 'function') {
|
||
return String(turnstile.getResponse() || '').trim();
|
||
}
|
||
return '';
|
||
} catch(e) { return ''; }
|
||
""")
|
||
if token and len(token) >= 80:
|
||
print(f"[+] Turnstile token already present (len={len(token)})")
|
||
return
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
# Method 2: Try to find and click Turnstile checkbox
|
||
challenge_input = page.ele("@name=cf-turnstile-response", timeout=2)
|
||
if challenge_input:
|
||
try:
|
||
wrapper = challenge_input.parent()
|
||
iframe = None
|
||
try:
|
||
iframe = wrapper.shadow_root.ele("tag:iframe")
|
||
except Exception:
|
||
pass
|
||
if iframe:
|
||
try:
|
||
iframe.run_js("""
|
||
window.dtp = 1;
|
||
function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; }
|
||
let sx = getRandomInt(800, 1200);
|
||
let sy = getRandomInt(400, 700);
|
||
Object.defineProperty(MouseEvent.prototype, 'screenX', { value: sx });
|
||
Object.defineProperty(MouseEvent.prototype, 'screenY', { value: sy });
|
||
""")
|
||
except Exception:
|
||
pass
|
||
try:
|
||
body_sr = iframe.ele("tag:body").shadow_root
|
||
btn = body_sr.ele("tag:input")
|
||
if btn:
|
||
btn.click()
|
||
print("[+] Clicked Turnstile checkbox via shadow DOM")
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
except Exception:
|
||
pass
|
||
|
||
try:
|
||
# Method 3: Click any visible Turnstile container
|
||
page.run_js("""
|
||
const nodes = Array.from(document.querySelectorAll('div,span,iframe')).filter((n) => {
|
||
const txt = (n.className || '') + ' ' + (n.id || '') + ' ' + (n.getAttribute?.('src') || '');
|
||
return String(txt).toLowerCase().includes('turnstile');
|
||
});
|
||
if (nodes.length && typeof nodes[0].click === 'function') nodes[0].click();
|
||
""")
|
||
except Exception:
|
||
pass
|
||
|
||
def click_email_signup_button(page, timeout: int = 15) -> bool:
|
||
"""Click the 'Sign up with email' button on the landing page."""
|
||
deadline = time.time() + timeout
|
||
while time.time() < deadline:
|
||
clicked = page.run_js("""
|
||
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"]'));
|
||
const target = candidates.find((node) => {
|
||
const text = (node.innerText || node.textContent || '').replace(/\\s+/g, '');
|
||
const lower = text.toLowerCase();
|
||
return (
|
||
text.includes('使用邮箱注册') ||
|
||
lower.includes('signupwithemail') ||
|
||
lower.includes('continuewithemail') ||
|
||
lower.includes('email')
|
||
);
|
||
});
|
||
if (!target) return false;
|
||
target.click();
|
||
return true;
|
||
""")
|
||
if clicked:
|
||
print("[+] Clicked 'Sign up with email' button")
|
||
return True
|
||
time.sleep(1)
|
||
return False
|
||
|
||
def fill_email_js(page, email: str) -> bool:
|
||
"""Fill email input using React-compatible JS injection."""
|
||
deadline = time.time() + 15
|
||
while time.time() < deadline:
|
||
result = page.run_js("""
|
||
function isVisible(node) {
|
||
if (!node) return false;
|
||
const style = window.getComputedStyle(node);
|
||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
||
const rect = node.getBoundingClientRect();
|
||
return rect.width > 0 && rect.height > 0;
|
||
}
|
||
const input = Array.from(document.querySelectorAll('input[data-testid="email"], input[name="email"], input[type="email"], input[autocomplete="email"]')).find((node) => isVisible(node) && !node.disabled && !node.readOnly) || null;
|
||
if (!input) return 'not-ready';
|
||
input.focus(); input.click();
|
||
const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
||
const tracker = input._valueTracker;
|
||
if (tracker) tracker.setValue(arguments[0]);
|
||
if (valueSetter) valueSetter.call(input, arguments[0]); else input.value = arguments[0];
|
||
input.dispatchEvent(new Event('focus', { bubbles: true }));
|
||
input.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: arguments[0], inputType: 'insertText' }));
|
||
input.dispatchEvent(new InputEvent('input', { bubbles: true, data: arguments[0], inputType: 'insertText' }));
|
||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||
input.dispatchEvent(new Event('blur', { bubbles: true }));
|
||
const current = (input.value || '').trim();
|
||
if (current === arguments[0]) return 'filled';
|
||
input.value = '';
|
||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||
for (const ch of arguments[0]) {
|
||
input.dispatchEvent(new KeyboardEvent('keydown', { key: ch, bubbles: true }));
|
||
input.value += ch;
|
||
input.dispatchEvent(new InputEvent('input', { bubbles: true, data: ch, inputType: 'insertText' }));
|
||
input.dispatchEvent(new KeyboardEvent('keyup', { key: ch, bubbles: true }));
|
||
}
|
||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||
if ((input.value || '').trim() === arguments[0]) return 'filled';
|
||
return input.value;
|
||
""", email)
|
||
if result == "not-ready":
|
||
time.sleep(0.5)
|
||
continue
|
||
if result == "filled":
|
||
return True
|
||
time.sleep(0.5)
|
||
return False
|
||
|
||
def click_continue_button(page) -> bool:
|
||
"""Click continue/signup button after email is filled."""
|
||
clicked = page.run_js(r"""
|
||
function isVisible(node) {
|
||
if (!node) return false;
|
||
const style = window.getComputedStyle(node);
|
||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
||
const rect = node.getBoundingClientRect();
|
||
return rect.width > 0 && rect.height > 0;
|
||
}
|
||
const input = Array.from(document.querySelectorAll('input[data-testid="email"], input[name="email"], input[type="email"], input[autocomplete="email"]')).find((node) => isVisible(node) && !node.disabled && !node.readOnly) || null;
|
||
if (!input || !(input.value || '').trim()) return false;
|
||
const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')).filter((node) => isVisible(node) && !node.disabled && node.getAttribute('aria-disabled') !== 'true');
|
||
const submitButton = buttons.find((node) => {
|
||
const raw = (node.innerText || node.textContent || '').trim();
|
||
const compact = raw.replace(/\s+/g, '').toLowerCase();
|
||
return (
|
||
raw.includes('注册') ||
|
||
compact === 'signup' ||
|
||
compact.includes('signup') ||
|
||
compact.includes('continue') ||
|
||
compact.includes('next') ||
|
||
compact.includes('createaccount') ||
|
||
compact.includes('submit')
|
||
);
|
||
});
|
||
if (!submitButton || submitButton.disabled) return false;
|
||
submitButton.click();
|
||
return true;
|
||
""")
|
||
return bool(clicked)
|
||
|
||
def gen_name() -> tuple[str, str]:
|
||
first = ["Alex", "Sam", "Jordan", "Taylor", "Casey", "Riley", "Morgan", "Avery", "Quinn", "Jamie"]
|
||
last = ["Smith", "Johnson", "Brown", "Taylor", "Anderson", "Thomas", "Jackson", "White", "Harris", "Martin"]
|
||
return random.choice(first), random.choice(last)
|
||
|
||
|
||
def fill_profile_js(page, password: str, timeout: int = 120) -> dict:
|
||
"""Fill given/family name + password and submit after Turnstile is ready."""
|
||
given_name, family_name = gen_name()
|
||
deadline = time.time() + timeout
|
||
form_filled = False
|
||
while time.time() < deadline:
|
||
state = page.run_js(
|
||
r"""
|
||
const givenName = arguments[0];
|
||
const familyName = arguments[1];
|
||
const password = arguments[2];
|
||
function isVisible(node) {
|
||
if (!node) return false;
|
||
const style = window.getComputedStyle(node);
|
||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
||
const rect = node.getBoundingClientRect();
|
||
return rect.width > 0 && rect.height > 0;
|
||
}
|
||
function pick(sel) {
|
||
return Array.from(document.querySelectorAll(sel)).find((n) => isVisible(n) && !n.disabled && !n.readOnly) || null;
|
||
}
|
||
function setVal(input, value) {
|
||
if (!input) return false;
|
||
input.focus(); input.click();
|
||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
|
||
const tracker = input._valueTracker;
|
||
if (tracker) tracker.setValue('');
|
||
if (setter) setter.call(input, value); else input.value = value;
|
||
input.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, data: value, inputType: 'insertText' }));
|
||
input.dispatchEvent(new InputEvent('input', { bubbles: true, data: value, inputType: 'insertText' }));
|
||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||
input.blur();
|
||
return String(input.value || '').trim() === String(value || '').trim();
|
||
}
|
||
const givenInput = pick('input[data-testid="givenName"], input[name="givenName"], input[autocomplete="given-name"]');
|
||
const familyInput = pick('input[data-testid="familyName"], input[name="familyName"], input[autocomplete="family-name"]');
|
||
const passwordInput = pick('input[data-testid="password"], input[name="password"], input[type="password"], input[autocomplete="new-password"]');
|
||
if (!givenInput || !familyInput || !passwordInput) return 'not-ready';
|
||
const ok1 = setVal(givenInput, givenName);
|
||
const ok2 = setVal(familyInput, familyName);
|
||
const ok3 = setVal(passwordInput, password);
|
||
if (!ok1 || !ok2 || !ok3) return 'fill-failed';
|
||
const cfInput = document.querySelector('input[name="cf-turnstile-response"]');
|
||
const cfPresent = !!cfInput || !!document.querySelector('iframe[src*="turnstile"], div.cf-turnstile, [data-sitekey]');
|
||
if (cfPresent) {
|
||
const token = String((cfInput && cfInput.value) || '').trim();
|
||
if (token.length < 80) return 'wait-cf:' + token.length;
|
||
}
|
||
const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')).filter((n) => isVisible(n) && !n.disabled && n.getAttribute('aria-disabled') !== 'true');
|
||
const submitBtn = buttons.find((n) => {
|
||
const t = (n.innerText || n.textContent || '').replace(/\s+/g, '').toLowerCase();
|
||
return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('signup') || t.includes('createaccount') || t.includes('continue') || t.includes('confirm');
|
||
});
|
||
if (!submitBtn) return 'filled-no-submit';
|
||
submitBtn.click();
|
||
return 'submitted';
|
||
""",
|
||
given_name,
|
||
family_name,
|
||
password,
|
||
)
|
||
if state == "not-ready":
|
||
time.sleep(0.8)
|
||
continue
|
||
if state == "fill-failed":
|
||
time.sleep(0.5)
|
||
continue
|
||
if isinstance(state, str) and state.startswith("wait-cf"):
|
||
form_filled = True
|
||
bypass_turnstile(page)
|
||
time.sleep(1.5)
|
||
continue
|
||
if state == "filled-no-submit":
|
||
form_filled = True
|
||
time.sleep(1)
|
||
continue
|
||
if state == "submitted":
|
||
return {"ok": True, "given_name": given_name, "family_name": family_name, "password": password}
|
||
time.sleep(0.8)
|
||
return {"ok": False, "error": "timeout waiting profile/turnstile", "given_name": given_name, "family_name": family_name}
|
||
|
||
|
||
def wait_for_sso(page, timeout: int = 180) -> str:
|
||
deadline = time.time() + timeout
|
||
last_click = 0.0
|
||
while time.time() < deadline:
|
||
# periodically re-click final submit if still on complete-registration page
|
||
now = time.time()
|
||
if now - last_click >= 3:
|
||
try:
|
||
page.run_js(r"""
|
||
function isVisible(node) {
|
||
if (!node) return false;
|
||
const style = window.getComputedStyle(node);
|
||
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
||
const rect = node.getBoundingClientRect();
|
||
return rect.width > 0 && rect.height > 0;
|
||
}
|
||
const titleHit = !!Array.from(document.querySelectorAll('h1,h2,div,span')).find((el) => {
|
||
const t = (el.textContent || '').replace(/\s+/g, '');
|
||
return t.includes('完成注册') || t.toLowerCase().includes('complete');
|
||
});
|
||
if (!titleHit) return false;
|
||
const cfInput = document.querySelector('input[name="cf-turnstile-response"]');
|
||
if (cfInput && String(cfInput.value || '').trim().length < 80) return false;
|
||
const buttons = Array.from(document.querySelectorAll('button[type="submit"], button')).filter((n) => isVisible(n) && !n.disabled);
|
||
const submitBtn = buttons.find((n) => {
|
||
const t = (n.innerText || n.textContent || '').replace(/\s+/g, '').toLowerCase();
|
||
return t.includes('完成注册') || t.includes('创建账户') || t.includes('signup') || t.includes('signup') || t.includes('createaccount');
|
||
});
|
||
if (submitBtn) { submitBtn.click(); return true; }
|
||
return false;
|
||
""")
|
||
except Exception:
|
||
pass
|
||
last_click = now
|
||
try:
|
||
cookies = None
|
||
for getter in (
|
||
lambda: page.cookies(all_domains=True, all_info=True),
|
||
lambda: page.cookies(all_domains=True),
|
||
lambda: page.cookies(),
|
||
):
|
||
try:
|
||
cookies = getter()
|
||
if cookies:
|
||
break
|
||
except TypeError:
|
||
continue
|
||
except Exception:
|
||
continue
|
||
if isinstance(cookies, list):
|
||
for c in cookies:
|
||
if not isinstance(c, dict):
|
||
continue
|
||
if str(c.get("name", "")).strip() == "sso" and c.get("value"):
|
||
return str(c.get("value")).strip()
|
||
except Exception:
|
||
pass
|
||
time.sleep(1)
|
||
return ""
|
||
|
||
|
||
def maybe_export_cpa(email: str, password: str, sso: str, page) -> str | None:
|
||
"""Best-effort CPA export using project cpa_export + config.json."""
|
||
cfg_path = Path(__file__).resolve().parent / "config.json"
|
||
if not cfg_path.is_file():
|
||
return None
|
||
try:
|
||
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
|
||
except Exception:
|
||
return None
|
||
if not cfg.get("cpa_export_enabled", True):
|
||
return None
|
||
try:
|
||
import cpa_export
|
||
cookies = cpa_export.export_cookies_from_page(page)
|
||
result = cpa_export.export_cpa_xai_for_account(
|
||
email,
|
||
password,
|
||
page=None,
|
||
cookies=cookies,
|
||
sso=sso,
|
||
config=cfg,
|
||
log_callback=lambda m: print(f"[cpa] {m}"),
|
||
)
|
||
if result.get("ok") and result.get("path"):
|
||
return str(result["path"])
|
||
except Exception as exc:
|
||
print(f"[!] CPA export exception: {exc}")
|
||
return None
|
||
|
||
|
||
def do_register(alias: str, password: str, imap_user: str, imap_password: str, headless: bool = False, proxy: str = "") -> dict:
|
||
email = alias
|
||
print(f"[*] Registering: {email}")
|
||
|
||
opts = create_browser_options(headless=headless, proxy=proxy)
|
||
browser = None
|
||
imap_conn = None
|
||
try:
|
||
browser = Chromium(opts)
|
||
page = wait_for_tab(browser, timeout=20)
|
||
if page is None:
|
||
raise Exception("无法获取浏览器标签页")
|
||
|
||
page.get(SIGNUP_URL)
|
||
page.wait.doc_loaded(timeout=20)
|
||
time.sleep(2)
|
||
print(f"[+] Page loaded: {page.url}")
|
||
|
||
# 尝试绕过 Turnstile
|
||
bypass_turnstile(page)
|
||
time.sleep(1)
|
||
|
||
# 点击"使用邮箱注册"按钮
|
||
if not click_email_signup_button(page, timeout=10):
|
||
print("[!] No email signup button found, continuing anyway...")
|
||
time.sleep(2)
|
||
|
||
# 填写邮箱
|
||
if not fill_email_js(page, email):
|
||
raise Exception("未找到邮箱输入框或写入失败")
|
||
print(f"[+] Email submitted: {email}")
|
||
time.sleep(0.8)
|
||
|
||
# 点击继续
|
||
if not click_continue_button(page):
|
||
raise Exception("未找到注册按钮")
|
||
print(f"[+] Continue clicked")
|
||
time.sleep(3)
|
||
|
||
# 连接 IMAP 准备取验证码
|
||
print(f"[*] Connecting IMAP {imap_user}...")
|
||
imap_conn = imap_login(imap_user, imap_password)
|
||
|
||
# 等待验证码
|
||
print(f"[*] Waiting for code...")
|
||
code = fetch_code(imap_conn, email, timeout=CODE_TIMEOUT, poll=CODE_POLL)
|
||
if not code:
|
||
raise Exception(f"未收到验证码: {email}")
|
||
|
||
# 填写验证码
|
||
code_input = page.ele(
|
||
"css:input[data-input-otp='true'], input[name='code'], input[autocomplete='one-time-code']",
|
||
timeout=5,
|
||
)
|
||
if code_input:
|
||
code_input.clear()
|
||
code_input.input(code)
|
||
else:
|
||
boxes = page.eles("css:input[inputmode='numeric']")
|
||
for i, ch in enumerate(code):
|
||
if i < len(boxes):
|
||
boxes[i].input(ch)
|
||
time.sleep(0.1)
|
||
print(f"[+] Code filled: {code}")
|
||
time.sleep(0.5)
|
||
|
||
# 点击确认
|
||
page.run_js("""
|
||
const btns = Array.from(document.querySelectorAll('button'));
|
||
const btn = btns.find(b => {
|
||
const t = (b.innerText || b.textContent || '').replace(/\\s+/g, '').toLowerCase();
|
||
return t.includes('continue') || t.includes('confirm') || t.includes('下一步');
|
||
});
|
||
if (btn) btn.click();
|
||
""")
|
||
time.sleep(3)
|
||
|
||
# 验证码后通常进入资料页:姓名 + 密码 + Turnstile
|
||
print("[*] Filling profile (name/password)...")
|
||
profile = fill_profile_js(page, password, timeout=120)
|
||
if not profile.get("ok"):
|
||
raise Exception(f"资料填写失败: {profile.get('error') or 'unknown'}")
|
||
print(f"[+] Profile filled: {profile.get('given_name')} {profile.get('family_name')}")
|
||
|
||
# 等待 sso cookie(最终提交后)
|
||
print("[*] Waiting for sso cookie...")
|
||
sso_cookie = wait_for_sso(page, timeout=180)
|
||
current_url = page.url
|
||
print(f"[+] Current URL: {current_url}")
|
||
if not sso_cookie:
|
||
raise Exception("未获取到 sso cookie")
|
||
|
||
save_account(email, password, sso_cookie)
|
||
result = {"ok": True, "email": email, "password": password, "sso": sso_cookie, "url": current_url}
|
||
|
||
# 可选:协议优先 CPA mint
|
||
if os.environ.get("CPA_EXPORT", "1") not in ("0", "false", "False"):
|
||
try:
|
||
cpa_path = maybe_export_cpa(email, password, sso_cookie, page)
|
||
if cpa_path:
|
||
result["cpa_path"] = cpa_path
|
||
print(f"[+] CPA auth: {cpa_path}")
|
||
except Exception as cpa_exc:
|
||
print(f"[!] CPA export failed: {cpa_exc}")
|
||
result["cpa_error"] = str(cpa_exc)
|
||
return result
|
||
|
||
except Exception as e:
|
||
print(f"[-] Failed: {e}")
|
||
try:
|
||
if browser is not None:
|
||
tab = wait_for_tab(browser, timeout=5)
|
||
if tab is not None:
|
||
tab.get_screenshot(path="register_error.png", full_page=False)
|
||
print(f"[!] Screenshot: register_error.png")
|
||
except Exception:
|
||
pass
|
||
return {"ok": False, "error": str(e), "email": email}
|
||
finally:
|
||
if imap_conn:
|
||
try:
|
||
imap_conn.close()
|
||
except Exception:
|
||
pass
|
||
try:
|
||
imap_conn.logout()
|
||
except Exception:
|
||
pass
|
||
if browser is not None:
|
||
try:
|
||
browser.quit()
|
||
except Exception:
|
||
pass
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="Grok 自动注册(Cloudflare catch-all + Gmail IMAP)")
|
||
parser.add_argument("--count", type=int, default=1)
|
||
parser.add_argument("--threads", type=int, default=1)
|
||
parser.add_argument("--headless", action="store_true")
|
||
parser.add_argument("--proxy", default=None)
|
||
parser.add_argument("--domain", default=DOMAIN)
|
||
parser.add_argument("--imap-user", default=None)
|
||
parser.add_argument("--imap-password", default=None)
|
||
args = parser.parse_args()
|
||
|
||
env = load_env()
|
||
proxy = args.proxy or env.get("proxy") or env.get("cpa_proxy") or ""
|
||
if proxy == "http://127.0.0.1:7890":
|
||
proxy = ""
|
||
imap_user = args.imap_user or env.get("GMAIL_USER") or ""
|
||
imap_password = args.imap_password or env.get("GMAIL_PASSWORD") or ""
|
||
if not imap_user or not imap_password:
|
||
print("Error: IMAP credentials required via --imap-user/--imap-password or GMAIL_USER/GMAIL_PASSWORD")
|
||
return 1
|
||
|
||
print(f"[*] Domain: {args.domain}")
|
||
print(f"[*] Proxy: {proxy or 'direct'}")
|
||
print(f"[*] IMAP: {imap_user}")
|
||
print(f"[*] Count: {args.count}, Threads: {args.threads}")
|
||
|
||
tasks = []
|
||
for i in range(args.count):
|
||
tasks.append((i + 1, gen_alias(args.domain), gen_password()))
|
||
|
||
results = []
|
||
threads = max(1, min(int(args.threads or 1), 10))
|
||
if threads == 1:
|
||
for idx, alias, password in tasks:
|
||
print(f"\n=== Account {idx}/{args.count} ===")
|
||
res = do_register(alias, password, imap_user, imap_password, headless=args.headless, proxy=proxy or None)
|
||
results.append(res)
|
||
if idx < args.count:
|
||
time.sleep(3)
|
||
else:
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
print(f"[*] Running with {threads} threads")
|
||
with ThreadPoolExecutor(max_workers=threads) as ex:
|
||
futs = {
|
||
ex.submit(
|
||
do_register, alias, password, imap_user, imap_password, args.headless, proxy or None
|
||
): (idx, alias)
|
||
for idx, alias, password in tasks
|
||
}
|
||
for fut in as_completed(futs):
|
||
idx, alias = futs[fut]
|
||
try:
|
||
res = fut.result()
|
||
except Exception as exc:
|
||
res = {"ok": False, "email": alias, "error": str(exc)}
|
||
results.append(res)
|
||
status = "OK" if res.get("ok") else "FAIL"
|
||
print(f"[{status}] #{idx} {res.get('email')} {res.get('error', '')[:80]}")
|
||
|
||
print(f"\n=== Summary ===")
|
||
ok = sum(1 for r in results if r.get("ok"))
|
||
print(f"Success: {ok}/{len(results)}")
|
||
for r in results:
|
||
status = "OK" if r.get("ok") else "FAIL"
|
||
extra = r.get("cpa_path") or r.get("error", "")
|
||
print(f" [{status}] {r.get('email')} - {str(extra)[:100]}")
|
||
return 0 if ok else 1
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main() or 0)
|