Update 2925 OpenAI code scraping flow

This commit is contained in:
2026-05-11 21:25:49 +08:00
parent 7df33e2611
commit fe492e0f2e
29 changed files with 2757 additions and 17 deletions
@@ -0,0 +1,236 @@
#!/usr/bin/env python3
"""Luban SMS API client for Codex/OpenAI phone verification.
Reads secrets from ~/.hermes/env/codex_oauth_onboarding.env.
Never prints API keys, full phone numbers, or SMS codes unless explicitly requested by caller.
Docs: https://lubansms.com/api_docs/
"""
from __future__ import annotations
import argparse
import json
import os
import pathlib
import re
import shlex
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
from typing import Any, Dict, Iterable, List, Optional, Tuple
ENV_PATH = pathlib.Path.home() / ".hermes/env/codex_oauth_onboarding.env"
DEFAULT_BASE = "https://lubansms.com/v2/api"
def load_env(path: pathlib.Path = ENV_PATH) -> Dict[str, str]:
env: Dict[str, str] = {}
if not path.exists():
return env
for line in path.read_text().splitlines():
s = line.strip()
if not s or s.startswith("#") or "=" not in s:
continue
if s.startswith("export "):
s = s[7:].strip()
k, v = s.split("=", 1)
k = k.strip()
v = v.strip()
if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", k):
continue
try:
if v and v[0] in "'\"":
v = shlex.split(v)[0]
except Exception:
pass
env[k] = v.strip("'\"")
return env
def redact_phone(phone: Any) -> Optional[str]:
if phone is None:
return None
s = re.sub(r"\D", "", str(phone))
if len(s) <= 4:
return "[REDACTED]"
return "[REDACTED]" + s[-4:]
def strict_code(text: str) -> Optional[str]:
m = re.search(r"(?<!\d)(\d{6})(?!\d)", text or "")
return m.group(1) if m else None
class LubanSms:
def __init__(self, apikey: str, base: str = DEFAULT_BASE, timeout: int = 30):
self.apikey = apikey
self.base = base.rstrip("/")
self.timeout = timeout
@classmethod
def from_env(cls) -> "LubanSms":
env = load_env()
key = env.get("LUBAN_SMS_API_KEY") or env.get("LUBAN_API_KEY")
if not key:
raise SystemExit("missing LUBAN_SMS_API_KEY in ~/.hermes/env/codex_oauth_onboarding.env")
return cls(key, env.get("LUBAN_SMS_API_BASE", DEFAULT_BASE))
def get(self, endpoint: str, **params: Any) -> Tuple[int, Dict[str, Any], str]:
params = {k: v for k, v in params.items() if v is not None}
params["apikey"] = self.apikey
url = f"{self.base}/{endpoint.lstrip('/')}?{urllib.parse.urlencode(params)}"
req = urllib.request.Request(url, headers={
"Accept": "application/json",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
})
try:
with urllib.request.urlopen(req, timeout=self.timeout) as r:
raw = r.read().decode(errors="ignore")
status = r.status
except urllib.error.HTTPError as e:
raw = e.read().decode(errors="ignore")
status = e.code
except Exception as e:
return 0, {"code": 0, "msg": f"{type(e).__name__}: {e}"}, ""
try:
data = json.loads(raw)
except Exception:
data = {"raw": raw[:500]}
return status, data, raw
def balance(self) -> Tuple[int, Dict[str, Any], str]:
return self.get("getBalance")
def services(self, country: Optional[str] = None, service: Optional[str] = None, language: str = "en", page: int = 1):
return self.get("List", country=country, service=service, language=language, page=page)
def countries(self):
return self.get("countries")
def buy(self, service_id: str) -> Tuple[int, Dict[str, Any], str]:
return self.get("getNumber", service_id=service_id)
def sms(self, request_id: str) -> Tuple[int, Dict[str, Any], str]:
return self.get("getSms", request_id=request_id)
def reject(self, request_id: str) -> Tuple[int, Dict[str, Any], str]:
return self.get("setStatus", request_id=request_id, status="reject")
def again(self, request_id: str) -> Tuple[int, Dict[str, Any], str]:
return self.get("getAgainNmuber", request_id=request_id)
def history(self, language: str = "en", page: int = 1) -> Tuple[int, Dict[str, Any], str]:
return self.get("smsHistory", language=language, page=page)
def summarize_services(data: Dict[str, Any], limit: int = 20) -> List[Dict[str, Any]]:
msg = data.get("msg")
rows = msg if isinstance(msg, list) else []
out: List[Dict[str, Any]] = []
for r in rows[:limit]:
if not isinstance(r, dict):
continue
out.append({
"service_id": str(r.get("service_id", "")),
"country_en": r.get("country_name_en") or r.get("country_name") or r.get("country"),
"country_zh": r.get("country_name_zh"),
"service_name": r.get("service_name"),
"provider": r.get("provider"),
"cost": r.get("cost"),
})
return out
def resolve_service_id(api: LubanSms, country: Optional[str], service: str, language: str = "en", pages: int = 3) -> Optional[Dict[str, Any]]:
# Exact service_name match preferred, then substring match. Keep first cheapest by numeric cost when possible.
candidates: List[Dict[str, Any]] = []
for page in range(1, pages + 1):
st, data, _ = api.services(country=country, service=service, language=language, page=page)
if st != 200 or data.get("code") != 0:
continue
for row in summarize_services(data, limit=500):
name = str(row.get("service_name") or "")
if service.lower() in name.lower() or name.lower() in service.lower():
candidates.append(row)
if not candidates:
return None
def cost_key(r: Dict[str, Any]) -> float:
try: return float(r.get("cost"))
except Exception: return 999999.0
exact = [r for r in candidates if str(r.get("service_name", "")).lower() == service.lower()]
return sorted(exact or candidates, key=cost_key)[0]
def cmd_balance(args: argparse.Namespace) -> int:
api = LubanSms.from_env()
st, data, _ = api.balance()
print(json.dumps({"http": st, "code": data.get("code"), "ok": st == 200 and data.get("code") == 0, "balance": data.get("balance"), "msg": data.get("msg")}, ensure_ascii=False))
return 0 if st == 200 and data.get("code") == 0 else 1
def cmd_services(args: argparse.Namespace) -> int:
api = LubanSms.from_env()
st, data, _ = api.services(country=args.country, service=args.service, language=args.language, page=args.page)
print(json.dumps({"http": st, "code": data.get("code"), "ok": st == 200 and data.get("code") == 0, "services": summarize_services(data, args.limit), "msg_type": type(data.get("msg")).__name__}, ensure_ascii=False))
return 0 if st == 200 and data.get("code") == 0 else 1
def cmd_resolve(args: argparse.Namespace) -> int:
api = LubanSms.from_env()
row = resolve_service_id(api, args.country, args.service, args.language, args.pages)
print(json.dumps({"ok": bool(row), "selected": row}, ensure_ascii=False))
return 0 if row else 2
def cmd_buy(args: argparse.Namespace) -> int:
api = LubanSms.from_env()
service_id = args.service_id
selected = None
if not service_id:
selected = resolve_service_id(api, args.country, args.service, args.language, args.pages)
if not selected:
print(json.dumps({"phase": "resolve_service_fail", "country": args.country, "service": args.service}, ensure_ascii=False))
return 2
service_id = selected["service_id"]
st, data, _ = api.buy(service_id)
ok = st == 200 and data.get("code") == 0 and data.get("request_id") and data.get("number")
print(json.dumps({"http": st, "ok": bool(ok), "code": data.get("code"), "msg": data.get("msg"), "request_id": data.get("request_id"), "phone_redacted": redact_phone(data.get("number")), "selected": selected}, ensure_ascii=False))
return 0 if ok else 1
def cmd_sms(args: argparse.Namespace) -> int:
api = LubanSms.from_env()
st, data, _ = api.sms(args.request_id)
code = data.get("sms_code") or strict_code(json.dumps(data, ensure_ascii=False))
print(json.dumps({"http": st, "ok": st == 200 and data.get("code") == 0, "msg": data.get("msg"), "has_code": bool(code), "code_len": len(str(code)) if code else 0}, ensure_ascii=False))
return 0
def cmd_reject(args: argparse.Namespace) -> int:
api = LubanSms.from_env()
st, data, _ = api.reject(args.request_id)
print(json.dumps({"http": st, "ok": st == 200 and data.get("code") == 0, "code": data.get("code"), "msg": data.get("msg")}, ensure_ascii=False))
return 0 if st == 200 and data.get("code") == 0 else 1
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(description="Luban SMS API helper")
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("balance")
s = sub.add_parser("services"); s.add_argument("--country"); s.add_argument("--service", default="OpenAI"); s.add_argument("--language", default="en"); s.add_argument("--page", type=int, default=1); s.add_argument("--limit", type=int, default=30)
r = sub.add_parser("resolve"); r.add_argument("--country"); r.add_argument("--service", default="OpenAI"); r.add_argument("--language", default="en"); r.add_argument("--pages", type=int, default=3)
b = sub.add_parser("buy"); b.add_argument("--service-id"); b.add_argument("--country"); b.add_argument("--service", default="OpenAI"); b.add_argument("--language", default="en"); b.add_argument("--pages", type=int, default=3)
g = sub.add_parser("sms"); g.add_argument("request_id")
x = sub.add_parser("reject"); x.add_argument("request_id")
return p
def main(argv: Optional[List[str]] = None) -> int:
args = build_parser().parse_args(argv)
return globals()[f"cmd_{args.cmd}"](args)
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""Scrape OpenAI/ChatGPT verification codes from 2925 Webmail via Chrome CDP.
Key 2925 quirks captured from live runs:
- Do NOT navigate to #/mailList?type=收件箱 and trust the result; click the left nav item `收件箱`.
- Real mail rows are `table.maillist-table tr`.
- The generated alias local-part may be hidden in sender tooltip/bounce text such as
`...-chickliu2031som6d8=2925.com@tm1.openai.com`, not visible row text.
- Open the newest matching row, then extract the strict 6-digit code near code words.
Inputs:
--cdp http://127.0.0.1:9227
--account chickliu2031@2925.com
--alias chickliu2031som6d8@2925.com
--out /tmp/openai_2925_email_code.txt
The script prints JSON with code redacted and writes the raw code only to --out.
"""
import argparse, json, pathlib, re, sys, time, urllib.request, websocket, itertools
def tabs(cdp):
return json.loads(urllib.request.urlopen(cdp.rstrip('/') + '/json', timeout=10).read().decode())
def connect(cdp):
pages = [t for t in tabs(cdp) if t.get('type') == 'page']
sel = next((t for t in pages if '2925.com' in (t.get('url') or '')), None) or pages[0]
ws = websocket.create_connection(sel['webSocketDebuggerUrl'], timeout=20)
counter = itertools.count(1)
def call(method, params=None, timeout=45):
i = next(counter)
ws.send(json.dumps({'id': i, 'method': method, 'params': params or {}}))
end = time.time() + timeout
while time.time() < end:
msg = json.loads(ws.recv())
if msg.get('id') == i:
if 'error' in msg:
raise RuntimeError(msg['error'])
return msg.get('result', {})
raise TimeoutError(method)
call('Page.enable')
call('Runtime.enable')
return call
def ev(call, js, awaitp=False, timeout=45):
return call('Runtime.evaluate', {
'expression': js,
'awaitPromise': awaitp,
'returnByValue': True,
}, timeout).get('result', {}).get('value')
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--cdp', default='http://127.0.0.1:9227')
ap.add_argument('--account', required=True)
ap.add_argument('--alias', required=True)
ap.add_argument('--out', required=True)
args = ap.parse_args()
local = args.alias.split('@', 1)[0].lower()
call = connect(args.cdp)
js = r'''(async(local,account)=>{
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const click=e=>{if(!e)return false; e.scrollIntoView({block:'center'}); e.dispatchEvent(new MouseEvent('mousedown',{bubbles:true})); e.dispatchEvent(new MouseEvent('mouseup',{bubbles:true})); e.click(); return true;};
const all=[...document.querySelectorAll('*')];
const inbox=all.find(e=>(e.innerText||e.textContent||'').trim()==='收件箱');
const clickedInbox=click(inbox); await sleep(1800);
const refresh=[...document.querySelectorAll('*')].filter(e=>(e.innerText||e.textContent||'').trim()==='刷新').pop();
const clickedRefresh=click(refresh); await sleep(3000);
const pageText=document.body?.innerText||'';
const accountOk=pageText.includes(account);
const rows=[...document.querySelectorAll('table.maillist-table tr')].filter(r=>/OpenAI|ChatGPT|验证码|临时验证码|verification|code/i.test(r.innerText||''));
const rowSummaries=[];
let chosen=null, chosenIndex=-1;
for(let i=0;i<rows.length;i++){
const r=rows[i];
const hidden=[...r.querySelectorAll('*')].map(e=>[e.innerText,e.textContent,e.title,e.getAttribute('aria-label')].filter(Boolean).join(' ')).join(' ');
const combined=(r.innerText+' '+hidden).toLowerCase();
rowSummaries.push({i,text:(r.innerText||'').replace(/\s+/g,' ').trim().slice(0,180),hiddenHasLocal:combined.includes(local)});
if(!chosen && combined.includes(local)){ chosen=r; chosenIndex=i; }
}
if(!chosen && rows[0]){ chosen=rows[0]; chosenIndex=0; }
if(chosen){ click(chosen); await sleep(3500); }
let body=document.body?.innerText||'';
for(const f of [...document.querySelectorAll('iframe')]){ try{ body+='\n'+(f.contentDocument?.body?.innerText||''); }catch(e){} }
const candidates=[];
for(const m of body.matchAll(/(?<!\d)(\d{6})(?!\d)/g)){
const ctx=body.slice(Math.max(0,m.index-120), Math.min(body.length,m.index+126));
const score=(/验证码|verification|code|临时验证码/i.test(ctx)?10:0)+(/OpenAI|ChatGPT/i.test(ctx)?3:0)+(body.toLowerCase().includes(local)?2:0);
candidates.push({code:m[1],score,ctx:ctx.replace(m[1],'[CODE]')});
}
candidates.sort((a,b)=>b.score-a.score);
return {href:location.href,clickedInbox,clickedRefresh,accountOk,rowCount:rows.length,chosenIndex,rowSummaries:rowSummaries.slice(0,8),hasLocal:body.toLowerCase().includes(local),candidates};
})(LOCAL,ACCOUNT)'''.replace('LOCAL', json.dumps(local)).replace('ACCOUNT', json.dumps(args.account))
val = ev(call, js, True, 90) or {}
candidates = val.get('candidates') or []
code = candidates[0]['code'] if candidates else ''
if code:
pathlib.Path(args.out).write_text(code)
redacted = dict(val)
redacted['candidates'] = [{k: v for k, v in c.items() if k != 'code'} for c in candidates[:5]]
redacted['codeSaved'] = bool(code)
print(json.dumps(redacted, ensure_ascii=False))
return 0 if code else 2
if __name__ == '__main__':
sys.exit(main())
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Poll 2925 Webmail for an OpenAI/ChatGPT code matching the current generated alias.
Why this exists:
- 2925 OpenAI mail may land in 垃圾箱 instead of 收件箱.
- 垃圾箱/已删除 can contain stale OpenAI codes for older aliases.
- Never submit a visible 6-digit code unless the surrounding message matches the current alias/local-part.
Outputs compact JSONL and saves the code to --out-file without printing it.
"""
import argparse
import json
import pathlib
import re
import time
import urllib.request
import websocket
import itertools
FOLDERS = {
"inbox": "https://2925.com/#/mailList?type=%E6%94%B6%E4%BB%B6%E7%AE%B1",
"junk": "https://2925.com/#/mailList?type=%E5%9E%83%E5%9C%BE%E7%AE%B1",
"deleted": "https://2925.com/#/mailList?type=%E5%B7%B2%E5%88%A0%E9%99%A4",
}
def log(obj):
print(json.dumps({**obj, "ts": int(time.time())}, ensure_ascii=False), flush=True)
def cdp_json(base, path):
return json.loads(urllib.request.urlopen(base + path, timeout=10).read().decode())
def connect_tab(base, url_substr="2925.com"):
tabs = cdp_json(base, "/json")
tab = next((t for t in tabs if url_substr in (t.get("url") or "")), tabs[0])
ws = websocket.create_connection(tab["webSocketDebuggerUrl"], timeout=20)
counter = itertools.count(1)
def call(method, params=None, timeout=25):
msg_id = next(counter)
ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
end = time.time() + timeout
while time.time() < end:
msg = json.loads(ws.recv())
if msg.get("id") == msg_id:
if "error" in msg:
raise RuntimeError(msg["error"])
return msg.get("result", {})
raise TimeoutError(method)
call("Runtime.enable")
call("Page.enable")
def eval_js(expr, await_promise=False, timeout=25):
result = call(
"Runtime.evaluate",
{"expression": expr, "awaitPromise": await_promise, "returnByValue": True},
timeout,
)
return result.get("result", {}).get("value")
return call, eval_js
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--cdp-port", type=int, required=True)
ap.add_argument("--account", required=True, help="Selected 2925 base account, e.g. user@2925.com")
ap.add_argument("--alias", required=True, help="Current generated alias used on OpenAI")
ap.add_argument("--out-file", default="/tmp/openai_2925_email_code.txt")
ap.add_argument("--attempts", type=int, default=15)
ap.add_argument("--interval", type=float, default=15.0)
ap.add_argument("--folders", default="inbox,junk", help="comma list: inbox,junk,deleted")
ap.add_argument("--reject-code", action="append", default=[])
args = ap.parse_args()
base = f"http://127.0.0.1:{args.cdp_port}"
account = args.account.strip().lower()
alias = args.alias.strip().lower()
alias_local = alias.split("@", 1)[0]
rejected = {str(x).strip() for x in args.reject_code if str(x).strip()}
folders = [x.strip() for x in args.folders.split(",") if x.strip()]
call, ev = connect_tab(base)
for attempt in range(1, args.attempts + 1):
for folder in folders:
url = FOLDERS.get(folder)
if not url:
continue
call("Page.navigate", {"url": url})
time.sleep(4)
js = f"""
(() => {{
try {{
const account = {json.dumps(account)};
const aliasLocal = {json.dumps(alias_local)};
const txt = document.body?.innerText || '';
const emails = [...new Set([...txt.matchAll(/[A-Za-z0-9._%+-]+@2925\\.com/g)].map(m => m[0].toLowerCase()))];
const lines = txt.split(/\n+/).map(s => s.trim()).filter(Boolean);
const snippets = [];
for (let i = 0; i < lines.length; i++) {{
const line = lines[i].toLowerCase();
if (line.includes(aliasLocal) || /openai|chatgpt|tm1\\.openai|验证码|verification/.test(line)) {{
const s = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 9)).join('\n');
if (s.toLowerCase().includes(aliasLocal) && /OpenAI|ChatGPT|tm1\\.openai|验证码|verification/i.test(s)) snippets.push(s);
}}
}}
const codes = [...new Set(snippets.flatMap(s => [...s.matchAll(/(?<!\\d)(\\d{{6}})(?!\\d)/g)].map(m => m[1])))];
return {{ href: location.href, emails, hasAccount: emails.includes(account), hasAliasLocal: txt.toLowerCase().includes(aliasLocal), snippets: snippets.slice(0, 5), codes }};
}} catch (e) {{
return {{ error: String(e), emails: [], snippets: [], codes: [] }};
}}
}})()
"""
data = ev(js) or {"error": "null_eval", "emails": [], "snippets": [], "codes": []}
if account not in data.get("emails", []):
log({"phase": "account_gate_failed", "folder": folder, "expected": account, "emails": data.get("emails", [])})
raise SystemExit(4)
fresh = [c for c in data.get("codes", []) if c not in rejected]
log({
"phase": "poll_alias_code",
"attempt": attempt,
"folder": folder,
"hasAliasLocal": bool(data.get("hasAliasLocal")),
"snippetCount": len(data.get("snippets", [])),
"freshCount": len(fresh),
})
if fresh:
pathlib.Path(args.out_file).write_text(fresh[0])
log({"phase": "code_saved", "folder": folder, "attempt": attempt, "outFile": args.out_file})
return
time.sleep(args.interval)
log({"phase": "timeout_no_current_alias_code", "alias": alias})
raise SystemExit(2)
if __name__ == "__main__":
main()
+138
View File
@@ -0,0 +1,138 @@
#!/usr/bin/env python3
"""2925 account-pool helper. Never prints passwords."""
import argparse, json, os, random, re, stat, string, time
from pathlib import Path
ACCOUNTS_FILE = Path(os.environ.get('MAIL2925_ACCOUNTS_FILE', str(Path.home()/'.hermes/secrets/mail2925_accounts.txt'))).expanduser()
STATE_FILE = Path(os.environ.get('MAIL2925_STATE_FILE', str(Path.home()/'.hermes/state/mail2925_accounts_state.json'))).expanduser()
COOLDOWN_SECONDS = int(os.environ.get('MAIL2925_LIMIT_COOLDOWN_SECONDS', str(24*60*60)))
SUFFIX_LEN = int(os.environ.get('MAIL2925_ALIAS_SUFFIX_LEN', '6'))
EMAIL_RE = re.compile(r'^[^@\s]+@2925\.com$', re.I)
def load_accounts(path=ACCOUNTS_FILE):
text = path.read_text()
out=[]; seen=set()
for raw in text.splitlines():
line=raw.strip()
if not line or line.lower() == '邮箱----密码':
continue
if '----' not in line:
raise ValueError(f'bad account line: {line[:20]}...')
email,pwd=[x.strip() for x in line.split('----',1)]
email=email.lower()
if not EMAIL_RE.match(email):
raise ValueError(f'bad 2925 email: {email}')
if not pwd:
raise ValueError(f'missing password: {email}')
if email in seen:
continue
seen.add(email); out.append({'id': email, 'email': email, 'password': pwd})
return out
def load_state():
if not STATE_FILE.exists(): return {'accounts': {}}
try: return json.loads(STATE_FILE.read_text())
except Exception: return {'accounts': {}}
def save_state(state):
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2, sort_keys=True))
os.chmod(STATE_FILE, 0o600)
def public_account(a, state):
s=state.get('accounts',{}).get(a['email'],{})
now=time.time(); disabled=float(s.get('disabledUntil') or 0)
status='cooldown' if disabled>now else ('disabled' if s.get('enabled') is False else 'ready')
return {'email': a['email'], 'status': status, 'lastUsedAt': int(s.get('lastUsedAt') or 0), 'lastLoginAt': int(s.get('lastLoginAt') or 0), 'disabledUntil': int(disabled), 'lastError': s.get('lastError','')}
def available_accounts(accounts, state):
now=time.time(); out=[]
for a in accounts:
s=state.get('accounts',{}).get(a['email'],{})
if s.get('enabled') is False: continue
if float(s.get('disabledUntil') or 0)>now: continue
out.append(a)
return out
def pick_account(preferred=None, exclude=None):
accounts=load_accounts(); state=load_state(); exclude=set(exclude or [])
pool=[a for a in available_accounts(accounts,state) if a['email'] not in exclude]
if preferred:
preferred=preferred.lower().strip()
for a in pool:
if a['email']==preferred: return a,state
if not pool: raise RuntimeError('no available 2925 account')
def key(a):
s=state.get('accounts',{}).get(a['email'],{})
return (float(s.get('lastUsedAt') or 0), a['email'])
return sorted(pool,key=key)[0], state
def generate_alias_for(account):
local=account['email'].split('@',1)[0].lower()
suffix=''.join(random.choice(string.ascii_lowercase+string.digits) for _ in range(SUFFIX_LEN))
alias=f'{local}{suffix}@2925.com'
assert alias.startswith(local) and alias.endswith('@2925.com')
return alias
def mark(email, **patch):
state=load_state(); d=state.setdefault('accounts',{}).setdefault(email.lower(),{})
d.update(patch); save_state(state)
def cmd_list(args):
accounts=load_accounts(); state=load_state()
print(json.dumps({'count':len(accounts),'accounts':[public_account(a,state) for a in accounts]}, ensure_ascii=False))
def cmd_pick(args):
a,state=pick_account(args.preferred, args.exclude or [])
alias=generate_alias_for(a)
if not args.dry_run:
mark(a['email'], lastUsedAt=int(time.time()), lastError='')
print(json.dumps({'email':a['email'],'alias':alias,'baseLocal':a['email'].split('@')[0],'aliasMatchesAccount':alias.startswith(a['email'].split('@')[0]),'dryRun':bool(args.dry_run)}, ensure_ascii=False))
def cmd_limit(args):
email=args.email.lower().strip(); until=int(time.time()+COOLDOWN_SECONDS)
mark(email, lastLimitAt=int(time.time()), disabledUntil=until, lastError=args.reason or '子邮箱已达上限邮箱')
print(json.dumps({'email':email,'disabledUntil':until,'cooldownSeconds':COOLDOWN_SECONDS}, ensure_ascii=False))
def cmd_mark_login(args):
mark(args.email.lower().strip(), lastLoginAt=int(time.time()), lastError='')
print(json.dumps({'email':args.email.lower().strip(),'ok':True}, ensure_ascii=False))
def cmd_selftest(args):
accounts=load_accounts(); state=load_state()
failures=[]
for a in accounts:
for _ in range(5):
alias=generate_alias_for(a); local=a['email'].split('@')[0]
if not (alias.startswith(local) and alias.endswith('@2925.com') and len(alias.split('@')[0])==len(local)+SUFFIX_LEN):
failures.append({'email':a['email'],'alias':alias})
st=ACCOUNTS_FILE.stat()
ok_mode=(stat.S_IMODE(st.st_mode)==0o600)
print(json.dumps({'accounts':len(accounts),'aliasFailures':failures,'secretMode':oct(stat.S_IMODE(st.st_mode)),'ok':not failures and ok_mode}, ensure_ascii=False))
raise SystemExit(0 if not failures and ok_mode else 1)
def main():
p=argparse.ArgumentParser()
sub=p.add_subparsers(dest='cmd', required=True)
sub.add_parser('list').set_defaults(func=cmd_list)
q=sub.add_parser('pick'); q.add_argument('--preferred'); q.add_argument('--exclude', action='append'); q.add_argument('--dry-run', action='store_true'); q.set_defaults(func=cmd_pick)
l=sub.add_parser('mark-limit'); l.add_argument('email'); l.add_argument('--reason', default=''); l.set_defaults(func=cmd_limit)
m=sub.add_parser('mark-login'); m.add_argument('email'); m.set_defaults(func=cmd_mark_login)
sub.add_parser('selftest').set_defaults(func=cmd_selftest)
args=p.parse_args(); args.func(args)
if __name__=='__main__': main()
@@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""Strict 2925/OpenAI email resend cadence runner.
Cadence encoded from BOSS correction:
for each resend slot:
refresh 2925 Inbox + Junk for N rounds, sleeping between rounds
only then click OpenAI resend once
This script is intentionally env-driven and JSONL-only. It does not buy SMS.
Required environment:
RUN_JSON=/tmp/codex_2925_luban_uk_run.json # contains {"email":..., "alias":...}
Optional environment:
MAIL_CDP=http://127.0.0.1:9227
OPENAI_CDP=http://127.0.0.1:9228
START_RESEND_DONE=0
TARGET_RESENDS=10
REFRESH_ROUNDS=3
SLEEP_BETWEEN_REFRESH_ROUNDS=15
CODE_PATH=/tmp/openai_2925_email_code.txt
"""
import itertools
import json
import os
import pathlib
import time
import urllib.request
import websocket
RUN_JSON = pathlib.Path(os.environ.get("RUN_JSON", "/tmp/codex_2925_luban_uk_run.json"))
RUN = json.loads(RUN_JSON.read_text())
ACCOUNT = RUN["email"]
ALIAS = RUN["alias"]
LOCAL = ALIAS.split("@")[0].lower()
MAIL_CDP = os.environ.get("MAIL_CDP", "http://127.0.0.1:9227")
OPENAI_CDP = os.environ.get("OPENAI_CDP", "http://127.0.0.1:9228")
START_RESEND_DONE = int(os.environ.get("START_RESEND_DONE", "0"))
TARGET_RESENDS = int(os.environ.get("TARGET_RESENDS", "10"))
REFRESH_ROUNDS = int(os.environ.get("REFRESH_ROUNDS", "3"))
SLEEP_BETWEEN_REFRESH_ROUNDS = int(os.environ.get("SLEEP_BETWEEN_REFRESH_ROUNDS", "15"))
CODE_PATH = pathlib.Path(os.environ.get("CODE_PATH", "/tmp/openai_2925_email_code.txt"))
FOLDERS = [
("inbox", "https://2925.com/#/mailList?type=%E6%94%B6%E4%BB%B6%E7%AE%B1"),
("junk", "https://2925.com/#/mailList?type=%E5%9E%83%E5%9C%BE%E7%AE%B1"),
]
def log(obj):
print(json.dumps({**obj, "ts": int(time.time())}, ensure_ascii=False), flush=True)
def tabs(cdp):
return json.loads(urllib.request.urlopen(cdp + "/json", timeout=10).read().decode())
def connect(cdp, pred):
pages = [t for t in tabs(cdp) if t.get("type") == "page"]
selected = next((t for t in pages if pred(t)), None) or (pages[0] if pages else None)
if not selected:
raise RuntimeError(f"no_page_target:{cdp}")
ws = websocket.create_connection(selected["webSocketDebuggerUrl"], timeout=20)
counter = itertools.count(1)
def call(method, params=None, timeout=45):
msg_id = next(counter)
ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
end = time.time() + timeout
while time.time() < end:
msg = json.loads(ws.recv())
if msg.get("id") == msg_id:
if "error" in msg:
raise RuntimeError(msg["error"])
return msg.get("result", {})
raise TimeoutError(method)
call("Page.enable")
call("Runtime.enable")
return call
def evaluate(call, js, await_promise=False, timeout=45):
result = call(
"Runtime.evaluate",
{"expression": js, "awaitPromise": await_promise, "returnByValue": True},
timeout,
)
return result.get("result", {}).get("value")
def refresh_once(folder, url, resend_index, refresh_round):
call = connect(MAIL_CDP, lambda t: "2925.com" in (t.get("url") or ""))
call("Page.navigate", {"url": url})
time.sleep(2)
js = r'''(async(local,account)=>{
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const click=e=>{if(!e)return false; e.dispatchEvent(new MouseEvent('mousedown',{bubbles:true})); e.dispatchEvent(new MouseEvent('mouseup',{bubbles:true})); e.click(); return true;};
const refreshButtons=[...document.querySelectorAll('button,.el-button,a,span,div')].filter(e=>(e.innerText||e.textContent||'').trim()==='刷新');
const clicked=click(refreshButtons[refreshButtons.length-1]||refreshButtons[0]);
await sleep(2500);
const txt=document.body?.innerText||'';
const low=txt.toLowerCase();
const emails=[...new Set([...txt.matchAll(/[A-Za-z0-9._%+-]+@2925\.com/g)].map(m=>m[0].toLowerCase()))];
const lines=txt.split(/\n+/).map(s=>s.trim()).filter(Boolean);
const snippets=[];
for(let i=0;i<lines.length;i++){
const around=lines.slice(Math.max(0,i-8),Math.min(lines.length,i+12)).join('\n');
if(around.toLowerCase().includes(local)&&/OpenAI|ChatGPT|验证码|verification|code/i.test(around)) snippets.push(around);
}
const codes=[...new Set(snippets.flatMap(s=>[...s.matchAll(/(?<!\d)(\d{6})(?!\d)/g)].map(m=>m[1])))];
return {clicked,emails,accountOk:emails.includes(account.toLowerCase()),hasLocal:low.includes(local),hasOpenAI:/OpenAI|ChatGPT|验证码|verification|code/i.test(txt),codes};
})(LOCAL,ACCOUNT)'''.replace("LOCAL", json.dumps(LOCAL)).replace("ACCOUNT", json.dumps(ACCOUNT))
val = evaluate(call, js, True) or {}
log({
"phase": "mail_refresh",
"resend_index": resend_index,
"refresh_round": refresh_round,
"folder": folder,
"clicked": val.get("clicked"),
"accountOk": val.get("accountOk"),
"hasLocal": val.get("hasLocal"),
"hasOpenAI": val.get("hasOpenAI"),
"codesCount": len(val.get("codes") or []),
"emails": val.get("emails"),
})
if not val.get("accountOk"):
log({"phase": "account_mismatch", "expected": ACCOUNT, "emails": val.get("emails")})
raise SystemExit(4)
if val.get("codes"):
CODE_PATH.write_text(val["codes"][0])
log({"phase": "current_alias_code_saved", "resend_index": resend_index, "folder": folder})
return True
return False
def click_resend(resend_index):
call = connect(OPENAI_CDP, lambda t: "auth.openai.com" in (t.get("url") or ""))
res = evaluate(call, r'''(async()=>{
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const btn=[...document.querySelectorAll('button,a')].find(e=>/重新发送|Resend/i.test(e.innerText||e.textContent||''));
if(btn){btn.scrollIntoView({block:'center'}); btn.click(); await sleep(2500);}
return {clicked:!!btn,href:location.href,body:(document.body?.innerText||'').slice(0,260)};
})()''', True) or {}
log({"phase": "openai_resend", "resend_index": resend_index, "clicked": res.get("clicked"), "href": res.get("href")})
if not res.get("clicked"):
log({"phase": "openai_resend_not_found", "resend_index": resend_index, "body": res.get("body")})
raise SystemExit(5)
def main():
log({
"phase": "strict_resend_cadence_start",
"alias": ALIAS,
"account": ACCOUNT,
"start_resend_done": START_RESEND_DONE,
"target_resends": TARGET_RESENDS,
"refresh_rounds": REFRESH_ROUNDS,
})
for resend_index in range(START_RESEND_DONE + 1, TARGET_RESENDS + 1):
log({"phase": "pre_resend_refresh_block_start", "resend_index": resend_index})
for refresh_round in range(1, REFRESH_ROUNDS + 1):
for name, url in FOLDERS:
if refresh_once(name, url, resend_index, refresh_round):
return 0
if refresh_round < REFRESH_ROUNDS:
time.sleep(SLEEP_BETWEEN_REFRESH_ROUNDS)
click_resend(resend_index)
log({"phase": "no_code_after_target_resends", "alias": ALIAS, "total_resends": TARGET_RESENDS})
return 2
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Continue OpenAI add-phone verification with 5sim until SMS succeeds.
Designed for codex-oauth-plus-onboarding runs on BOSS infra.
Reads secrets from ~/.hermes/env/codex_oauth_onboarding.env and never prints them.
Emits compact JSONL and tracks cumulative phone-number usage.
Environment knobs:
CDP_PORT=9226
START_USED_NUMBERS=57
MAX_NEW_NUMBERS=300 # 0 means unlimited; keep a guard unless BOSS explicitly approves
PHONE_POLL_SECONDS=75
PER_COUNTRY_ROUND=3
"""
import json, urllib.request, urllib.error, websocket, itertools, time, pathlib, re, shlex, sys, os
CDP_PORT=os.environ.get('CDP_PORT','9226')
CDP=f'http://127.0.0.1:{CDP_PORT}'
POLL_SECONDS=int(os.environ.get('PHONE_POLL_SECONDS','75'))
START_USED=int(os.environ.get('START_USED_NUMBERS','0'))
MAX_NEW_NUMBERS=int(os.environ.get('MAX_NEW_NUMBERS','300'))
PER_COUNTRY_ROUND=int(os.environ.get('PER_COUNTRY_ROUND','3'))
COUNTRIES=[
('argentina','AR','阿根廷','54'), ('netherlands','NL','荷兰','31'), ('germany','DE','德国','49'),
('indonesia','ID','印度尼西亚','62'), ('vietnam','VN','越南','84'), ('italy','IT','意大利','39'),
('poland','PL','波兰','48'), ('spain','ES','西班牙','34'), ('england','GB','英国','44'),
('france','FR','法国','33'), ('malaysia','MY','马来西亚','60'), ('thailand','TH','泰国','66'),
('philippines','PH','菲律宾','63'), ('colombia','CO','哥伦比亚','57'), ('mexico','MX','墨西哥','52'),
('brazil','BR','巴西','55'), ('peru','PE','秘鲁','51'), ('turkey','TR','土耳其','90'),
('kazakhstan','KZ','哈萨克斯坦','7'), ('chile','CL','智利','56'), ('portugal','PT','葡萄牙','351'),
('romania','RO','罗马尼亚','40'),
]
FIVESIM_TO_LUBAN_COUNTRY={
'argentina':'Argentina','netherlands':'Netherlands','germany':'Germany','indonesia':'Indonesia','vietnam':'Vietnam',
'italy':'Italy','poland':'Poland','spain':'Spain','england':'United Kingdom/England','france':'France',
'malaysia':'Malaysia','thailand':'Thailand','philippines':'Philippines','colombia':'Colombia','mexico':'Mexico',
'brazil':'Brazil','peru':'Peru','turkey':'Turkey','kazakhstan':'Kazakhstan','chile':'Chile','portugal':'Portugal','romania':'Romania'
}
ENV=pathlib.Path.home()/'.hermes/env/codex_oauth_onboarding.env'; env={}
for line in ENV.read_text().splitlines():
line=line.strip()
if not line or line.startswith('#') or '=' not in line: continue
if line.startswith('export '): line=line[7:].strip()
k,v=line.split('=',1); k=k.strip(); v=v.strip()
if re.match(r'^[A-Za-z_][A-Za-z0-9_]*$',k):
try:
if v and v[0] in '"\'': v=shlex.split(v)[0]
except Exception: pass
env[k]=v.strip('"\'')
PHONE_SMS_PROVIDER=os.environ.get('PHONE_SMS_PROVIDER') or env.get('PHONE_SMS_PROVIDER','5sim').lower()
FIVE_SIM_KEY=env.get('FIVE_SIM_API_KEY')
LUBAN_KEY=env.get('LUBAN_SMS_API_KEY') or env.get('LUBAN_API_KEY')
LUBAN_BASE=(env.get('LUBAN_SMS_API_BASE') or 'https://lubansms.com/v2/api').rstrip('/')
LUBAN_SERVICE_QUERY=env.get('LUBAN_SMS_SERVICE_QUERY','OpenAI')
LUBAN_LANGUAGE=env.get('LUBAN_SMS_LANGUAGE','en')
if PHONE_SMS_PROVIDER == 'luban' and not LUBAN_KEY:
raise SystemExit('PHONE_SMS_PROVIDER=luban but LUBAN_SMS_API_KEY missing')
if PHONE_SMS_PROVIDER != 'luban' and not FIVE_SIM_KEY:
raise SystemExit('FIVE_SIM_API_KEY missing')
STATE_FILE=pathlib.Path('/tmp/phone_verify_until_success_state.json')
def log(obj): obj.setdefault('ts', int(time.time())); print(json.dumps(obj,ensure_ascii=False), flush=True)
def save_state(**kw):
cur={}
if STATE_FILE.exists():
try: cur=json.loads(STATE_FILE.read_text())
except Exception: cur={}
cur.update(kw); STATE_FILE.write_text(json.dumps(cur,ensure_ascii=False,indent=2))
def strict_code(text):
m=re.search(r'(?<!\\d)(\\d{6})(?!\\d)', text or '')
return m.group(1) if m else None
def http_json(url, headers=None, timeout=30):
req=urllib.request.Request(url,headers=headers or {'Accept':'application/json'})
try:
with urllib.request.urlopen(req,timeout=timeout) as r: txt=r.read().decode(); st=r.status
except urllib.error.HTTPError as e: txt=e.read().decode(errors='ignore'); st=e.code
except Exception as e: return 0, {'error':type(e).__name__+': '+str(e)}
try: data=json.loads(txt)
except Exception: data={'raw':txt[:300]}
return st,data
def fivesim(path):
return http_json('https://5sim.net/v1'+path, headers={'Authorization':'Bearer '+FIVE_SIM_KEY,'Accept':'application/json'})
def luban(endpoint, **params):
params={k:v for k,v in params.items() if v is not None}; params['apikey']=LUBAN_KEY
url=LUBAN_BASE+'/'+endpoint.lstrip('/')+'?'+urllib.parse.urlencode(params)
return http_json(url, headers={'Accept':'application/json','User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36'})
def luban_services(country=None, service=None, page=1):
return luban('List', country=country, service=service or LUBAN_SERVICE_QUERY, language=LUBAN_LANGUAGE, page=page)
def luban_resolve_service(country_en=None):
candidates=[]
for page in range(1,4):
st,data=luban_services(country_en, LUBAN_SERVICE_QUERY, page)
if st==200 and data.get('code')==0 and isinstance(data.get('msg'),list):
for r in data['msg']:
if not isinstance(r,dict) or not r.get('service_id'): continue
name=str(r.get('service_name') or '')
ctry=str(r.get('country_name_en') or '')
if LUBAN_SERVICE_QUERY.lower() in name.lower() or 'openai' in name.lower() or 'chatgpt' in name.lower():
candidates.append(r)
if not candidates: return None
def cost(r):
try: return float(r.get('cost'))
except Exception: return 999999.0
return sorted(candidates,key=cost)[0]
def luban_buy(country_en=None):
svc=luban_resolve_service(country_en)
if not svc: return 200, {'code':400,'msg':'no_luban_service'}
st,data=luban('getNumber', service_id=svc['service_id'])
if isinstance(data,dict): data['_service']=svc
return st,data
def sms_buy(country):
if PHONE_SMS_PROVIDER == 'luban': return luban_buy(FIVESIM_TO_LUBAN_COUNTRY.get(country,country))
return fivesim(f'/user/buy/activation/{country}/any/openai')
def sms_check(act):
if PHONE_SMS_PROVIDER == 'luban': return luban('getSms', request_id=act)
return fivesim(f'/user/check/{act}')
def sms_cancel(act):
if PHONE_SMS_PROVIDER == 'luban': return luban('setStatus', request_id=act, status='reject')
return fivesim(f'/user/cancel/{act}')
def sms_finish(act):
if PHONE_SMS_PROVIDER == 'luban': return 200, {'code':0,'msg':'success'}
return fivesim(f'/user/finish/{act}')
def cdp():
tabs=json.load(urllib.request.urlopen(CDP+'/json/list', timeout=10))
pages=[t for t in tabs if t.get('type')=='page' and 'auth.openai.com' in t.get('url','')] or [t for t in tabs if t.get('type')=='page']
page=pages[0]
ws=websocket.create_connection(page['webSocketDebuggerUrl'], timeout=10, suppress_origin=True); c=itertools.count(1)
def call(m,p=None,timeout=30):
i=next(c); ws.send(json.dumps({'id':i,'method':m,'params':p or {}})); end=time.time()+timeout
while time.time()<end:
msg=json.loads(ws.recv())
if msg.get('id')==i:
if 'error' in msg: raise RuntimeError(msg['error'])
return msg.get('result')
raise TimeoutError(m)
def ev(e,timeout=90): return call('Runtime.evaluate',{'expression':e,'returnByValue':True,'awaitPromise':True},timeout).get('result',{})
call('Runtime.enable'); return ws,call,ev
def navigate(url, wait=6):
ws,call,ev=cdp()
try: call('Page.enable'); call('Page.navigate',{'url':url}); time.sleep(wait)
finally:
try: ws.close()
except Exception: pass
def page_state():
ws,call,ev=cdp()
try: return ev("({href:location.href,title:document.title,body:(document.body.innerText||'').slice(0,1500)})").get('value')
finally:
try: ws.close()
except Exception: pass
def ensure_add_phone():
st=page_state() or {}; href=st.get('href',''); body=st.get('body','')
if 'account_deactivated' in body.lower() or 'deactivated' in body.lower(): log({'phase':'account_deactivated_stop','href':href}); sys.exit(4)
if 'add-phone' in href: return True
navigate('https://auth.openai.com/add-phone', 8)
st=page_state() or {}; ok='add-phone' in st.get('href','')
if not ok: log({'phase':'recover_add_phone_fail','state':st})
return ok
def select_country(iso, cname):
if not ensure_add_phone(): return False, {'error':'not_add_phone'}
ws,call,ev=cdp()
try:
gate=ev(f"""(async()=>{{const sel=document.querySelector('select'); if(sel){{sel.value={json.dumps(iso)}; sel.dispatchEvent(new Event('input',{{bubbles:true}})); sel.dispatchEvent(new Event('change',{{bubbles:true}})); await new Promise(r=>setTimeout(r,900));}} const tel=document.querySelector('input[type=tel]'); if(tel) tel.focus(); return {{href:location.href,countryValue:sel?.value,countryText:sel?.options?.[sel.selectedIndex]?.text,body:(document.body.innerText||'').slice(0,1200)}};}})()""").get('value')
for typ,key,code,wvc in [('keyDown','a','KeyA',65),('keyUp','a','KeyA',65),('keyDown','Backspace','Backspace',8),('keyUp','Backspace','Backspace',8)]:
call('Input.dispatchKeyEvent',{'type':typ,'key':key,'code':code,'windowsVirtualKeyCode':wvc,'nativeVirtualKeyCode':wvc,'modifiers':2 if key=='a' else 0})
finally:
try: ws.close()
except Exception: pass
ok=gate and 'add-phone' in gate.get('href','') and gate.get('countryValue')==iso and cname in (gate.get('body','')+gate.get('countryText',''))
return ok,gate
def submit_phone(iso,phone):
digits=re.sub(r'\D','',str(phone)); dial=next((d for _,i,_,d in COUNTRIES if i==iso),''); local=digits
if dial and local.startswith(dial): local=local[len(dial):]
if local.startswith('0'): local=local[1:]
ws,call,ev=cdp()
try:
ev("document.querySelector('input[type=tel]')?.focus()"); call('Input.insertText',{'text':local}); time.sleep(.2)
return ev("""(async()=>{const sel=document.querySelector('select'); const tel=document.querySelector('input[type=tel]'); const btn=[...document.querySelectorAll('button')].find(b=>/继续|Continue|Next/.test(b.innerText)); const before={countryValue:sel?.value,countryText:sel?.options?.[sel.selectedIndex]?.text,tel_len:tel?.value?.length}; btn?.click(); await new Promise(r=>setTimeout(r,8500)); return {before,href:location.href,title:document.title,body:(document.body.innerText||'').slice(0,2200)};})()""",140).get('value')
finally:
try: ws.close()
except Exception: pass
def classify_submit(res):
body=res.get('body','') if res else ''; href=res.get('href','') if res else ''
rejected=any(x in body for x in ['无法向此电话号码发送验证码','电话号码无效']) or any(x in body.lower() for x in ['invalid phone','unable to send','try again later'])
sms_page=('phone-verification' in href) or ('验证码' in body and '重新发送' in body and not rejected)
deact='account_deactivated' in body.lower() or 'deactivated' in body.lower()
return rejected,sms_page,deact
def poll_sms(act):
end=time.time()+POLL_SECONDS; last={}
while time.time()<end:
st,data=sms_check(act); last=data if isinstance(data,dict) else {}
if PHONE_SMS_PROVIDER == 'luban':
msg=str(last.get('msg','')).lower()
if msg == 'success' and last.get('sms_code'):
return str(last.get('sms_code')), last
code=strict_code(json.dumps(last,ensure_ascii=False))
if code: return code,last
sms=last.get('sms') or []
if sms:
text=' '.join(str(x) for item in sms for x in (item.values() if isinstance(item,dict) else [item]))
m=re.search(r'(?<!\d)(\d{4,8})(?!\d)', text); return m.group(1) if m else 'SMS_PRESENT_NO_CODE', last
time.sleep(5)
return None,last
def submit_code(code):
ws,call,ev=cdp()
try:
ev("document.querySelector('input[name=code],input[autocomplete=one-time-code],input[type=text]')?.focus()"); call('Input.insertText',{'text':code}); time.sleep(.2)
return ev("""(async()=>{const btn=[...document.querySelectorAll('button')].find(b=>/继续|Continue|Next|验证|Verify/.test(b.innerText)); btn?.click(); await new Promise(r=>setTimeout(r,12000)); return {href:location.href,title:document.title,body:(document.body.innerText||'').slice(0,2000)};})()""",150).get('value')
finally:
try: ws.close()
except Exception: pass
new_total=0; rejected_total=0; sms0_total=0; nofree_count=0; country_counts={c:0 for c,_,_,_ in COUNTRIES}; round_no=0
log({'phase':'until_success_start','start_used_numbers':START_USED,'max_new_numbers':MAX_NEW_NUMBERS or 'unlimited','poll_seconds':POLL_SECONDS})
while True:
round_no += 1
for country,iso,cname,dial in COUNTRIES:
for _ in range(PER_COUNTRY_ROUND):
if MAX_NEW_NUMBERS and new_total >= MAX_NEW_NUMBERS:
log({'phase':'safety_max_reached_no_success','start_used_numbers':START_USED,'new_numbers':new_total,'total_numbers':START_USED+new_total,'rejected_total':rejected_total,'sms0_total':sms0_total,'nofree_count':nofree_count,'counts':country_counts}); sys.exit(3)
ok,gate=select_country(iso,cname)
if not ok: log({'phase':'country_gate_fail','country':country,'iso':iso,'gate':gate}); break
st,data=sms_buy(country)
act=(data.get('request_id') or data.get('id')) if isinstance(data,dict) else None; phone=(data.get('number') or data.get('phone')) if isinstance(data,dict) else None
if not(act and phone):
nofree_count+=1; body=(data.get('raw') or data.get('error') or str(data))[:120] if isinstance(data,dict) else str(data)[:120]
log({'phase':'buy_no_activation','country':country,'http':st,'body':body,'nofree_count':nofree_count}); break
new_total+=1; country_counts[country]+=1; save_state(current_activation_id=act,new_numbers=new_total,total_numbers=START_USED+new_total,country=country); pathlib.Path('/tmp/current_fivesim_activation_id.txt').write_text(str(act))
log({'phase':'activation_bought','new_numbers':new_total,'total_numbers':START_USED+new_total,'country':country,'activation_id':act})
try: res=submit_phone(iso,phone)
except Exception as e:
st2,_=sms_cancel(act); log({'phase':'submit_exception_cancel','activation_id':act,'error':type(e).__name__,'http':st2,'ok':200<=st2<300}); navigate('https://auth.openai.com/add-phone',6); continue
rejected,sms_page,deact=classify_submit(res)
log({'phase':'phone_submitted','new_numbers':new_total,'total_numbers':START_USED+new_total,'country':country,'activation_id':act,'href':res.get('href') if res else None,'rejected':rejected,'sms_page':sms_page,'deactivated':deact})
if deact: sms_cancel(act); log({'phase':'account_deactivated_stop','activation_id':act,'total_numbers':START_USED+new_total}); sys.exit(4)
if rejected:
rejected_total+=1; st2,_=sms_cancel(act); log({'phase':'activation_cancel_rejected','activation_id':act,'http':st2,'ok':200<=st2<300}); continue
code,sdata=poll_sms(act)
if code and code!='SMS_PRESENT_NO_CODE':
pathlib.Path('/tmp/current_phone_sms_code.txt').write_text(code); log({'phase':'sms_received','new_numbers':new_total,'total_numbers':START_USED+new_total,'country':country,'activation_id':act})
res2=submit_code(code); log({'phase':'sms_code_submitted','href':res2.get('href'),'title':res2.get('title'),'total_numbers':START_USED+new_total}); sms_finish(act); save_state(success=True,success_activation_id=act,total_numbers=START_USED+new_total,country=country,provider=PHONE_SMS_PROVIDER,href=res2.get('href')); sys.exit(0)
sms0_total+=1; st2,_=sms_cancel(act); log({'phase':'sms_timeout_cancel','new_numbers':new_total,'total_numbers':START_USED+new_total,'country':country,'activation_id':act,'http':st2,'ok':200<=st2<300}); navigate('https://auth.openai.com/add-phone',6)
log({'phase':'round_done_no_success','round':round_no,'new_numbers':new_total,'total_numbers':START_USED+new_total,'rejected_total':rejected_total,'sms0_total':sms0_total,'nofree_count':nofree_count,'counts':country_counts})