Update 2925 OpenAI code scraping flow
This commit is contained in:
@@ -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())
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Scrape OpenAI/ChatGPT verification codes from 2925 Webmail via Chrome CDP.
|
||||
|
||||
Key 2925 quirks captured from live runs:
|
||||
- Do NOT navigate to #/mailList?type=收件箱 and trust the result; click the left nav item `收件箱`.
|
||||
- Real mail rows are `table.maillist-table tr`.
|
||||
- The generated alias local-part may be hidden in sender tooltip/bounce text such as
|
||||
`...-chickliu2031som6d8=2925.com@tm1.openai.com`, not visible row text.
|
||||
- Open the newest matching row, then extract the strict 6-digit code near code words.
|
||||
|
||||
Inputs:
|
||||
--cdp http://127.0.0.1:9227
|
||||
--account chickliu2031@2925.com
|
||||
--alias chickliu2031som6d8@2925.com
|
||||
--out /tmp/openai_2925_email_code.txt
|
||||
|
||||
The script prints JSON with code redacted and writes the raw code only to --out.
|
||||
"""
|
||||
import argparse, json, pathlib, re, sys, time, urllib.request, websocket, itertools
|
||||
|
||||
|
||||
def tabs(cdp):
|
||||
return json.loads(urllib.request.urlopen(cdp.rstrip('/') + '/json', timeout=10).read().decode())
|
||||
|
||||
|
||||
def connect(cdp):
|
||||
pages = [t for t in tabs(cdp) if t.get('type') == 'page']
|
||||
sel = next((t for t in pages if '2925.com' in (t.get('url') or '')), None) or pages[0]
|
||||
ws = websocket.create_connection(sel['webSocketDebuggerUrl'], timeout=20)
|
||||
counter = itertools.count(1)
|
||||
|
||||
def call(method, params=None, timeout=45):
|
||||
i = next(counter)
|
||||
ws.send(json.dumps({'id': i, 'method': method, 'params': params 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(method)
|
||||
|
||||
call('Page.enable')
|
||||
call('Runtime.enable')
|
||||
return call
|
||||
|
||||
|
||||
def ev(call, js, awaitp=False, timeout=45):
|
||||
return call('Runtime.evaluate', {
|
||||
'expression': js,
|
||||
'awaitPromise': awaitp,
|
||||
'returnByValue': True,
|
||||
}, timeout).get('result', {}).get('value')
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--cdp', default='http://127.0.0.1:9227')
|
||||
ap.add_argument('--account', required=True)
|
||||
ap.add_argument('--alias', required=True)
|
||||
ap.add_argument('--out', required=True)
|
||||
args = ap.parse_args()
|
||||
local = args.alias.split('@', 1)[0].lower()
|
||||
|
||||
call = connect(args.cdp)
|
||||
js = r'''(async(local,account)=>{
|
||||
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
const click=e=>{if(!e)return false; e.scrollIntoView({block:'center'}); e.dispatchEvent(new MouseEvent('mousedown',{bubbles:true})); e.dispatchEvent(new MouseEvent('mouseup',{bubbles:true})); e.click(); return true;};
|
||||
const all=[...document.querySelectorAll('*')];
|
||||
const inbox=all.find(e=>(e.innerText||e.textContent||'').trim()==='收件箱');
|
||||
const clickedInbox=click(inbox); await sleep(1800);
|
||||
const refresh=[...document.querySelectorAll('*')].filter(e=>(e.innerText||e.textContent||'').trim()==='刷新').pop();
|
||||
const clickedRefresh=click(refresh); await sleep(3000);
|
||||
const pageText=document.body?.innerText||'';
|
||||
const accountOk=pageText.includes(account);
|
||||
const rows=[...document.querySelectorAll('table.maillist-table tr')].filter(r=>/OpenAI|ChatGPT|验证码|临时验证码|verification|code/i.test(r.innerText||''));
|
||||
const rowSummaries=[];
|
||||
let chosen=null, chosenIndex=-1;
|
||||
for(let i=0;i<rows.length;i++){
|
||||
const r=rows[i];
|
||||
const hidden=[...r.querySelectorAll('*')].map(e=>[e.innerText,e.textContent,e.title,e.getAttribute('aria-label')].filter(Boolean).join(' ')).join(' ');
|
||||
const combined=(r.innerText+' '+hidden).toLowerCase();
|
||||
rowSummaries.push({i,text:(r.innerText||'').replace(/\s+/g,' ').trim().slice(0,180),hiddenHasLocal:combined.includes(local)});
|
||||
if(!chosen && combined.includes(local)){ chosen=r; chosenIndex=i; }
|
||||
}
|
||||
if(!chosen && rows[0]){ chosen=rows[0]; chosenIndex=0; }
|
||||
if(chosen){ click(chosen); await sleep(3500); }
|
||||
let body=document.body?.innerText||'';
|
||||
for(const f of [...document.querySelectorAll('iframe')]){ try{ body+='\n'+(f.contentDocument?.body?.innerText||''); }catch(e){} }
|
||||
const candidates=[];
|
||||
for(const m of body.matchAll(/(?<!\d)(\d{6})(?!\d)/g)){
|
||||
const ctx=body.slice(Math.max(0,m.index-120), Math.min(body.length,m.index+126));
|
||||
const score=(/验证码|verification|code|临时验证码/i.test(ctx)?10:0)+(/OpenAI|ChatGPT/i.test(ctx)?3:0)+(body.toLowerCase().includes(local)?2:0);
|
||||
candidates.push({code:m[1],score,ctx:ctx.replace(m[1],'[CODE]')});
|
||||
}
|
||||
candidates.sort((a,b)=>b.score-a.score);
|
||||
return {href:location.href,clickedInbox,clickedRefresh,accountOk,rowCount:rows.length,chosenIndex,rowSummaries:rowSummaries.slice(0,8),hasLocal:body.toLowerCase().includes(local),candidates};
|
||||
})(LOCAL,ACCOUNT)'''.replace('LOCAL', json.dumps(local)).replace('ACCOUNT', json.dumps(args.account))
|
||||
val = ev(call, js, True, 90) or {}
|
||||
candidates = val.get('candidates') or []
|
||||
code = candidates[0]['code'] if candidates else ''
|
||||
if code:
|
||||
pathlib.Path(args.out).write_text(code)
|
||||
redacted = dict(val)
|
||||
redacted['candidates'] = [{k: v for k, v in c.items() if k != 'code'} for c in candidates[:5]]
|
||||
redacted['codeSaved'] = bool(code)
|
||||
print(json.dumps(redacted, ensure_ascii=False))
|
||||
return 0 if code else 2
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Poll 2925 Webmail for an OpenAI/ChatGPT code matching the current generated alias.
|
||||
|
||||
Why this exists:
|
||||
- 2925 OpenAI mail may land in 垃圾箱 instead of 收件箱.
|
||||
- 垃圾箱/已删除 can contain stale OpenAI codes for older aliases.
|
||||
- Never submit a visible 6-digit code unless the surrounding message matches the current alias/local-part.
|
||||
|
||||
Outputs compact JSONL and saves the code to --out-file without printing it.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
import time
|
||||
import urllib.request
|
||||
import websocket
|
||||
import itertools
|
||||
|
||||
FOLDERS = {
|
||||
"inbox": "https://2925.com/#/mailList?type=%E6%94%B6%E4%BB%B6%E7%AE%B1",
|
||||
"junk": "https://2925.com/#/mailList?type=%E5%9E%83%E5%9C%BE%E7%AE%B1",
|
||||
"deleted": "https://2925.com/#/mailList?type=%E5%B7%B2%E5%88%A0%E9%99%A4",
|
||||
}
|
||||
|
||||
|
||||
def log(obj):
|
||||
print(json.dumps({**obj, "ts": int(time.time())}, ensure_ascii=False), flush=True)
|
||||
|
||||
|
||||
def cdp_json(base, path):
|
||||
return json.loads(urllib.request.urlopen(base + path, timeout=10).read().decode())
|
||||
|
||||
|
||||
def connect_tab(base, url_substr="2925.com"):
|
||||
tabs = cdp_json(base, "/json")
|
||||
tab = next((t for t in tabs if url_substr in (t.get("url") or "")), tabs[0])
|
||||
ws = websocket.create_connection(tab["webSocketDebuggerUrl"], timeout=20)
|
||||
counter = itertools.count(1)
|
||||
|
||||
def call(method, params=None, timeout=25):
|
||||
msg_id = next(counter)
|
||||
ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
msg = json.loads(ws.recv())
|
||||
if msg.get("id") == msg_id:
|
||||
if "error" in msg:
|
||||
raise RuntimeError(msg["error"])
|
||||
return msg.get("result", {})
|
||||
raise TimeoutError(method)
|
||||
|
||||
call("Runtime.enable")
|
||||
call("Page.enable")
|
||||
|
||||
def eval_js(expr, await_promise=False, timeout=25):
|
||||
result = call(
|
||||
"Runtime.evaluate",
|
||||
{"expression": expr, "awaitPromise": await_promise, "returnByValue": True},
|
||||
timeout,
|
||||
)
|
||||
return result.get("result", {}).get("value")
|
||||
|
||||
return call, eval_js
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--cdp-port", type=int, required=True)
|
||||
ap.add_argument("--account", required=True, help="Selected 2925 base account, e.g. user@2925.com")
|
||||
ap.add_argument("--alias", required=True, help="Current generated alias used on OpenAI")
|
||||
ap.add_argument("--out-file", default="/tmp/openai_2925_email_code.txt")
|
||||
ap.add_argument("--attempts", type=int, default=15)
|
||||
ap.add_argument("--interval", type=float, default=15.0)
|
||||
ap.add_argument("--folders", default="inbox,junk", help="comma list: inbox,junk,deleted")
|
||||
ap.add_argument("--reject-code", action="append", default=[])
|
||||
args = ap.parse_args()
|
||||
|
||||
base = f"http://127.0.0.1:{args.cdp_port}"
|
||||
account = args.account.strip().lower()
|
||||
alias = args.alias.strip().lower()
|
||||
alias_local = alias.split("@", 1)[0]
|
||||
rejected = {str(x).strip() for x in args.reject_code if str(x).strip()}
|
||||
folders = [x.strip() for x in args.folders.split(",") if x.strip()]
|
||||
|
||||
call, ev = connect_tab(base)
|
||||
|
||||
for attempt in range(1, args.attempts + 1):
|
||||
for folder in folders:
|
||||
url = FOLDERS.get(folder)
|
||||
if not url:
|
||||
continue
|
||||
call("Page.navigate", {"url": url})
|
||||
time.sleep(4)
|
||||
js = f"""
|
||||
(() => {{
|
||||
try {{
|
||||
const account = {json.dumps(account)};
|
||||
const aliasLocal = {json.dumps(alias_local)};
|
||||
const txt = document.body?.innerText || '';
|
||||
const emails = [...new Set([...txt.matchAll(/[A-Za-z0-9._%+-]+@2925\\.com/g)].map(m => m[0].toLowerCase()))];
|
||||
const lines = txt.split(/\n+/).map(s => s.trim()).filter(Boolean);
|
||||
const snippets = [];
|
||||
for (let i = 0; i < lines.length; i++) {{
|
||||
const line = lines[i].toLowerCase();
|
||||
if (line.includes(aliasLocal) || /openai|chatgpt|tm1\\.openai|验证码|verification/.test(line)) {{
|
||||
const s = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 9)).join('\n');
|
||||
if (s.toLowerCase().includes(aliasLocal) && /OpenAI|ChatGPT|tm1\\.openai|验证码|verification/i.test(s)) snippets.push(s);
|
||||
}}
|
||||
}}
|
||||
const codes = [...new Set(snippets.flatMap(s => [...s.matchAll(/(?<!\\d)(\\d{{6}})(?!\\d)/g)].map(m => m[1])))];
|
||||
return {{ href: location.href, emails, hasAccount: emails.includes(account), hasAliasLocal: txt.toLowerCase().includes(aliasLocal), snippets: snippets.slice(0, 5), codes }};
|
||||
}} catch (e) {{
|
||||
return {{ error: String(e), emails: [], snippets: [], codes: [] }};
|
||||
}}
|
||||
}})()
|
||||
"""
|
||||
data = ev(js) or {"error": "null_eval", "emails": [], "snippets": [], "codes": []}
|
||||
if account not in data.get("emails", []):
|
||||
log({"phase": "account_gate_failed", "folder": folder, "expected": account, "emails": data.get("emails", [])})
|
||||
raise SystemExit(4)
|
||||
fresh = [c for c in data.get("codes", []) if c not in rejected]
|
||||
log({
|
||||
"phase": "poll_alias_code",
|
||||
"attempt": attempt,
|
||||
"folder": folder,
|
||||
"hasAliasLocal": bool(data.get("hasAliasLocal")),
|
||||
"snippetCount": len(data.get("snippets", [])),
|
||||
"freshCount": len(fresh),
|
||||
})
|
||||
if fresh:
|
||||
pathlib.Path(args.out_file).write_text(fresh[0])
|
||||
log({"phase": "code_saved", "folder": folder, "attempt": attempt, "outFile": args.out_file})
|
||||
return
|
||||
time.sleep(args.interval)
|
||||
|
||||
log({"phase": "timeout_no_current_alias_code", "alias": alias})
|
||||
raise SystemExit(2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
"""2925 account-pool helper. Never prints passwords."""
|
||||
import argparse, json, os, random, re, stat, string, time
|
||||
from pathlib import Path
|
||||
|
||||
ACCOUNTS_FILE = Path(os.environ.get('MAIL2925_ACCOUNTS_FILE', str(Path.home()/'.hermes/secrets/mail2925_accounts.txt'))).expanduser()
|
||||
STATE_FILE = Path(os.environ.get('MAIL2925_STATE_FILE', str(Path.home()/'.hermes/state/mail2925_accounts_state.json'))).expanduser()
|
||||
COOLDOWN_SECONDS = int(os.environ.get('MAIL2925_LIMIT_COOLDOWN_SECONDS', str(24*60*60)))
|
||||
SUFFIX_LEN = int(os.environ.get('MAIL2925_ALIAS_SUFFIX_LEN', '6'))
|
||||
|
||||
EMAIL_RE = re.compile(r'^[^@\s]+@2925\.com$', re.I)
|
||||
|
||||
|
||||
def load_accounts(path=ACCOUNTS_FILE):
|
||||
text = path.read_text()
|
||||
out=[]; seen=set()
|
||||
for raw in text.splitlines():
|
||||
line=raw.strip()
|
||||
if not line or line.lower() == '邮箱----密码':
|
||||
continue
|
||||
if '----' not in line:
|
||||
raise ValueError(f'bad account line: {line[:20]}...')
|
||||
email,pwd=[x.strip() for x in line.split('----',1)]
|
||||
email=email.lower()
|
||||
if not EMAIL_RE.match(email):
|
||||
raise ValueError(f'bad 2925 email: {email}')
|
||||
if not pwd:
|
||||
raise ValueError(f'missing password: {email}')
|
||||
if email in seen:
|
||||
continue
|
||||
seen.add(email); out.append({'id': email, 'email': email, 'password': pwd})
|
||||
return out
|
||||
|
||||
|
||||
def load_state():
|
||||
if not STATE_FILE.exists(): return {'accounts': {}}
|
||||
try: return json.loads(STATE_FILE.read_text())
|
||||
except Exception: return {'accounts': {}}
|
||||
|
||||
|
||||
def save_state(state):
|
||||
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2, sort_keys=True))
|
||||
os.chmod(STATE_FILE, 0o600)
|
||||
|
||||
|
||||
def public_account(a, state):
|
||||
s=state.get('accounts',{}).get(a['email'],{})
|
||||
now=time.time(); disabled=float(s.get('disabledUntil') or 0)
|
||||
status='cooldown' if disabled>now else ('disabled' if s.get('enabled') is False else 'ready')
|
||||
return {'email': a['email'], 'status': status, 'lastUsedAt': int(s.get('lastUsedAt') or 0), 'lastLoginAt': int(s.get('lastLoginAt') or 0), 'disabledUntil': int(disabled), 'lastError': s.get('lastError','')}
|
||||
|
||||
|
||||
def available_accounts(accounts, state):
|
||||
now=time.time(); out=[]
|
||||
for a in accounts:
|
||||
s=state.get('accounts',{}).get(a['email'],{})
|
||||
if s.get('enabled') is False: continue
|
||||
if float(s.get('disabledUntil') or 0)>now: continue
|
||||
out.append(a)
|
||||
return out
|
||||
|
||||
|
||||
def pick_account(preferred=None, exclude=None):
|
||||
accounts=load_accounts(); state=load_state(); exclude=set(exclude or [])
|
||||
pool=[a for a in available_accounts(accounts,state) if a['email'] not in exclude]
|
||||
if preferred:
|
||||
preferred=preferred.lower().strip()
|
||||
for a in pool:
|
||||
if a['email']==preferred: return a,state
|
||||
if not pool: raise RuntimeError('no available 2925 account')
|
||||
def key(a):
|
||||
s=state.get('accounts',{}).get(a['email'],{})
|
||||
return (float(s.get('lastUsedAt') or 0), a['email'])
|
||||
return sorted(pool,key=key)[0], state
|
||||
|
||||
|
||||
def generate_alias_for(account):
|
||||
local=account['email'].split('@',1)[0].lower()
|
||||
suffix=''.join(random.choice(string.ascii_lowercase+string.digits) for _ in range(SUFFIX_LEN))
|
||||
alias=f'{local}{suffix}@2925.com'
|
||||
assert alias.startswith(local) and alias.endswith('@2925.com')
|
||||
return alias
|
||||
|
||||
|
||||
def mark(email, **patch):
|
||||
state=load_state(); d=state.setdefault('accounts',{}).setdefault(email.lower(),{})
|
||||
d.update(patch); save_state(state)
|
||||
|
||||
|
||||
def cmd_list(args):
|
||||
accounts=load_accounts(); state=load_state()
|
||||
print(json.dumps({'count':len(accounts),'accounts':[public_account(a,state) for a in accounts]}, ensure_ascii=False))
|
||||
|
||||
|
||||
def cmd_pick(args):
|
||||
a,state=pick_account(args.preferred, args.exclude or [])
|
||||
alias=generate_alias_for(a)
|
||||
if not args.dry_run:
|
||||
mark(a['email'], lastUsedAt=int(time.time()), lastError='')
|
||||
print(json.dumps({'email':a['email'],'alias':alias,'baseLocal':a['email'].split('@')[0],'aliasMatchesAccount':alias.startswith(a['email'].split('@')[0]),'dryRun':bool(args.dry_run)}, ensure_ascii=False))
|
||||
|
||||
|
||||
def cmd_limit(args):
|
||||
email=args.email.lower().strip(); until=int(time.time()+COOLDOWN_SECONDS)
|
||||
mark(email, lastLimitAt=int(time.time()), disabledUntil=until, lastError=args.reason or '子邮箱已达上限邮箱')
|
||||
print(json.dumps({'email':email,'disabledUntil':until,'cooldownSeconds':COOLDOWN_SECONDS}, ensure_ascii=False))
|
||||
|
||||
|
||||
def cmd_mark_login(args):
|
||||
mark(args.email.lower().strip(), lastLoginAt=int(time.time()), lastError='')
|
||||
print(json.dumps({'email':args.email.lower().strip(),'ok':True}, ensure_ascii=False))
|
||||
|
||||
|
||||
def cmd_selftest(args):
|
||||
accounts=load_accounts(); state=load_state()
|
||||
failures=[]
|
||||
for a in accounts:
|
||||
for _ in range(5):
|
||||
alias=generate_alias_for(a); local=a['email'].split('@')[0]
|
||||
if not (alias.startswith(local) and alias.endswith('@2925.com') and len(alias.split('@')[0])==len(local)+SUFFIX_LEN):
|
||||
failures.append({'email':a['email'],'alias':alias})
|
||||
st=ACCOUNTS_FILE.stat()
|
||||
ok_mode=(stat.S_IMODE(st.st_mode)==0o600)
|
||||
print(json.dumps({'accounts':len(accounts),'aliasFailures':failures,'secretMode':oct(stat.S_IMODE(st.st_mode)),'ok':not failures and ok_mode}, ensure_ascii=False))
|
||||
raise SystemExit(0 if not failures and ok_mode else 1)
|
||||
|
||||
|
||||
def main():
|
||||
p=argparse.ArgumentParser()
|
||||
sub=p.add_subparsers(dest='cmd', required=True)
|
||||
sub.add_parser('list').set_defaults(func=cmd_list)
|
||||
q=sub.add_parser('pick'); q.add_argument('--preferred'); q.add_argument('--exclude', action='append'); q.add_argument('--dry-run', action='store_true'); q.set_defaults(func=cmd_pick)
|
||||
l=sub.add_parser('mark-limit'); l.add_argument('email'); l.add_argument('--reason', default=''); l.set_defaults(func=cmd_limit)
|
||||
m=sub.add_parser('mark-login'); m.add_argument('email'); m.set_defaults(func=cmd_mark_login)
|
||||
sub.add_parser('selftest').set_defaults(func=cmd_selftest)
|
||||
args=p.parse_args(); args.func(args)
|
||||
if __name__=='__main__': main()
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Strict 2925/OpenAI email resend cadence runner.
|
||||
|
||||
Cadence encoded from BOSS correction:
|
||||
for each resend slot:
|
||||
refresh 2925 Inbox + Junk for N rounds, sleeping between rounds
|
||||
only then click OpenAI resend once
|
||||
|
||||
This script is intentionally env-driven and JSONL-only. It does not buy SMS.
|
||||
Required environment:
|
||||
RUN_JSON=/tmp/codex_2925_luban_uk_run.json # contains {"email":..., "alias":...}
|
||||
Optional environment:
|
||||
MAIL_CDP=http://127.0.0.1:9227
|
||||
OPENAI_CDP=http://127.0.0.1:9228
|
||||
START_RESEND_DONE=0
|
||||
TARGET_RESENDS=10
|
||||
REFRESH_ROUNDS=3
|
||||
SLEEP_BETWEEN_REFRESH_ROUNDS=15
|
||||
CODE_PATH=/tmp/openai_2925_email_code.txt
|
||||
"""
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
import websocket
|
||||
|
||||
RUN_JSON = pathlib.Path(os.environ.get("RUN_JSON", "/tmp/codex_2925_luban_uk_run.json"))
|
||||
RUN = json.loads(RUN_JSON.read_text())
|
||||
ACCOUNT = RUN["email"]
|
||||
ALIAS = RUN["alias"]
|
||||
LOCAL = ALIAS.split("@")[0].lower()
|
||||
MAIL_CDP = os.environ.get("MAIL_CDP", "http://127.0.0.1:9227")
|
||||
OPENAI_CDP = os.environ.get("OPENAI_CDP", "http://127.0.0.1:9228")
|
||||
START_RESEND_DONE = int(os.environ.get("START_RESEND_DONE", "0"))
|
||||
TARGET_RESENDS = int(os.environ.get("TARGET_RESENDS", "10"))
|
||||
REFRESH_ROUNDS = int(os.environ.get("REFRESH_ROUNDS", "3"))
|
||||
SLEEP_BETWEEN_REFRESH_ROUNDS = int(os.environ.get("SLEEP_BETWEEN_REFRESH_ROUNDS", "15"))
|
||||
CODE_PATH = pathlib.Path(os.environ.get("CODE_PATH", "/tmp/openai_2925_email_code.txt"))
|
||||
FOLDERS = [
|
||||
("inbox", "https://2925.com/#/mailList?type=%E6%94%B6%E4%BB%B6%E7%AE%B1"),
|
||||
("junk", "https://2925.com/#/mailList?type=%E5%9E%83%E5%9C%BE%E7%AE%B1"),
|
||||
]
|
||||
|
||||
|
||||
def log(obj):
|
||||
print(json.dumps({**obj, "ts": int(time.time())}, ensure_ascii=False), flush=True)
|
||||
|
||||
|
||||
def tabs(cdp):
|
||||
return json.loads(urllib.request.urlopen(cdp + "/json", timeout=10).read().decode())
|
||||
|
||||
|
||||
def connect(cdp, pred):
|
||||
pages = [t for t in tabs(cdp) if t.get("type") == "page"]
|
||||
selected = next((t for t in pages if pred(t)), None) or (pages[0] if pages else None)
|
||||
if not selected:
|
||||
raise RuntimeError(f"no_page_target:{cdp}")
|
||||
ws = websocket.create_connection(selected["webSocketDebuggerUrl"], timeout=20)
|
||||
counter = itertools.count(1)
|
||||
|
||||
def call(method, params=None, timeout=45):
|
||||
msg_id = next(counter)
|
||||
ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))
|
||||
end = time.time() + timeout
|
||||
while time.time() < end:
|
||||
msg = json.loads(ws.recv())
|
||||
if msg.get("id") == msg_id:
|
||||
if "error" in msg:
|
||||
raise RuntimeError(msg["error"])
|
||||
return msg.get("result", {})
|
||||
raise TimeoutError(method)
|
||||
|
||||
call("Page.enable")
|
||||
call("Runtime.enable")
|
||||
return call
|
||||
|
||||
|
||||
def evaluate(call, js, await_promise=False, timeout=45):
|
||||
result = call(
|
||||
"Runtime.evaluate",
|
||||
{"expression": js, "awaitPromise": await_promise, "returnByValue": True},
|
||||
timeout,
|
||||
)
|
||||
return result.get("result", {}).get("value")
|
||||
|
||||
|
||||
def refresh_once(folder, url, resend_index, refresh_round):
|
||||
call = connect(MAIL_CDP, lambda t: "2925.com" in (t.get("url") or ""))
|
||||
call("Page.navigate", {"url": url})
|
||||
time.sleep(2)
|
||||
js = r'''(async(local,account)=>{
|
||||
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
const click=e=>{if(!e)return false; e.dispatchEvent(new MouseEvent('mousedown',{bubbles:true})); e.dispatchEvent(new MouseEvent('mouseup',{bubbles:true})); e.click(); return true;};
|
||||
const refreshButtons=[...document.querySelectorAll('button,.el-button,a,span,div')].filter(e=>(e.innerText||e.textContent||'').trim()==='刷新');
|
||||
const clicked=click(refreshButtons[refreshButtons.length-1]||refreshButtons[0]);
|
||||
await sleep(2500);
|
||||
const txt=document.body?.innerText||'';
|
||||
const low=txt.toLowerCase();
|
||||
const emails=[...new Set([...txt.matchAll(/[A-Za-z0-9._%+-]+@2925\.com/g)].map(m=>m[0].toLowerCase()))];
|
||||
const lines=txt.split(/\n+/).map(s=>s.trim()).filter(Boolean);
|
||||
const snippets=[];
|
||||
for(let i=0;i<lines.length;i++){
|
||||
const around=lines.slice(Math.max(0,i-8),Math.min(lines.length,i+12)).join('\n');
|
||||
if(around.toLowerCase().includes(local)&&/OpenAI|ChatGPT|验证码|verification|code/i.test(around)) snippets.push(around);
|
||||
}
|
||||
const codes=[...new Set(snippets.flatMap(s=>[...s.matchAll(/(?<!\d)(\d{6})(?!\d)/g)].map(m=>m[1])))];
|
||||
return {clicked,emails,accountOk:emails.includes(account.toLowerCase()),hasLocal:low.includes(local),hasOpenAI:/OpenAI|ChatGPT|验证码|verification|code/i.test(txt),codes};
|
||||
})(LOCAL,ACCOUNT)'''.replace("LOCAL", json.dumps(LOCAL)).replace("ACCOUNT", json.dumps(ACCOUNT))
|
||||
val = evaluate(call, js, True) or {}
|
||||
log({
|
||||
"phase": "mail_refresh",
|
||||
"resend_index": resend_index,
|
||||
"refresh_round": refresh_round,
|
||||
"folder": folder,
|
||||
"clicked": val.get("clicked"),
|
||||
"accountOk": val.get("accountOk"),
|
||||
"hasLocal": val.get("hasLocal"),
|
||||
"hasOpenAI": val.get("hasOpenAI"),
|
||||
"codesCount": len(val.get("codes") or []),
|
||||
"emails": val.get("emails"),
|
||||
})
|
||||
if not val.get("accountOk"):
|
||||
log({"phase": "account_mismatch", "expected": ACCOUNT, "emails": val.get("emails")})
|
||||
raise SystemExit(4)
|
||||
if val.get("codes"):
|
||||
CODE_PATH.write_text(val["codes"][0])
|
||||
log({"phase": "current_alias_code_saved", "resend_index": resend_index, "folder": folder})
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def click_resend(resend_index):
|
||||
call = connect(OPENAI_CDP, lambda t: "auth.openai.com" in (t.get("url") or ""))
|
||||
res = evaluate(call, r'''(async()=>{
|
||||
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
|
||||
const btn=[...document.querySelectorAll('button,a')].find(e=>/重新发送|Resend/i.test(e.innerText||e.textContent||''));
|
||||
if(btn){btn.scrollIntoView({block:'center'}); btn.click(); await sleep(2500);}
|
||||
return {clicked:!!btn,href:location.href,body:(document.body?.innerText||'').slice(0,260)};
|
||||
})()''', True) or {}
|
||||
log({"phase": "openai_resend", "resend_index": resend_index, "clicked": res.get("clicked"), "href": res.get("href")})
|
||||
if not res.get("clicked"):
|
||||
log({"phase": "openai_resend_not_found", "resend_index": resend_index, "body": res.get("body")})
|
||||
raise SystemExit(5)
|
||||
|
||||
|
||||
def main():
|
||||
log({
|
||||
"phase": "strict_resend_cadence_start",
|
||||
"alias": ALIAS,
|
||||
"account": ACCOUNT,
|
||||
"start_resend_done": START_RESEND_DONE,
|
||||
"target_resends": TARGET_RESENDS,
|
||||
"refresh_rounds": REFRESH_ROUNDS,
|
||||
})
|
||||
for resend_index in range(START_RESEND_DONE + 1, TARGET_RESENDS + 1):
|
||||
log({"phase": "pre_resend_refresh_block_start", "resend_index": resend_index})
|
||||
for refresh_round in range(1, REFRESH_ROUNDS + 1):
|
||||
for name, url in FOLDERS:
|
||||
if refresh_once(name, url, resend_index, refresh_round):
|
||||
return 0
|
||||
if refresh_round < REFRESH_ROUNDS:
|
||||
time.sleep(SLEEP_BETWEEN_REFRESH_ROUNDS)
|
||||
click_resend(resend_index)
|
||||
log({"phase": "no_code_after_target_resends", "alias": ALIAS, "total_resends": TARGET_RESENDS})
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -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})
|
||||
Reference in New Issue
Block a user