246 lines
16 KiB
Python
246 lines
16 KiB
Python
#!/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})
|