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