128 lines
7.5 KiB
Python
128 lines
7.5 KiB
Python
import json, pathlib, urllib.request, urllib.parse, re, subprocess, time, sys
|
|
ENV=pathlib.Path('/Users/chick/.hermes/env/codex_oauth_onboarding.env')
|
|
text=ENV.read_text()
|
|
def get(k,default=''):
|
|
m=re.search(r'^'+re.escape(k)+r'=(.*)$', text, re.M)
|
|
return (m.group(1).strip().strip('"\'') if m else default)
|
|
TOKEN=get('FIVE_SIM_API_KEY') or get('FIVESIM_API_KEY')
|
|
ORDER=[x.strip() for x in get('FIVE_SIM_COUNTRY_ORDER','').split(',') if x.strip()] or ([get('FIVE_SIM_COUNTRY_ID')] if get('FIVE_SIM_COUNTRY_ID') else ['vietnam'])
|
|
LIMIT=int(get('PHONE_VERIFICATION_REPLACEMENT_LIMIT','3') or '3')
|
|
PRODUCT=get('FIVE_SIM_PRODUCT','openai') or 'openai'
|
|
OP=get('FIVE_SIM_OPERATOR','any') or 'any'
|
|
META={'argentina':('阿根廷','54'),'netherlands':('荷兰','31'),'indonesia':('印度尼西亚','62'),'vietnam':('越南','84')}
|
|
HEAD={'Authorization':'Bearer '+TOKEN,'Accept':'application/json'}
|
|
|
|
def emit(**kw): print(json.dumps(kw,ensure_ascii=False),flush=True)
|
|
def req(method,url,data=None,timeout=30):
|
|
body=None
|
|
if data is not None: body=json.dumps(data).encode()
|
|
r=urllib.request.Request(url,data=body,headers=HEAD,method=method)
|
|
if body: r.add_header('Content-Type','application/json')
|
|
with urllib.request.urlopen(r,timeout=timeout) as resp: return json.loads(resp.read().decode())
|
|
def cdp(js,match='auth.openai.com',timeout=40):
|
|
p=subprocess.run(['bash','-lc',f"CDP_MATCH='{match}' python3 /tmp/cdp_async_eval.py"],input=js,text=True,capture_output=True,timeout=timeout)
|
|
return (p.stdout or '')+(p.stderr or '')
|
|
def page_state():
|
|
return cdp("""return {url:location.href,title:document.title,text:document.body.innerText.slice(0,1600),country:[...document.querySelectorAll('button')].map(b=>b.innerText).find(t=>/\\(\\+\\d+\\)/.test(t)),tel:document.querySelector('input[type=tel]')?.value};""",timeout=25)
|
|
def select_country(country):
|
|
label,prefix=META[country]
|
|
iso={"argentina":"AR","netherlands":"NL","indonesia":"ID","vietnam":"VN"}[country]
|
|
js=f"""
|
|
const iso={json.dumps(iso)};
|
|
const sel=document.querySelector('select');
|
|
sel.focus();
|
|
sel.value=iso;
|
|
sel.dispatchEvent(new Event('input',{{bubbles:true}}));
|
|
sel.dispatchEvent(new Event('change',{{bubbles:true}}));
|
|
await new Promise(r=>setTimeout(r,800));
|
|
return {{selectValue:sel.value,country:[...document.querySelectorAll('button')].map(b=>b.innerText).find(t=>/\\(\\+\\d+\\)/.test(t)),text:document.body.innerText.slice(0,700)}};
|
|
"""
|
|
return cdp(js,timeout=25)
|
|
def buy(country):
|
|
url=f'https://5sim.net/v1/user/buy/activation/{urllib.parse.quote(country)}/{urllib.parse.quote(OP)}/{urllib.parse.quote(PRODUCT)}'
|
|
return req('GET',url,timeout=40)
|
|
def cancel(aid):
|
|
try: return req('GET',f'https://5sim.net/v1/user/cancel/{aid}',timeout=20)
|
|
except Exception as e: return {'error':str(e)}
|
|
def check(aid):
|
|
return req('GET',f'https://5sim.net/v1/user/check/{aid}',timeout=20)
|
|
def submit_phone(country,phone):
|
|
label,prefix=META[country]
|
|
digits=re.sub(r'\D+','',phone)
|
|
local=digits[len(prefix):] if digits.startswith(prefix) else digits
|
|
js=f"""
|
|
const local={json.dumps(local)};
|
|
const input=document.querySelector('input[type=tel]');
|
|
const setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;
|
|
input.focus(); setter.call(input,''); input.dispatchEvent(new InputEvent('input',{{bubbles:true,inputType:'deleteContentBackward'}}));
|
|
setter.call(input,local); input.dispatchEvent(new InputEvent('input',{{bubbles:true,inputType:'insertText',data:local}})); input.dispatchEvent(new Event('change',{{bubbles:true}}));
|
|
await new Promise(r=>setTimeout(r,500));
|
|
let btn=[...document.querySelectorAll('button')].find(b=>(b.innerText||'').trim()==='继续'); btn?.click();
|
|
await new Promise(r=>setTimeout(r,12000));
|
|
return {{url:location.href,title:document.title,text:document.body.innerText.slice(0,1200),country:[...document.querySelectorAll('button')].map(b=>b.innerText).find(t=>/\\(\\+\\d+\\)/.test(t)),tel:document.querySelector('input[type=tel]')?.value}};
|
|
"""
|
|
return cdp(js,timeout=45)
|
|
def resend_phone():
|
|
js="""const btn=[...document.querySelectorAll('button')].find(b=>/重新发送/.test(b.innerText||'')); btn?.click(); await new Promise(r=>setTimeout(r,3000)); return {clicked:!!btn,text:document.body.innerText.slice(0,800),url:location.href};"""
|
|
return cdp(js,timeout=20)
|
|
|
|
def poll_sms(aid,rounds=8):
|
|
for i in range(rounds):
|
|
time.sleep(12)
|
|
st=check(aid)
|
|
sms=st.get('sms') or []
|
|
emit(phase='sms_poll',activation_id=aid,round=i+1,status=st.get('status'),sms_count=len(sms))
|
|
if sms:
|
|
txt=json.dumps(sms,ensure_ascii=False)
|
|
m=re.search(r'(?<!\d)(\d{4,8})(?!\d)',txt)
|
|
if m: return m.group(1),st
|
|
return None,None
|
|
|
|
emit(phase='phone_start',countries=ORDER,limit=LIMIT)
|
|
for country in ORDER:
|
|
if country not in META:
|
|
emit(phase='country_skip',country=country,reason='missing_meta'); continue
|
|
for attempt in range(1,LIMIT+1):
|
|
sel=select_country(country)
|
|
if META[country][0] not in sel or META[country][1] not in sel:
|
|
emit(phase='country_select_failed',country=country,attempt=attempt,out=sel[-500:])
|
|
continue
|
|
act=buy(country)
|
|
aid=act.get('id'); phone=act.get('phone')
|
|
pathlib.Path('/tmp/fivesim_activation.json').write_text(json.dumps(act,ensure_ascii=False,indent=2))
|
|
emit(phase='buy',country=country,attempt=attempt,activation_id=aid,masked=re.sub(r'(\+?\d{4})\d+(\d{3})',r'\1***\2',str(phone)),status=act.get('status'))
|
|
out=submit_phone(country,phone)
|
|
if '电话号码无效' in out or '美国 (+1)' in out and country!='usa':
|
|
emit(phase='submit_invalid_or_country_revert',country=country,activation_id=aid)
|
|
emit(phase='cancel',activation_id=aid,result=cancel(aid).get('status'))
|
|
continue
|
|
if '无法向此电话号码发送验证码' in out:
|
|
emit(phase='direct_reject',country=country,activation_id=aid)
|
|
emit(phase='cancel',activation_id=aid,result=cancel(aid).get('status'))
|
|
continue
|
|
emit(phase='accepted_or_waiting',country=country,activation_id=aid,state=out[-300:])
|
|
code,st=poll_sms(aid,rounds=5)
|
|
if not code:
|
|
emit(phase='resend_phone',activation_id=aid,out=resend_phone()[-300:])
|
|
code,st=poll_sms(aid,rounds=5)
|
|
if code:
|
|
emit(phase='sms_code_received',activation_id=aid,code='[REDACTED]')
|
|
js=f"""
|
|
const code={json.dumps(code)};
|
|
let inputs=[...document.querySelectorAll('input')].filter(i=>i.type==='text'||i.type==='tel'||i.inputMode==='numeric');
|
|
let input=inputs.find(i=>i!==document.querySelector('input[type=tel]'))||inputs[0];
|
|
const setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set;
|
|
input.focus(); setter.call(input,code); input.dispatchEvent(new InputEvent('input',{{bubbles:true,inputType:'insertText',data:code}})); input.dispatchEvent(new Event('change',{{bubbles:true}}));
|
|
await new Promise(r=>setTimeout(r,500));
|
|
[...document.querySelectorAll('button')].find(b=>/继续|Continue|验证/.test(b.innerText||''))?.click();
|
|
await new Promise(r=>setTimeout(r,15000));
|
|
return {{url:location.href,title:document.title,text:document.body.innerText.slice(0,1600)}};
|
|
"""
|
|
final=cdp(js,timeout=45)
|
|
emit(phase='phone_code_submitted',activation_id=aid,state=final[-500:])
|
|
sys.exit(0)
|
|
emit(phase='no_sms_after_resend',country=country,activation_id=aid)
|
|
emit(phase='cancel',activation_id=aid,result=cancel(aid).get('status'))
|
|
emit(phase='phone_failed_all')
|
|
sys.exit(3)
|