Add Luban SMS provider for OpenAI phone verification

This commit is contained in:
2026-05-11 10:15:12 +08:00
parent 26d3c26933
commit 26f42427be
4 changed files with 1027 additions and 0 deletions
+236
View File
@@ -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())
+245
View File
@@ -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})