Initial project setup
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
*.py text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.txt text eol=lf
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
accounts_*.txt
|
||||||
|
mail_credentials.txt
|
||||||
|
*.txt
|
||||||
|
!requirements.txt
|
||||||
|
|
||||||
|
config.json
|
||||||
|
config.local.json
|
||||||
|
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
DrissionPage/
|
||||||
|
autoPortData/
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Grok Register
|
||||||
|
|
||||||
|
Grok Register 是一个 Python 自动化注册工具,支持 GUI 和 CLI 两种运行方式。项目可接入临时邮箱服务完成注册流程,并将成功账号保存到本地文件或写入 [grok2api](https://github.com/chenyme/grok2api) 令牌池。
|
||||||
|
|
||||||
|
> 本项目仅用于自动化流程研究、测试环境验证和个人学习。请遵守目标网站服务条款、当地法律法规和第三方服务限制。
|
||||||
|
|
||||||
|
## 功能
|
||||||
|
|
||||||
|
- 支持 GUI 图形界面运行。
|
||||||
|
- 支持 CLI 终端运行,不启动 Tk GUI。
|
||||||
|
- 注册流程使用 Chromium/Chrome 浏览器页面完成。
|
||||||
|
- 支持 DuckMail、YYDS、Cloudflare 临时邮箱接口。
|
||||||
|
- 支持验证码邮件轮询和解析。
|
||||||
|
- 支持成功账号实时写入 `accounts_*.txt`。
|
||||||
|
- 支持将 SSO token 写入 grok2api 本地或远端池。
|
||||||
|
- 支持注册后尝试开启 NSFW。
|
||||||
|
- 支持页面卡住检测、当前账号重试、浏览器重启和内存清理。
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- Python 3.9+
|
||||||
|
- Google Chrome 或 Chromium
|
||||||
|
- 可访问注册页面和临时邮箱 API 的网络环境
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
下载项目到电脑:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/AaronL725/grok-register.git
|
||||||
|
cd grok-register
|
||||||
|
```
|
||||||
|
|
||||||
|
安装依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
复制配置文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp config.example.json config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
然后按需编辑 `config.json`。
|
||||||
|
|
||||||
|
## 配置
|
||||||
|
|
||||||
|
常用配置项:
|
||||||
|
|
||||||
|
| 配置项 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| `email_provider` | 邮箱服务商:`duckmail`、`yyds`、`cloudflare` |
|
||||||
|
| `register_count` | 本次目标注册数量 |
|
||||||
|
| `proxy` | 代理地址,可留空 |
|
||||||
|
| `enable_nsfw` | 注册后是否尝试开启 NSFW |
|
||||||
|
| `cloudflare_api_base` | Cloudflare 临时邮箱 API 地址 |
|
||||||
|
| `cloudflare_auth_mode` | Cloudflare API 鉴权模式:`none`、`bearer`、`x-api-key`、`query-key` |
|
||||||
|
| `defaultDomains` | Cloudflare 临时邮箱默认域名 |
|
||||||
|
| `grok2api_auto_add_local` | 是否写入本地 grok2api token 池 |
|
||||||
|
| `grok2api_local_token_file` | 本地 grok2api token 文件路径 |
|
||||||
|
| `grok2api_auto_add_remote` | 是否写入远端 grok2api |
|
||||||
|
| `grok2api_remote_base` | 远端 grok2api 管理 API 地址 |
|
||||||
|
| `grok2api_remote_app_key` | 远端 grok2api app key |
|
||||||
|
|
||||||
|
`config.json` 包含个人配置和密钥,不要提交到 Git。
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
|
||||||
|
### CLI 模式
|
||||||
|
|
||||||
|
CLI 模式不会启动 Tk GUI,但注册流程仍会打开 Chromium/Chrome 浏览器页面。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python grok_register_ttk.py cli
|
||||||
|
```
|
||||||
|
|
||||||
|
看到提示后输入:
|
||||||
|
|
||||||
|
```text
|
||||||
|
start
|
||||||
|
```
|
||||||
|
|
||||||
|
停止任务:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Ctrl+C
|
||||||
|
```
|
||||||
|
|
||||||
|
CLI 模式适合长时间批量运行。程序每成功注册 5 个账号会关闭浏览器、清理运行时对象并重新启动浏览器,降低长任务内存占用。
|
||||||
|
|
||||||
|
### GUI 模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python grok_register_ttk.py
|
||||||
|
```
|
||||||
|
|
||||||
|
GUI 模式会打开 Tkinter 窗口,适合手动调整配置和观察日志。
|
||||||
|
|
||||||
|
## 输出文件
|
||||||
|
|
||||||
|
运行过程中会生成:
|
||||||
|
|
||||||
|
- `accounts_*.txt`:成功账号、密码和 SSO token。
|
||||||
|
- `mail_credentials.txt`:临时邮箱凭证。
|
||||||
|
- `*.log`:可选日志文件。
|
||||||
|
|
||||||
|
这些文件包含敏感信息,已被 `.gitignore` 忽略。
|
||||||
|
|
||||||
|
## 稳定性机制
|
||||||
|
|
||||||
|
- 每个账号结束后重启浏览器。
|
||||||
|
- 每成功 5 个账号执行一次内存清理。
|
||||||
|
- CLI 模式支持 `Ctrl+C` 中断并清理浏览器。
|
||||||
|
- 最终页长时间无变化时自动重试当前账号。
|
||||||
|
- 验证码未收到时自动更换邮箱重试。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### CLI 模式为什么还会打开浏览器?
|
||||||
|
|
||||||
|
CLI 模式只是不启动 Tk GUI。注册页、Turnstile、验证码提交和 SSO cookie 获取仍依赖真实浏览器环境。
|
||||||
|
|
||||||
|
### NSFW 开启失败怎么办?
|
||||||
|
|
||||||
|
如果日志显示 `Cloudflare 防护拦截,HTTP 403`,说明请求被目标站点防护拦截。程序会继续保存账号和写入 grok2api。
|
||||||
|
|
||||||
|
### GUI 显示的数量和配置不同?
|
||||||
|
|
||||||
|
GUI 数量控件可能有上限。CLI 模式直接读取 `config.json` 中的 `register_count`。
|
||||||
|
|
||||||
|
## Git 忽略规则
|
||||||
|
|
||||||
|
以下内容不会提交:
|
||||||
|
|
||||||
|
- `config.json`
|
||||||
|
- `*.txt`
|
||||||
|
- `.venv/`
|
||||||
|
- 日志、缓存和本地 IDE 文件
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── grok_register_ttk.py # 主程序
|
||||||
|
├── cf_mail_debug.py # Cloudflare 邮箱调试工具
|
||||||
|
├── config.example.json # 配置示例
|
||||||
|
├── requirements.txt # Python 依赖
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
Executable
+175
@@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from curl_cffi import requests
|
||||||
|
|
||||||
|
|
||||||
|
def extract_code(text: str, subject: str = "") -> Optional[str]:
|
||||||
|
if subject:
|
||||||
|
m = re.search(r"\b([A-Z0-9]{3}-[A-Z0-9]{3})\b", subject, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
m = re.search(r"\b([A-Z0-9]{3}-[A-Z0-9]{3})\b", text, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
for p in [
|
||||||
|
r"verification\s+code[:\s]+(\d{4,8})",
|
||||||
|
r"your\s+code[:\s]+(\d{4,8})",
|
||||||
|
r"confirm(?:ation)?\s+code[:\s]+(\d{4,8})",
|
||||||
|
]:
|
||||||
|
m = re.search(p, text, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def json_or_text(resp: requests.Response) -> Tuple[Optional[Dict[str, Any]], str]:
|
||||||
|
try:
|
||||||
|
data = resp.json()
|
||||||
|
return data, ""
|
||||||
|
except Exception:
|
||||||
|
return None, (resp.text or "")[:400]
|
||||||
|
|
||||||
|
|
||||||
|
def create_address(api_base: str) -> Tuple[str, str]:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{api_base.rstrip('/')}/api/new_address",
|
||||||
|
json={},
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data, raw = json_or_text(resp)
|
||||||
|
if not data:
|
||||||
|
raise RuntimeError(f"/api/new_address 非JSON: {raw}")
|
||||||
|
address = str(data.get("address", "")).strip()
|
||||||
|
jwt = str(data.get("jwt", "")).strip()
|
||||||
|
if not address or not jwt:
|
||||||
|
raise RuntimeError(f"/api/new_address 缺少 address/jwt: {data}")
|
||||||
|
return address, jwt
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_box(api_base: str, jwt: str, path: str, params: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
resp = requests.get(
|
||||||
|
f"{api_base.rstrip('/')}{path}",
|
||||||
|
params=params,
|
||||||
|
headers={"Authorization": f"Bearer {jwt}"},
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
if resp.status_code >= 400:
|
||||||
|
return []
|
||||||
|
data, _ = json_or_text(resp)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return []
|
||||||
|
if isinstance(data.get("results"), list):
|
||||||
|
return data["results"]
|
||||||
|
if isinstance(data.get("data"), list):
|
||||||
|
return data["data"]
|
||||||
|
if isinstance(data.get("messages"), list):
|
||||||
|
return data["messages"]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def probe_all_boxes(api_base: str, jwt: str) -> List[Tuple[str, List[Dict[str, Any]]]]:
|
||||||
|
probes = [
|
||||||
|
("/api/mails", {"limit": 20, "offset": 0}),
|
||||||
|
("/api/sendbox", {"limit": 20, "offset": 0}),
|
||||||
|
("/api/mails", {"limit": 20, "offset": 0, "box": "trash"}),
|
||||||
|
("/api/mails", {"limit": 20, "offset": 0, "folder": "trash"}),
|
||||||
|
("/api/mails", {"limit": 20, "offset": 0, "deleted": "1"}),
|
||||||
|
("/api/mails", {"limit": 20, "offset": 0, "status": "deleted"}),
|
||||||
|
]
|
||||||
|
out: List[Tuple[str, List[Dict[str, Any]]]] = []
|
||||||
|
for path, params in probes:
|
||||||
|
mails = fetch_box(api_base, jwt, path, params)
|
||||||
|
out.append((f"{path}?{params}", mails))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def get_detail(api_base: str, jwt: str, mail_id: Any) -> Dict[str, Any]:
|
||||||
|
for url in [
|
||||||
|
f"{api_base.rstrip('/')}/api/mail/{mail_id}",
|
||||||
|
f"{api_base.rstrip('/')}/api/mails/{mail_id}",
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, headers={"Authorization": f"Bearer {jwt}"}, timeout=20)
|
||||||
|
if resp.status_code >= 400:
|
||||||
|
continue
|
||||||
|
data, _ = json_or_text(resp)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_mail_text(item: Dict[str, Any], detail: Dict[str, Any]) -> Tuple[str, str]:
|
||||||
|
subject = str(item.get("subject") or detail.get("subject") or "")
|
||||||
|
parts: List[str] = []
|
||||||
|
for src in (item, detail):
|
||||||
|
for k in ("text", "raw", "content", "intro", "body", "snippet"):
|
||||||
|
v = src.get(k)
|
||||||
|
if isinstance(v, str) and v.strip():
|
||||||
|
parts.append(v)
|
||||||
|
html_val = src.get("html")
|
||||||
|
if isinstance(html_val, str):
|
||||||
|
html_val = [html_val]
|
||||||
|
if isinstance(html_val, list):
|
||||||
|
for h in html_val:
|
||||||
|
if isinstance(h, str):
|
||||||
|
parts.append(re.sub(r"<[^>]+>", " ", h))
|
||||||
|
return subject, "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--api-base", required=True)
|
||||||
|
ap.add_argument("--address", default="")
|
||||||
|
ap.add_argument("--credential", default="")
|
||||||
|
ap.add_argument("--timeout", type=int, default=180)
|
||||||
|
ap.add_argument("--interval", type=int, default=3)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
address = args.address.strip()
|
||||||
|
credential = args.credential.strip()
|
||||||
|
if not credential:
|
||||||
|
address, credential = create_address(args.api_base)
|
||||||
|
print(f"[NEW] address={address}")
|
||||||
|
print(f"[NEW] credential(jwt)={credential}")
|
||||||
|
else:
|
||||||
|
print(f"[USE] address={address or '(unknown, from credential)'}")
|
||||||
|
|
||||||
|
deadline = time.time() + max(args.timeout, 1)
|
||||||
|
seen_ids = set()
|
||||||
|
while time.time() < deadline:
|
||||||
|
boxes = probe_all_boxes(args.api_base, credential)
|
||||||
|
total = 0
|
||||||
|
for name, mails in boxes:
|
||||||
|
if mails:
|
||||||
|
print(f"[BOX] {name} -> {len(mails)}")
|
||||||
|
total += len(mails)
|
||||||
|
for m in mails:
|
||||||
|
mail_id = m.get("id") or m.get("mail_id")
|
||||||
|
if not mail_id or mail_id in seen_ids:
|
||||||
|
continue
|
||||||
|
seen_ids.add(mail_id)
|
||||||
|
detail = get_detail(args.api_base, credential, mail_id)
|
||||||
|
subj, text = flatten_mail_text(m, detail)
|
||||||
|
code = extract_code(text, subj)
|
||||||
|
print(f"[MAIL] id={mail_id} subject={subj!r} code={code!r}")
|
||||||
|
if code:
|
||||||
|
print(f"[FOUND] {code}")
|
||||||
|
return
|
||||||
|
if total == 0:
|
||||||
|
print("[INFO] no mails yet")
|
||||||
|
time.sleep(max(args.interval, 1))
|
||||||
|
print("[TIMEOUT] no code found")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+24
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"duckmail_api_key": "",
|
||||||
|
"cloudflare_api_base": "https://cloudflare.pages.dev",
|
||||||
|
"cloudflare_api_key": "",
|
||||||
|
"cloudflare_auth_mode": "none",
|
||||||
|
"cloudflare_path_domains": "/api/domains",
|
||||||
|
"cloudflare_path_accounts": "/api/new_address",
|
||||||
|
"cloudflare_path_token": "/api/token",
|
||||||
|
"cloudflare_path_messages": "/api/mails",
|
||||||
|
"proxy": "",
|
||||||
|
"enable_nsfw": true,
|
||||||
|
"register_count": 1,
|
||||||
|
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
|
||||||
|
"grok2api_auto_add_local": false,
|
||||||
|
"grok2api_local_token_file": "",
|
||||||
|
"grok2api_pool_name": "ssoBasic",
|
||||||
|
"grok2api_auto_add_remote": false,
|
||||||
|
"grok2api_remote_base": "",
|
||||||
|
"grok2api_remote_app_key": "",
|
||||||
|
"defaultDomains": "example.com",
|
||||||
|
"email_provider": "cloudflare",
|
||||||
|
"yyds_api_key": "",
|
||||||
|
"yyds_jwt": ""
|
||||||
|
}
|
||||||
Executable
+2992
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
|||||||
|
certifi==2026.4.22
|
||||||
|
cffi==2.0.0
|
||||||
|
charset-normalizer==3.4.7
|
||||||
|
cssselect==1.3.0
|
||||||
|
curl_cffi==0.13.0
|
||||||
|
DataRecorder==3.6.2
|
||||||
|
DownloadKit==2.0.7
|
||||||
|
DrissionPage==4.1.1.2
|
||||||
|
filelock==3.19.1
|
||||||
|
idna==3.13
|
||||||
|
lxml==6.1.0
|
||||||
|
psutil==7.2.2
|
||||||
|
pycparser==2.23
|
||||||
|
requests==2.32.5
|
||||||
|
requests-file==3.0.1
|
||||||
|
tldextract==5.3.0
|
||||||
|
urllib3==2.6.3
|
||||||
|
websocket-client==1.9.0
|
||||||
Reference in New Issue
Block a user