import json, pathlib, re, subprocess, sys, time, urllib.request, urllib.parse ENV = pathlib.Path('/Users/chick/.hermes/env/codex_oauth_onboarding.env').read_text() def get(k, d=''): m = re.search(r'^' + re.escape(k) + r'=(.*)$', ENV, re.M) return (m.group(1).strip().strip('"\'') if m else d) TOKEN = get('FIVE_SIM_API_KEY') or get('FIVESIM_API_KEY') HEAD = {'Authorization': 'Bearer ' + TOKEN, 'Accept': 'application/json'} ORDER = [x.strip() for x in get('FIVE_SIM_COUNTRY_ORDER', '').split(',') if x.strip()] or ['indonesia'] LIMIT = int(get('PHONE_VERIFICATION_REPLACEMENT_LIMIT', '10') or '10') PRODUCT = get('FIVE_SIM_PRODUCT', 'openai') or 'openai' OPERATOR = get('FIVE_SIM_OPERATOR', 'any') or 'any' META = { 'argentina': {'iso': 'AR', 'visible': '阿根廷 (+54)', 'prefix': '54'}, 'netherlands': {'iso': 'NL', 'visible': '荷兰 (+31)', 'prefix': '31'}, 'indonesia': {'iso': 'ID', 'visible': '印度尼西亚 (+62)', 'prefix': '62'}, 'england': {'iso': 'GB', 'visible': '英国 (+44)', 'prefix': '44'}, 'italy': {'iso': 'IT', 'visible': '意大利 (+39)', 'prefix': '39'}, 'poland': {'iso': 'PL', 'visible': '波兰 (+48)', 'prefix': '48'}, 'spain': {'iso': 'ES', 'visible': '西班牙 (+34)', 'prefix': '34'}, 'vietnam': {'iso': 'VN', 'visible': '越南 (+84)', 'prefix': '84'}, } def emit(**kw): print(json.dumps(kw, ensure_ascii=False), flush=True) def req_json(url): req = urllib.request.Request(url, headers=HEAD) with urllib.request.urlopen(req, timeout=45) as r: return json.loads(r.read().decode()) def cancel(aid): try: return req_json(f'https://5sim.net/v1/user/cancel/{aid}') except Exception as e: return {'error': str(e)} def finish(aid): try: return req_json(f'https://5sim.net/v1/user/finish/{aid}') except Exception as e: return {'error': str(e)} def check(aid): return req_json(f'https://5sim.net/v1/user/check/{aid}') def cdp(js, match='auth.openai.com', timeout=45): 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 close_stale_oauth_tabs(reason='recover'): # Avoid tab pile-up and CDP target ambiguity before starting a fresh OAuth/add-phone recovery. try: tabs = json.loads(urllib.request.urlopen('http://127.0.0.1:9223/json/list', timeout=5).read().decode()) except Exception as e: emit(phase='close_stale_oauth_tabs_failed', reason=reason, error=str(e)) return 0 patterns = ('auth.openai.com', 'chatgpt.com/auth', 'localhost', '127.0.0.1') closed = 0 for t in tabs: url = t.get('url', '') tid = t.get('id') if tid and any(x in url for x in patterns): try: urllib.request.urlopen('http://127.0.0.1:9223/json/close/' + urllib.parse.quote(tid, safe=''), timeout=3).read() closed += 1 time.sleep(0.05) except Exception: pass if closed: emit(phase='close_stale_oauth_tabs', reason=reason, closed_count=closed) return closed def current_state(): return cdp(""" return {url: location.href, title: document.title, country: [...document.querySelectorAll('button')].map(b=>b.innerText).find(t=>/\(\+\d+\)/.test(t)), selectValue: document.querySelector('select')?.value, tel: document.querySelector('input[type=tel]')?.value, text: document.body.innerText.slice(0, 900)}; """, timeout=25) def restore_add_phone(): st = current_state() if 'auth.openai.com/add-phone' in st: return True # Generate fresh OAuth URL and login with existing account. This account is already email-verified. close_stale_oauth_tabs('before_oauth_recover') raw = get('CODEX2API_URL') u = urllib.parse.urlsplit(raw) origin = f'{u.scheme}://{u.netloc}' if u.scheme and u.netloc else raw.rstrip('/') req = urllib.request.Request(origin + '/api/admin/oauth/generate-auth-url', data=b'{}', headers={'X-Admin-Key': get('CODEX2API_ADMIN_KEY'), 'Content-Type': 'application/json'}) with urllib.request.urlopen(req, timeout=20) as r: data = json.loads(r.read().decode()) url = data.get('auth_url') or data.get('url') emit(phase='oauth_recover', session_id=data.get('session_id') or data.get('id')) urllib.request.urlopen(urllib.request.Request('http://127.0.0.1:9223/json/new?' + urllib.parse.quote(url, safe=''), method='PUT'), timeout=10).read() time.sleep(6) email = json.loads(pathlib.Path('/tmp/current_codex_run.json').read_text())['email'] pw = get('CUSTOM_PASSWORD') js = f""" const email={json.dumps(email)}, pw={json.dumps(pw)}; const setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; let emailI=document.querySelector('input[type=email], input[name=email], input[autocomplete=username], input'); if(emailI && location.href.includes('/log-in')){{emailI.focus(); setter.call(emailI,email); emailI.dispatchEvent(new InputEvent('input',{{bubbles:true,inputType:'insertText',data:email}})); emailI.dispatchEvent(new Event('change',{{bubbles:true}})); [...document.querySelectorAll('button')].find(b=>(b.innerText||'').trim()==='继续')?.click();}} await new Promise(r=>setTimeout(r,6000)); let pwI=document.querySelector('input[type=password]'); if(pwI){{pwI.focus(); setter.call(pwI,pw); pwI.dispatchEvent(new InputEvent('input',{{bubbles:true,inputType:'insertText',data:pw}})); pwI.dispatchEvent(new Event('change',{{bubbles:true}})); [...document.querySelectorAll('button')].find(b=>(b.innerText||'').trim()==='继续')?.click();}} await new Promise(r=>setTimeout(r,12000)); return {{url:location.href,title:document.title,text:document.body.innerText.slice(0,600)}}; """ out = cdp(js, timeout=40) emit(phase='recover_state', state=out[-800:]) return 'auth.openai.com/add-phone' in current_state() def set_country_gate(country): meta = META[country] js = f""" const iso={json.dumps(meta['iso'])}, visible={json.dumps(meta['visible'])}; const sel=document.querySelector('select'); if(!sel) return {{ok:false, reason:'no_select', url:location.href, text:document.body.innerText.slice(0,300)}}; 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,900)); const tel=document.querySelector('input[type=tel]'); if(tel){{const setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; setter.call(tel,''); tel.dispatchEvent(new InputEvent('input',{{bubbles:true,inputType:'deleteContentBackward'}})); tel.dispatchEvent(new Event('change',{{bubbles:true}}));}} const country=[...document.querySelectorAll('button')].map(b=>b.innerText).find(t=>/\\(\\+\\d+\\)/.test(t)); const selectValue=document.querySelector('select')?.value; return {{ok:selectValue===iso && country===visible, country, selectValue, expected:visible, iso, url:location.href}}; """ out = cdp(js, timeout=25) return out def submit_number(country, phone, aid): meta = META[country] digits = re.sub(r'\D+', '', str(phone)) local = digits[len(meta['prefix']):] if digits.startswith(meta['prefix']) else digits js = f""" const iso={json.dumps(meta['iso'])}, visible={json.dumps(meta['visible'])}, local={json.dumps(local)}; function visibleCountry(){{return [...document.querySelectorAll('button')].map(b=>b.innerText).find(t=>/\\(\\+\\d+\\)/.test(t));}} let country=visibleCountry(), selectValue=document.querySelector('select')?.value; if(selectValue!==iso || country!==visible) return {{clicked:false, reason:'pre_fill_country_gate_failed', country, selectValue, expected:visible, iso}}; const tel=document.querySelector('input[type=tel]'); if(!tel) return {{clicked:false, reason:'no_tel', country, selectValue}}; const setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; tel.focus(); setter.call(tel,''); tel.dispatchEvent(new InputEvent('input',{{bubbles:true,inputType:'deleteContentBackward'}})); setter.call(tel,local); tel.dispatchEvent(new InputEvent('input',{{bubbles:true,inputType:'insertText',data:local}})); tel.dispatchEvent(new Event('change',{{bubbles:true}})); await new Promise(r=>setTimeout(r,800)); country=visibleCountry(); selectValue=document.querySelector('select')?.value; if(selectValue!==iso || country!==visible) return {{clicked:false, reason:'post_fill_country_gate_failed', country, selectValue, expected:visible, iso, tel:document.querySelector('input[type=tel]')?.value}}; [...document.querySelectorAll('button')].find(b=>(b.innerText||'').trim()==='继续')?.click(); await new Promise(r=>setTimeout(r,10000)); return {{clicked:true,url:location.href,title:document.title,text:document.body.innerText.slice(0,1200),country:visibleCountry(),selectValue:document.querySelector('select')?.value,tel:document.querySelector('input[type=tel]')?.value}}; """ return cdp(js, timeout=35) MAX_TOTAL_ATTEMPTS = 10 attempt_total = 0 round_index = 0 emit(phase='phone_strict_start', countries=ORDER, limit=LIMIT, max_total_attempts=MAX_TOTAL_ATTEMPTS, rotation='round_robin') while attempt_total < MAX_TOTAL_ATTEMPTS: progressed = False round_index += 1 for country in ORDER: if attempt_total >= MAX_TOTAL_ATTEMPTS: break if country not in META: emit(phase='skip_unknown_country', country=country) continue local_attempt = round_index if local_attempt > LIMIT: continue progressed = True attempt_total += 1 attempt = attempt_total emit(phase='attempt_start', country=country, attempt=attempt, local_attempt=local_attempt, max_total=MAX_TOTAL_ATTEMPTS) if not restore_add_phone(): emit(phase='cannot_restore_add_phone', country=country, attempt=attempt, local_attempt=local_attempt, state=current_state()[-800:]) sys.exit(20) gate = set_country_gate(country) emit(phase='country_gate_before_buy', country=country, attempt=attempt, state=gate[-700:]) if '"ok": true' not in gate: continue try: act = req_json(f'https://5sim.net/v1/user/buy/activation/{country}/{OPERATOR}/{PRODUCT}') except Exception as e: emit(phase='buy_failed', country=country, attempt=attempt, error=str(e)) continue aid = act.get('id'); phone = str(act.get('phone')) pathlib.Path('/tmp/fivesim_activation.json').write_text(json.dumps(act, ensure_ascii=False, indent=2)) masked = re.sub(r'(\+?\d{4})\d+(\d{3})', r'\1***\2', phone) emit(phase='buy', country=country, attempt=attempt, activation_id=aid, masked=masked, status=act.get('status')) # Re-run country gate after buy; cancel instead of submit if wrong. gate2 = current_state() if META[country]['visible'] not in gate2 or ('"selectValue": "' + META[country]['iso'] + '"') not in gate2: emit(phase='cancel_before_submit_country_mismatch', country=country, attempt=attempt, activation_id=aid, state=gate2[-600:], result=cancel(aid).get('status')) continue out = submit_number(country, phone, aid) emit(phase='submit_result', country=country, attempt=attempt, activation_id=aid, state=out[-1000:]) if 'clicked": false' in out or 'country_gate_failed' in out: emit(phase='cancel_submit_gate_failed', country=country, attempt=attempt, activation_id=aid, result=cancel(aid).get('status')) continue if any(x in out for x in ['无法向此电话号码发送', '无法向该号码发送', '电话号码无效']): emit(phase='direct_reject_cancel', country=country, attempt=attempt, activation_id=aid, result=cancel(aid).get('status')) continue got = None for r in range(1, 7): time.sleep(10) st = check(aid); sms = st.get('sms') or [] emit(phase='sms_poll', country=country, attempt=attempt, activation_id=aid, round=r, status=st.get('status'), sms_count=len(sms)) if sms: got = sms[0].get('code') or sms[0].get('text') break if not got: # Try resend once if page still allows it. Probe channel first; if it is WhatsApp, stop/cancel SMS instead of clicking. resend = cdp(""" const text=(document.title+' '+document.body.innerText).replace(/\s+/g,' ').trim(); if(/this page isn['’]?t working|currently unable to handle this request|http error 500|500 internal server error/i.test(text)) return {clicked:false, reason:'resend_server_error', url:location.href, text:text.slice(0,600)}; const btn=[...document.querySelectorAll('button,input[type=submit],input[type=button]')].find(b=>{const t=[b.value,b.getAttribute?.('aria-label'),b.getAttribute?.('title'),b.innerText,b.textContent].filter(Boolean).join(' '); return /resend|重新发送|再次发送|whats\s*app/i.test(t);}); const channelText=btn?[btn.value,btn.getAttribute?.('aria-label'),btn.getAttribute?.('title'),btn.innerText,btn.textContent].filter(Boolean).join(' ').replace(/\s+/g,' ').trim():''; const channel=/whats\s*app/i.test(channelText)?'whatsapp':(/sms|text message|短信/i.test(channelText)?'sms':'unknown'); if(channel==='whatsapp') return {clicked:false, channel, channelText, url:location.href}; btn?.click(); await new Promise(r=>setTimeout(r,2000)); const after=(document.title+' '+document.body.innerText).replace(/\s+/g,' ').trim(); return {clicked:!!btn,channel,channelText,url:location.href,text:after.slice(0,600),serverError:/this page isn['’]?t working|currently unable to handle this request|http error 500|500 internal server error/i.test(after)}; """, match='auth.openai.com', timeout=20) emit(phase='resend', country=country, attempt=attempt, activation_id=aid, state=resend[-700:]) if 'whatsapp' in resend.lower(): emit(phase='whatsapp_resend_detected_cancel', country=country, attempt=attempt, activation_id=aid, result=cancel(aid).get('status')) continue if 'resend_server_error' in resend or 'serverError' in resend: emit(phase='resend_server_error_cancel', country=country, attempt=attempt, activation_id=aid, result=cancel(aid).get('status')) continue for r in range(1, 7): time.sleep(10) st = check(aid); sms = st.get('sms') or [] emit(phase='sms_poll_after_resend', country=country, attempt=attempt, activation_id=aid, round=r, status=st.get('status'), sms_count=len(sms)) if sms: got = sms[0].get('code') or sms[0].get('text') break if not got: emit(phase='no_sms_cancel', country=country, attempt=attempt, activation_id=aid, result=cancel(aid).get('status')) continue emit(phase='sms_received', country=country, attempt=attempt, activation_id=aid) code = ''.join(re.findall(r'\d', str(got)))[:6] fill = cdp(f""" const code={json.dumps(code)}; const input=document.querySelector('input[autocomplete=one-time-code], input[name=code], input'); 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}})); [...document.querySelectorAll('button')].find(b=>(b.innerText||'').trim()==='继续')?.click(); await new Promise(r=>setTimeout(r,12000)); return {{url:location.href,title:document.title,text:document.body.innerText.slice(0,1000)}}; """, match='auth.openai.com', timeout=35) emit(phase='code_submit', state=fill[-1000:]) finish(aid) sys.exit(0) if not progressed: break emit(phase='phone_failed_all', total_attempts=attempt_total, order=ORDER, rotation='round_robin') sys.exit(3)