Files
codex-oauth-plus-onboarding…/scripts/phone_verify_free_country_2each_total20_part2.py

322 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'}
# Free-country mode: ignore env country order and choose a broad set of countries supported by OpenAI's selector.
ORDER = [
'portugal','romania','croatia','serbia','bulgaria','ukraine','georgia','kazakhstan',
'pakistan','bangladesh','srilanka','cambodia','laos',
# fallback previously productive countries only if later countries lack stock
'vietnam','indonesia','argentina','netherlands','italy','poland','spain','england','germany'
]
LIMIT = 2
PRODUCT = get('FIVE_SIM_PRODUCT', 'openai') or 'openai'
OPERATOR = get('FIVE_SIM_OPERATOR', 'any') or 'any'
META = {
'philippines': {'iso': 'PH', 'visible': '菲律宾 (+63)', 'prefix': '63'},
'malaysia': {'iso': 'MY', 'visible': '马来西亚 (+60)', 'prefix': '60'},
'thailand': {'iso': 'TH', 'visible': '泰国 (+66)', 'prefix': '66'},
'vietnam': {'iso': 'VN', 'visible': '越南 (+84)', 'prefix': '84'},
'indonesia': {'iso': 'ID', 'visible': '印度尼西亚 (+62)', 'prefix': '62'},
'india': {'iso': 'IN', 'visible': '印度 (+91)', 'prefix': '91'},
'brazil': {'iso': 'BR', 'visible': '巴西 (+55)', 'prefix': '55'},
'mexico': {'iso': 'MX', 'visible': '墨西哥 (+52)', 'prefix': '52'},
'colombia': {'iso': 'CO', 'visible': '哥伦比亚 (+57)', 'prefix': '57'},
'argentina': {'iso': 'AR', 'visible': '阿根廷 (+54)', 'prefix': '54'},
'peru': {'iso': 'PE', 'visible': '秘鲁 (+51)', 'prefix': '51'},
'chile': {'iso': 'CL', 'visible': '智利 (+56)', 'prefix': '56'},
'turkey': {'iso': 'TR', 'visible': '土耳其 (+90)', 'prefix': '90'},
'southafrica': {'iso': 'ZA', 'visible': '南非 (+27)', 'prefix': '27'},
'kenya': {'iso': 'KE', 'visible': '肯尼亚 (+254)', 'prefix': '254'},
'nigeria': {'iso': 'NG', 'visible': '尼日利亚 (+234)', 'prefix': '234'},
'ghana': {'iso': 'GH', 'visible': '加纳 (+233)', 'prefix': '233'},
'egypt': {'iso': 'EG', 'visible': '埃及 (+20)', 'prefix': '20'},
'netherlands': {'iso': 'NL', 'visible': '荷兰 (+31)', 'prefix': '31'},
'italy': {'iso': 'IT', 'visible': '意大利 (+39)', 'prefix': '39'},
'poland': {'iso': 'PL', 'visible': '波兰 (+48)', 'prefix': '48'},
'spain': {'iso': 'ES', 'visible': '西班牙 (+34)', 'prefix': '34'},
'england': {'iso': 'GB', 'visible': '英国 (+44)', 'prefix': '44'},
'france': {'iso': 'FR', 'visible': '法国 (+33)', 'prefix': '33'},
'germany': {'iso': 'DE', 'visible': '德国 (+49)', 'prefix': '49'},
'portugal': {'iso': 'PT', 'visible': '葡萄牙 (+351)', 'prefix': '351'},
'romania': {'iso': 'RO', 'visible': '罗马尼亚 (+40)', 'prefix': '40'},
'croatia': {'iso': 'HR', 'visible': '克罗地亚 (+385)', 'prefix': '385'},
'serbia': {'iso': 'RS', 'visible': '塞尔维亚 (+381)', 'prefix': '381'},
'bulgaria': {'iso': 'BG', 'visible': '保加利亚 (+359)', 'prefix': '359'},
'ukraine': {'iso': 'UA', 'visible': '乌克兰 (+380)', 'prefix': '380'},
'georgia': {'iso': 'GE', 'visible': '格鲁吉亚 (+995)', 'prefix': '995'},
'kazakhstan': {'iso': 'KZ', 'visible': '哈萨克斯坦 (+7)', 'prefix': '7'},
'pakistan': {'iso': 'PK', 'visible': '巴基斯坦 (+92)', 'prefix': '92'},
'bangladesh': {'iso': 'BD', 'visible': '孟加拉国 (+880)', 'prefix': '880'},
'srilanka': {'iso': 'LK', 'visible': '斯里兰卡 (+94)', 'prefix': '94'},
'cambodia': {'iso': 'KH', 'visible': '柬埔寨 (+855)', 'prefix': '855'},
'laos': {'iso': 'LA', 'visible': '老挝 (+856)', 'prefix': '856'},
}
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:
body = r.read().decode(errors='replace')
ctype = r.headers.get('content-type', '')
try:
return json.loads(body)
except Exception:
raise RuntimeError(f'non_json_response status={getattr(r, "status", "?")} content_type={ctype} body={body[:200]!r}')
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 = 20
MAX_CONSECUTIVE_FAILURES_PER_COUNTRY = 2
START_BUY_COUNT = 15 # previous run created/canceled 10 real activations; continue to 20 real buys
buy_total = START_BUY_COUNT
scheduled_attempt = 0
emit(
phase='phone_strict_start',
countries=ORDER,
limit=LIMIT,
max_total_attempts=MAX_TOTAL_ATTEMPTS,
start_buy_count=START_BUY_COUNT,
max_consecutive_failures_per_country=MAX_CONSECUTIVE_FAILURES_PER_COUNTRY,
rotation='free_country_chunks_2_total20_real_buys_part2'
)
while buy_total < MAX_TOTAL_ATTEMPTS:
progressed_in_cycle = False
for country in ORDER:
if buy_total >= MAX_TOTAL_ATTEMPTS:
break
if country not in META:
emit(phase='skip_unknown_country', country=country)
continue
local_attempt = 0
while buy_total < MAX_TOTAL_ATTEMPTS and local_attempt < LIMIT and local_attempt < MAX_CONSECUTIVE_FAILURES_PER_COUNTRY:
local_attempt += 1
scheduled_attempt += 1
attempt = buy_total + 1
emit(phase='attempt_start', country=country, attempt=attempt, scheduled_attempt=scheduled_attempt, local_attempt=local_attempt, real_buys_so_far=buy_total, 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:
emit(phase='gate_failed_no_buy_retry_not_counted', country=country, scheduled_attempt=scheduled_attempt, real_buys_so_far=buy_total)
continue
try:
act = req_json(f'https://5sim.net/v1/user/buy/activation/{country}/{OPERATOR}/{PRODUCT}')
except Exception as e:
emit(phase='buy_failed_not_counted', country=country, attempt=attempt, error=str(e))
continue
buy_total += 1
progressed_in_cycle = True
attempt = buy_total
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'), real_buys_so_far=buy_total)
# 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:
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 local_attempt >= MAX_CONSECUTIVE_FAILURES_PER_COUNTRY:
emit(phase='country_consecutive_limit_switch', country=country, consecutive_attempts=local_attempt, next_policy='next_country')
if not progressed_in_cycle:
emit(phase='no_progress_in_cycle_stop', real_buys_so_far=buy_total)
break
emit(phase='phone_failed_all', total_attempts=buy_total, order=ORDER, rotation='free_country_chunks_2_total20_real_buys_part2', max_consecutive_failures_per_country=MAX_CONSECUTIVE_FAILURES_PER_COUNTRY)
sys.exit(3)