HouYunFei Codex commited on
Commit ยท
2331be7
1
Parent(s): 6de60d1
Add registration runner UI
Browse filesCo-Authored-By: Codex <noreply@openai.com>
- api/accounts.py +0 -1
- api/app.py +2 -1
- api/register.py +68 -0
- config.json +1 -1
- services/register/__init__.py +0 -0
- services/register/mail_provider.py +414 -0
- services/register/openai_register.py +608 -0
- services/register_service.py +190 -0
- web/src/app/register/components/register-card.tsx +287 -0
- web/src/app/register/page.tsx +74 -0
- web/src/app/settings/store.ts +184 -0
- web/src/components/top-nav.tsx +1 -0
- web/src/lib/api.ts +61 -0
api/accounts.py
CHANGED
|
@@ -327,4 +327,3 @@ def create_router() -> APIRouter:
|
|
| 327 |
return {"import_job": server.get("import_job")}
|
| 328 |
|
| 329 |
return router
|
| 330 |
-
|
|
|
|
| 327 |
return {"import_job": server.get("import_job")}
|
| 328 |
|
| 329 |
return router
|
|
|
api/app.py
CHANGED
|
@@ -8,7 +8,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 8 |
from fastapi.responses import FileResponse
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
| 10 |
|
| 11 |
-
from api import accounts, ai, system
|
| 12 |
from api.support import resolve_web_asset, start_limited_account_watcher
|
| 13 |
from services.config import config
|
| 14 |
|
|
@@ -37,6 +37,7 @@ def create_app() -> FastAPI:
|
|
| 37 |
)
|
| 38 |
app.include_router(ai.create_router())
|
| 39 |
app.include_router(accounts.create_router())
|
|
|
|
| 40 |
app.include_router(system.create_router(app_version))
|
| 41 |
if config.images_dir.exists():
|
| 42 |
app.mount("/images", StaticFiles(directory=str(config.images_dir)), name="images")
|
|
|
|
| 8 |
from fastapi.responses import FileResponse
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
| 10 |
|
| 11 |
+
from api import accounts, ai, register, system
|
| 12 |
from api.support import resolve_web_asset, start_limited_account_watcher
|
| 13 |
from services.config import config
|
| 14 |
|
|
|
|
| 37 |
)
|
| 38 |
app.include_router(ai.create_router())
|
| 39 |
app.include_router(accounts.create_router())
|
| 40 |
+
app.include_router(register.create_router())
|
| 41 |
app.include_router(system.create_router(app_version))
|
| 42 |
if config.images_dir.exists():
|
| 43 |
app.mount("/images", StaticFiles(directory=str(config.images_dir)), name="images")
|
api/register.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
from fastapi import APIRouter, Header
|
| 7 |
+
from fastapi.responses import StreamingResponse
|
| 8 |
+
from pydantic import BaseModel
|
| 9 |
+
|
| 10 |
+
from api.support import require_admin
|
| 11 |
+
from services.register_service import register_service
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class RegisterConfigRequest(BaseModel):
|
| 15 |
+
mail: dict | None = None
|
| 16 |
+
proxy: str | None = None
|
| 17 |
+
total: int | None = None
|
| 18 |
+
threads: int | None = None
|
| 19 |
+
mode: str | None = None
|
| 20 |
+
target_quota: int | None = None
|
| 21 |
+
target_available: int | None = None
|
| 22 |
+
check_interval: int | None = None
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def create_router() -> APIRouter:
|
| 26 |
+
router = APIRouter()
|
| 27 |
+
|
| 28 |
+
@router.get("/api/register")
|
| 29 |
+
async def get_register_config(authorization: str | None = Header(default=None)):
|
| 30 |
+
require_admin(authorization)
|
| 31 |
+
return {"register": register_service.get()}
|
| 32 |
+
|
| 33 |
+
@router.post("/api/register")
|
| 34 |
+
async def update_register_config(body: RegisterConfigRequest, authorization: str | None = Header(default=None)):
|
| 35 |
+
require_admin(authorization)
|
| 36 |
+
return {"register": register_service.update(body.model_dump(exclude_none=True))}
|
| 37 |
+
|
| 38 |
+
@router.post("/api/register/start")
|
| 39 |
+
async def start_register(authorization: str | None = Header(default=None)):
|
| 40 |
+
require_admin(authorization)
|
| 41 |
+
return {"register": register_service.start()}
|
| 42 |
+
|
| 43 |
+
@router.post("/api/register/stop")
|
| 44 |
+
async def stop_register(authorization: str | None = Header(default=None)):
|
| 45 |
+
require_admin(authorization)
|
| 46 |
+
return {"register": register_service.stop()}
|
| 47 |
+
|
| 48 |
+
@router.post("/api/register/reset")
|
| 49 |
+
async def reset_register(authorization: str | None = Header(default=None)):
|
| 50 |
+
require_admin(authorization)
|
| 51 |
+
return {"register": register_service.reset()}
|
| 52 |
+
|
| 53 |
+
@router.get("/api/register/events")
|
| 54 |
+
async def register_events(token: str = ""):
|
| 55 |
+
require_admin(f"Bearer {token}")
|
| 56 |
+
|
| 57 |
+
async def stream():
|
| 58 |
+
last = ""
|
| 59 |
+
while True:
|
| 60 |
+
payload = json.dumps(register_service.get(), ensure_ascii=False)
|
| 61 |
+
if payload != last:
|
| 62 |
+
last = payload
|
| 63 |
+
yield f"data: {payload}\n\n"
|
| 64 |
+
await asyncio.sleep(0.5)
|
| 65 |
+
|
| 66 |
+
return StreamingResponse(stream(), media_type="text/event-stream")
|
| 67 |
+
|
| 68 |
+
return router
|
config.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
"auth-key": "chatgpt2api",
|
| 3 |
"refresh_account_interval_minute": 60,
|
| 4 |
"image_retention_days": 15,
|
| 5 |
-
"auto_remove_invalid_accounts":
|
| 6 |
"log_levels": [
|
| 7 |
"debug",
|
| 8 |
"error",
|
|
|
|
| 2 |
"auth-key": "chatgpt2api",
|
| 3 |
"refresh_account_interval_minute": 60,
|
| 4 |
"image_retention_days": 15,
|
| 5 |
+
"auto_remove_invalid_accounts": true,
|
| 6 |
"log_levels": [
|
| 7 |
"debug",
|
| 8 |
"error",
|
services/register/__init__.py
ADDED
|
File without changes
|
services/register/mail_provider.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import random
|
| 4 |
+
import re
|
| 5 |
+
import string
|
| 6 |
+
import time
|
| 7 |
+
from datetime import datetime, timezone
|
| 8 |
+
from email import message_from_string, policy
|
| 9 |
+
from email.utils import parsedate_to_datetime
|
| 10 |
+
from threading import Lock
|
| 11 |
+
from typing import Any, Callable, TypeVar
|
| 12 |
+
|
| 13 |
+
import requests
|
| 14 |
+
from curl_cffi import requests as curl_requests
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
ResultT = TypeVar("ResultT")
|
| 18 |
+
domain_lock = Lock()
|
| 19 |
+
provider_lock = Lock()
|
| 20 |
+
domain_index = 0
|
| 21 |
+
provider_index = 0
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _config(mail_config: dict) -> dict:
|
| 25 |
+
return {
|
| 26 |
+
"request_timeout": float(mail_config.get("request_timeout") or 15),
|
| 27 |
+
"wait_timeout": float(mail_config.get("wait_timeout") or 30),
|
| 28 |
+
"wait_interval": float(mail_config.get("wait_interval") or 3),
|
| 29 |
+
"user_agent": str(mail_config.get("user_agent") or "Mozilla/5.0"),
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _random_mailbox_name() -> str:
|
| 34 |
+
return f"{''.join(random.choices(string.ascii_lowercase, k=5))}{''.join(random.choices(string.digits, k=random.randint(1, 3)))}{''.join(random.choices(string.ascii_lowercase, k=random.randint(1, 3)))}"
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _random_subdomain_label() -> str:
|
| 38 |
+
return "".join(random.choices(string.ascii_lowercase + string.digits, k=random.randint(4, 10)))
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def _next_domain(domains: list[str]) -> str:
|
| 42 |
+
global domain_index
|
| 43 |
+
domains = [str(item).strip() for item in domains if str(item).strip()]
|
| 44 |
+
if not domains:
|
| 45 |
+
raise RuntimeError("mail.domain ไธ่ฝไธบ็ฉบ")
|
| 46 |
+
if len(domains) == 1:
|
| 47 |
+
return domains[0]
|
| 48 |
+
with domain_lock:
|
| 49 |
+
value = domains[domain_index % len(domains)]
|
| 50 |
+
domain_index = (domain_index + 1) % len(domains)
|
| 51 |
+
return value
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def _parse_received_at(value: Any) -> datetime | None:
|
| 55 |
+
if isinstance(value, (int, float)):
|
| 56 |
+
try:
|
| 57 |
+
return datetime.fromtimestamp(float(value), tz=timezone.utc)
|
| 58 |
+
except Exception:
|
| 59 |
+
return None
|
| 60 |
+
text = str(value or "").strip()
|
| 61 |
+
if not text:
|
| 62 |
+
return None
|
| 63 |
+
try:
|
| 64 |
+
date = datetime.fromisoformat(text[:-1] + "+00:00" if text.endswith("Z") else text)
|
| 65 |
+
return date if date.tzinfo else date.replace(tzinfo=timezone.utc)
|
| 66 |
+
except Exception:
|
| 67 |
+
pass
|
| 68 |
+
try:
|
| 69 |
+
date = parsedate_to_datetime(text)
|
| 70 |
+
return date if date.tzinfo else date.replace(tzinfo=timezone.utc)
|
| 71 |
+
except Exception:
|
| 72 |
+
return None
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _extract_content(data: dict[str, Any]) -> tuple[str, str]:
|
| 76 |
+
text_content = str(data.get("text_content") or data.get("text") or data.get("body") or data.get("content") or "")
|
| 77 |
+
html_content = str(data.get("html_content") or data.get("html") or data.get("html_body") or data.get("body_html") or "")
|
| 78 |
+
if text_content or html_content:
|
| 79 |
+
return text_content, html_content
|
| 80 |
+
raw = data.get("raw")
|
| 81 |
+
if not isinstance(raw, str) or not raw.strip():
|
| 82 |
+
return "", ""
|
| 83 |
+
try:
|
| 84 |
+
parsed = message_from_string(raw, policy=policy.default)
|
| 85 |
+
except Exception:
|
| 86 |
+
return raw, ""
|
| 87 |
+
plain: list[str] = []
|
| 88 |
+
html: list[str] = []
|
| 89 |
+
for part in parsed.walk() if parsed.is_multipart() else [parsed]:
|
| 90 |
+
if part.get_content_maintype() == "multipart":
|
| 91 |
+
continue
|
| 92 |
+
try:
|
| 93 |
+
payload = part.get_content()
|
| 94 |
+
except Exception:
|
| 95 |
+
payload = ""
|
| 96 |
+
if not payload:
|
| 97 |
+
continue
|
| 98 |
+
if part.get_content_type() == "text/html":
|
| 99 |
+
html.append(str(payload))
|
| 100 |
+
else:
|
| 101 |
+
plain.append(str(payload))
|
| 102 |
+
return "\n".join(plain).strip(), "\n".join(html).strip()
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def _extract_text_candidates(value: Any) -> list[str]:
|
| 106 |
+
if isinstance(value, str):
|
| 107 |
+
return [value]
|
| 108 |
+
if isinstance(value, dict):
|
| 109 |
+
out: list[str] = []
|
| 110 |
+
for key in ("address", "email", "name", "value"):
|
| 111 |
+
if value.get(key):
|
| 112 |
+
out.extend(_extract_text_candidates(value.get(key)))
|
| 113 |
+
return out
|
| 114 |
+
if isinstance(value, list):
|
| 115 |
+
out: list[str] = []
|
| 116 |
+
for item in value:
|
| 117 |
+
out.extend(_extract_text_candidates(item))
|
| 118 |
+
return out
|
| 119 |
+
return []
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _message_matches_email(data: dict[str, Any], email: str) -> bool:
|
| 123 |
+
target = str(email or "").strip().lower()
|
| 124 |
+
candidates: list[str] = []
|
| 125 |
+
for key in ("to", "mailTo", "receiver", "receivers", "address", "email", "envelope_to"):
|
| 126 |
+
if key in data:
|
| 127 |
+
candidates.extend(_extract_text_candidates(data.get(key)))
|
| 128 |
+
return not target or not candidates or any(target in str(item).strip().lower() for item in candidates if str(item).strip())
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def _extract_code(message: dict[str, Any]) -> str | None:
|
| 132 |
+
content = f"{message.get('subject', '')}\n{message.get('text_content', '')}\n{message.get('html_content', '')}".strip()
|
| 133 |
+
if not content:
|
| 134 |
+
return None
|
| 135 |
+
match = re.search(r"background-color:\s*#F3F3F3[^>]*>[\s\S]*?(\d{6})[\s\S]*?</p>", content, re.I)
|
| 136 |
+
if match:
|
| 137 |
+
return match.group(1)
|
| 138 |
+
match = re.search(r"(?:Verification code|code is|ไปฃ็ ไธบ|้ช่ฏ็ )[:\s]*(\d{6})", content, re.I)
|
| 139 |
+
if match and match.group(1) != "177010":
|
| 140 |
+
return match.group(1)
|
| 141 |
+
for code in re.findall(r">\s*(\d{6})\s*<|(?<![#&])\b(\d{6})\b", content):
|
| 142 |
+
value = code[0] or code[1]
|
| 143 |
+
if value and value != "177010":
|
| 144 |
+
return value
|
| 145 |
+
return None
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
class BaseMailProvider:
|
| 149 |
+
name = "unknown"
|
| 150 |
+
|
| 151 |
+
def __init__(self, conf: dict, provider_ref: str = ""):
|
| 152 |
+
self.conf = conf
|
| 153 |
+
self.provider_ref = provider_ref
|
| 154 |
+
|
| 155 |
+
def wait_for(self, mailbox: dict[str, Any], on_message: Callable[[dict[str, Any]], ResultT | None]) -> ResultT | None:
|
| 156 |
+
deadline = time.monotonic() + self.conf["wait_timeout"]
|
| 157 |
+
while time.monotonic() < deadline:
|
| 158 |
+
message = self.fetch_latest_message(mailbox)
|
| 159 |
+
if message:
|
| 160 |
+
result = on_message(message)
|
| 161 |
+
if result is not None:
|
| 162 |
+
return result
|
| 163 |
+
time.sleep(max(0.2, self.conf["wait_interval"]))
|
| 164 |
+
return None
|
| 165 |
+
|
| 166 |
+
def wait_for_code(self, mailbox: dict[str, Any]) -> str | None:
|
| 167 |
+
return self.wait_for(mailbox, _extract_code)
|
| 168 |
+
|
| 169 |
+
def close(self) -> None:
|
| 170 |
+
pass
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
class CloudflareTempMailProvider(BaseMailProvider):
|
| 174 |
+
name = "cloudflare_temp_email"
|
| 175 |
+
|
| 176 |
+
def __init__(self, entry: dict, conf: dict):
|
| 177 |
+
super().__init__(conf, str(entry.get("provider_ref") or ""))
|
| 178 |
+
self.api_base = str(entry["api_base"]).rstrip("/")
|
| 179 |
+
self.admin_password = str(entry["admin_password"]).strip()
|
| 180 |
+
self.domain = entry.get("domain") or []
|
| 181 |
+
self.session = curl_requests.Session(impersonate="chrome")
|
| 182 |
+
|
| 183 |
+
def _request(self, method: str, path: str, headers: dict | None = None, params: dict | None = None, payload: dict | None = None, expected: tuple[int, ...] = (200,)):
|
| 184 |
+
resp = self.session.request(method.upper(), f"{self.api_base}{path}", headers={"Content-Type": "application/json", "User-Agent": self.conf["user_agent"], **(headers or {})}, params=params, json=payload, timeout=self.conf["request_timeout"], verify=False)
|
| 185 |
+
if resp.status_code not in expected:
|
| 186 |
+
raise RuntimeError(f"CloudflareTempMail ่ฏทๆฑๅคฑ่ดฅ: {method} {path}, HTTP {resp.status_code}, body={resp.text[:300]}")
|
| 187 |
+
return {} if resp.status_code == 204 else resp.json()
|
| 188 |
+
|
| 189 |
+
def create_mailbox(self, username: str | None = None) -> dict[str, Any]:
|
| 190 |
+
data = self._request("POST", "/admin/new_address", headers={"x-admin-auth": self.admin_password}, payload={"enablePrefix": True, "name": username or _random_mailbox_name(), "domain": _next_domain(self.domain)})
|
| 191 |
+
address = str(data.get("address") or "").strip()
|
| 192 |
+
token = str(data.get("jwt") or "").strip()
|
| 193 |
+
if not address or not token:
|
| 194 |
+
raise RuntimeError("CloudflareTempMail ็ผบๅฐ address ๆ jwt")
|
| 195 |
+
return {"provider": self.name, "provider_ref": self.provider_ref, "address": address, "token": token}
|
| 196 |
+
|
| 197 |
+
def fetch_latest_message(self, mailbox: dict[str, Any]) -> dict[str, Any] | None:
|
| 198 |
+
data = self._request("GET", "/api/mails", headers={"Authorization": f"Bearer {mailbox['token']}"}, params={"limit": 10, "offset": 0})
|
| 199 |
+
raw = list(data.get("results") or []) if isinstance(data, dict) else data if isinstance(data, list) else []
|
| 200 |
+
messages = [item for item in raw if isinstance(item, dict) and _message_matches_email(item, str(mailbox.get("address") or ""))]
|
| 201 |
+
if not messages:
|
| 202 |
+
return None
|
| 203 |
+
item = messages[0]
|
| 204 |
+
text_content, html_content = _extract_content(item)
|
| 205 |
+
sender = item.get("from") or item.get("sender") or ""
|
| 206 |
+
if isinstance(sender, dict):
|
| 207 |
+
sender = sender.get("address") or sender.get("email") or sender.get("name") or ""
|
| 208 |
+
return {"provider": self.name, "mailbox": mailbox["address"], "message_id": str(item.get("id") or item.get("_id") or ""), "subject": str(item.get("subject") or ""), "sender": str(sender), "text_content": text_content, "html_content": html_content, "received_at": _parse_received_at(item.get("createdAt") or item.get("created_at") or item.get("receivedAt") or item.get("date") or item.get("timestamp")), "raw": item}
|
| 209 |
+
|
| 210 |
+
def close(self) -> None:
|
| 211 |
+
self.session.close()
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
class TempMailLolProvider(BaseMailProvider):
|
| 215 |
+
name = "tempmail_lol"
|
| 216 |
+
|
| 217 |
+
def __init__(self, entry: dict, conf: dict):
|
| 218 |
+
super().__init__(conf, str(entry.get("provider_ref") or ""))
|
| 219 |
+
self.api_key = str(entry.get("api_key") or "").strip()
|
| 220 |
+
self.domain = [str(item).strip() for item in (entry.get("domain") or []) if str(item).strip()]
|
| 221 |
+
self.session = requests.Session()
|
| 222 |
+
self.session.trust_env = False
|
| 223 |
+
self.session.headers.update({"User-Agent": conf["user_agent"], "Accept": "application/json", "Content-Type": "application/json"})
|
| 224 |
+
if self.api_key:
|
| 225 |
+
self.session.headers["Authorization"] = f"Bearer {self.api_key}"
|
| 226 |
+
|
| 227 |
+
@staticmethod
|
| 228 |
+
def _resolve_domain(domain: str) -> tuple[str, bool]:
|
| 229 |
+
text = str(domain or "").strip().lower()
|
| 230 |
+
if text.startswith("*.") and len(text) > 2:
|
| 231 |
+
return f"{_random_subdomain_label()}.{text[2:]}", True
|
| 232 |
+
return text, False
|
| 233 |
+
|
| 234 |
+
def _request(self, method: str, path: str, params: dict | None = None, payload: dict | None = None, expected: tuple[int, ...] = (200,)):
|
| 235 |
+
resp = self.session.request(method.upper(), f"https://api.tempmail.lol/v2{path}", params=params, json=payload, timeout=self.conf["request_timeout"], verify=False)
|
| 236 |
+
if resp.status_code not in expected:
|
| 237 |
+
raise RuntimeError(f"TempMail.lol ่ฏทๆฑๅคฑ่ดฅ: {method} {path}, HTTP {resp.status_code}, body={resp.text[:300]}")
|
| 238 |
+
data = resp.json()
|
| 239 |
+
if not isinstance(data, dict):
|
| 240 |
+
raise RuntimeError(f"TempMail.lol {method} {path} ่ฟๅ็ปๆไธๆฏๅฏน่ฑก")
|
| 241 |
+
return data
|
| 242 |
+
|
| 243 |
+
def create_mailbox(self, username: str | None = None) -> dict[str, Any]:
|
| 244 |
+
payload: dict[str, Any] = {}
|
| 245 |
+
if self.domain:
|
| 246 |
+
domain, force_random_prefix = self._resolve_domain(random.choice(self.domain))
|
| 247 |
+
payload["domain"] = domain
|
| 248 |
+
if force_random_prefix:
|
| 249 |
+
payload["prefix"] = _random_mailbox_name()
|
| 250 |
+
if username and "prefix" not in payload:
|
| 251 |
+
payload["prefix"] = username
|
| 252 |
+
data = self._request("POST", "/inbox/create", payload=payload, expected=(200, 201))
|
| 253 |
+
address = str(data.get("address") or "").strip()
|
| 254 |
+
token = str(data.get("token") or "").strip()
|
| 255 |
+
if not address or not token:
|
| 256 |
+
raise RuntimeError("TempMail.lol ็ผบๅฐ address ๆ token")
|
| 257 |
+
return {"provider": self.name, "provider_ref": self.provider_ref, "address": address, "token": token}
|
| 258 |
+
|
| 259 |
+
def fetch_latest_message(self, mailbox: dict[str, Any]) -> dict[str, Any] | None:
|
| 260 |
+
data = self._request("GET", "/inbox", params={"token": mailbox["token"]})
|
| 261 |
+
items = data.get("emails") or data.get("messages") or []
|
| 262 |
+
messages = [item for item in items if isinstance(item, dict)] if isinstance(items, list) else []
|
| 263 |
+
if not messages:
|
| 264 |
+
return None
|
| 265 |
+
item = max(messages, key=lambda value: ((_parse_received_at(value.get("created_at") or value.get("createdAt") or value.get("date") or value.get("received_at") or value.get("timestamp")) or datetime.fromtimestamp(0, tz=timezone.utc)).timestamp(), str(value.get("id") or value.get("token") or "")))
|
| 266 |
+
text_content, html_content = _extract_content(item)
|
| 267 |
+
return {"provider": self.name, "mailbox": mailbox["address"], "message_id": str(item.get("id") or item.get("token") or ""), "subject": str(item.get("subject") or ""), "sender": str(item.get("from") or item.get("from_address") or ""), "text_content": text_content, "html_content": html_content, "received_at": _parse_received_at(item.get("created_at") or item.get("createdAt") or item.get("date") or item.get("received_at") or item.get("timestamp")), "raw": item}
|
| 268 |
+
|
| 269 |
+
def close(self) -> None:
|
| 270 |
+
self.session.close()
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
class DuckMailProvider(BaseMailProvider):
|
| 274 |
+
name = "duckmail"
|
| 275 |
+
|
| 276 |
+
def __init__(self, entry: dict, conf: dict):
|
| 277 |
+
super().__init__(conf, str(entry.get("provider_ref") or ""))
|
| 278 |
+
self.api_key = str(entry["api_key"]).strip()
|
| 279 |
+
self.default_domain = str(entry.get("default_domain") or "duckmail.sbs").strip() or "duckmail.sbs"
|
| 280 |
+
self.session = requests.Session()
|
| 281 |
+
self.session.trust_env = False
|
| 282 |
+
self.session.headers.update({"User-Agent": conf["user_agent"], "Accept": "application/json", "Content-Type": "application/json"})
|
| 283 |
+
|
| 284 |
+
def _request(self, method: str, path: str, token: str = "", use_api_key: bool = False, params: dict | None = None, payload: dict | None = None, expected: tuple[int, ...] = (200, 201, 204)):
|
| 285 |
+
headers = {"Authorization": f"Bearer {self.api_key if use_api_key else token}"} if use_api_key or token else {}
|
| 286 |
+
resp = self.session.request(method.upper(), f"https://api.duckmail.sbs{path}", headers=headers, params=params, json=payload, timeout=self.conf["request_timeout"], verify=False)
|
| 287 |
+
if resp.status_code not in expected:
|
| 288 |
+
raise RuntimeError(f"DuckMail ่ฏทๆฑๅคฑ่ดฅ: {method} {path}, HTTP {resp.status_code}, body={resp.text[:300]}")
|
| 289 |
+
return {} if resp.status_code == 204 else resp.json()
|
| 290 |
+
|
| 291 |
+
@staticmethod
|
| 292 |
+
def _items(data):
|
| 293 |
+
return data if isinstance(data, list) else data.get("hydra:member") or data.get("member") or data.get("data") or []
|
| 294 |
+
|
| 295 |
+
def create_mailbox(self, username: str | None = None) -> dict[str, Any]:
|
| 296 |
+
domains = self._items(self._request("GET", "/domains", use_api_key=True))
|
| 297 |
+
domain = random.choice(domains).get("domain") if domains else self.default_domain
|
| 298 |
+
password = "".join(random.choices(string.ascii_letters + string.digits, k=12))
|
| 299 |
+
address = f"{username or _random_mailbox_name()}@{domain}"
|
| 300 |
+
payload = {"address": address, "password": password}
|
| 301 |
+
account = self._request("POST", "/accounts", use_api_key=True, payload=payload)
|
| 302 |
+
token_data = self._request("POST", "/token", use_api_key=True, payload=payload)
|
| 303 |
+
return {"provider": self.name, "provider_ref": self.provider_ref, "address": address, "token": str(token_data.get("token") or ""), "password": password, "account_id": str(account.get("id") or "")}
|
| 304 |
+
|
| 305 |
+
def fetch_latest_message(self, mailbox: dict[str, Any]) -> dict[str, Any] | None:
|
| 306 |
+
data = self._request("GET", "/messages", token=str(mailbox.get("token") or ""), params={"page": 1})
|
| 307 |
+
items = self._items(data)
|
| 308 |
+
if not items:
|
| 309 |
+
return None
|
| 310 |
+
item = items[0]
|
| 311 |
+
message_id = str(item.get("id") or item.get("@id") or "").replace("/messages/", "")
|
| 312 |
+
if message_id:
|
| 313 |
+
item = self._request("GET", f"/messages/{message_id}", token=str(mailbox.get("token") or ""))
|
| 314 |
+
sender = item.get("from") or ""
|
| 315 |
+
if isinstance(sender, dict):
|
| 316 |
+
sender = sender.get("address") or sender.get("name") or ""
|
| 317 |
+
html_content = item.get("html") or ""
|
| 318 |
+
if isinstance(html_content, list):
|
| 319 |
+
html_content = "".join(str(value) for value in html_content)
|
| 320 |
+
return {"provider": self.name, "mailbox": mailbox["address"], "message_id": message_id, "subject": str(item.get("subject") or ""), "sender": str(sender), "text_content": str(item.get("text") or item.get("text_content") or ""), "html_content": str(html_content), "received_at": _parse_received_at(item.get("createdAt") or item.get("created_at") or item.get("receivedAt") or item.get("date")), "raw": item}
|
| 321 |
+
|
| 322 |
+
def close(self) -> None:
|
| 323 |
+
self.session.close()
|
| 324 |
+
|
| 325 |
+
|
| 326 |
+
class GptMailProvider(BaseMailProvider):
|
| 327 |
+
name = "gptmail"
|
| 328 |
+
|
| 329 |
+
def __init__(self, entry: dict, conf: dict):
|
| 330 |
+
super().__init__(conf, str(entry.get("provider_ref") or ""))
|
| 331 |
+
self.api_key = str(entry["api_key"]).strip()
|
| 332 |
+
self.default_domain = str(entry.get("default_domain") or "").strip()
|
| 333 |
+
self.session = requests.Session()
|
| 334 |
+
self.session.trust_env = False
|
| 335 |
+
self.session.headers.update({"User-Agent": conf["user_agent"], "Accept": "application/json", "Content-Type": "application/json", "X-API-Key": self.api_key})
|
| 336 |
+
|
| 337 |
+
def _request(self, method: str, path: str, params: dict | None = None, payload: dict | None = None):
|
| 338 |
+
query = dict(params or {})
|
| 339 |
+
resp = self.session.request(method.upper(), f"https://mail.chatgpt.org.uk{path}", params=query, json=payload, timeout=self.conf["request_timeout"], verify=False)
|
| 340 |
+
if resp.status_code != 200:
|
| 341 |
+
raise RuntimeError(f"GPTMail ่ฏทๆฑๅคฑ่ดฅ: {method} {path}, HTTP {resp.status_code}, body={resp.text[:300]}")
|
| 342 |
+
data = resp.json()
|
| 343 |
+
return data["data"] if isinstance(data, dict) and "data" in data else data
|
| 344 |
+
|
| 345 |
+
def create_mailbox(self, username: str | None = None) -> dict[str, Any]:
|
| 346 |
+
payload = {key: value for key, value in {"prefix": username, "domain": self.default_domain}.items() if value}
|
| 347 |
+
data = self._request("POST" if payload else "GET", "/api/generate-email", payload=payload or None)
|
| 348 |
+
return {"provider": self.name, "provider_ref": self.provider_ref, "address": str(data["email"])}
|
| 349 |
+
|
| 350 |
+
def fetch_latest_message(self, mailbox: dict[str, Any]) -> dict[str, Any] | None:
|
| 351 |
+
data = self._request("GET", "/api/emails", params={"email": mailbox["address"]})
|
| 352 |
+
emails = data if isinstance(data, list) else data.get("emails") or []
|
| 353 |
+
if not emails:
|
| 354 |
+
return None
|
| 355 |
+
item = max(emails, key=lambda value: (float(value.get("timestamp") or 0), str(value.get("id") or "")))
|
| 356 |
+
if item.get("id"):
|
| 357 |
+
item = self._request("GET", f"/api/email/{item['id']}")
|
| 358 |
+
return {"provider": self.name, "mailbox": mailbox["address"], "message_id": str(item.get("id") or ""), "subject": str(item.get("subject") or ""), "sender": str(item.get("from_address") or ""), "text_content": str(item.get("content") or ""), "html_content": str(item.get("html_content") or ""), "received_at": _parse_received_at(item.get("timestamp") or item.get("created_at")), "raw": item}
|
| 359 |
+
|
| 360 |
+
def close(self) -> None:
|
| 361 |
+
self.session.close()
|
| 362 |
+
|
| 363 |
+
|
| 364 |
+
def _entries(mail_config: dict) -> list[dict]:
|
| 365 |
+
return [{**item, "provider_ref": f"{item['type']}#{index + 1}"} for index, item in enumerate(mail_config["providers"])]
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def _enabled_entries(mail_config: dict) -> list[dict]:
|
| 369 |
+
items = [item for item in _entries(mail_config) if item.get("enable")]
|
| 370 |
+
if not items:
|
| 371 |
+
raise RuntimeError("mail.providers ๆฒกๆๅฏ็จ็ provider")
|
| 372 |
+
return items
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
def _next_entry(mail_config: dict) -> dict:
|
| 376 |
+
global provider_index
|
| 377 |
+
items = _enabled_entries(mail_config)
|
| 378 |
+
if len(items) == 1:
|
| 379 |
+
return dict(items[0])
|
| 380 |
+
with provider_lock:
|
| 381 |
+
value = dict(items[provider_index % len(items)])
|
| 382 |
+
provider_index = (provider_index + 1) % len(items)
|
| 383 |
+
return value
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def _create_provider(mail_config: dict, provider: str = "", provider_ref: str = "") -> BaseMailProvider:
|
| 387 |
+
entry = next((dict(item) for item in _entries(mail_config) if provider_ref and item["provider_ref"] == provider_ref), None)
|
| 388 |
+
entry = entry or next((dict(item) for item in _enabled_entries(mail_config) if provider and item["type"] == provider), None) or _next_entry(mail_config)
|
| 389 |
+
conf = _config(mail_config)
|
| 390 |
+
if entry["type"] == "cloudflare_temp_email":
|
| 391 |
+
return CloudflareTempMailProvider(entry, conf)
|
| 392 |
+
if entry["type"] == "tempmail_lol":
|
| 393 |
+
return TempMailLolProvider(entry, conf)
|
| 394 |
+
if entry["type"] == "duckmail":
|
| 395 |
+
return DuckMailProvider(entry, conf)
|
| 396 |
+
if entry["type"] == "gptmail":
|
| 397 |
+
return GptMailProvider(entry, conf)
|
| 398 |
+
raise RuntimeError(f"ไธๆฏๆ็ mail.provider: {entry['type']}")
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def create_mailbox(mail_config: dict, username: str | None = None) -> dict:
|
| 402 |
+
provider = _create_provider(mail_config)
|
| 403 |
+
try:
|
| 404 |
+
return provider.create_mailbox(username)
|
| 405 |
+
finally:
|
| 406 |
+
provider.close()
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
def wait_for_code(mail_config: dict, mailbox: dict) -> str | None:
|
| 410 |
+
provider = _create_provider(mail_config, str(mailbox.get("provider") or ""), str(mailbox.get("provider_ref") or ""))
|
| 411 |
+
try:
|
| 412 |
+
return provider.wait_for_code(mailbox)
|
| 413 |
+
finally:
|
| 414 |
+
provider.close()
|
services/register/openai_register.py
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import base64
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
import random
|
| 7 |
+
import secrets
|
| 8 |
+
import string
|
| 9 |
+
import threading
|
| 10 |
+
import time
|
| 11 |
+
import uuid
|
| 12 |
+
from datetime import datetime, timezone
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
from urllib.parse import parse_qs, urlencode, urlparse
|
| 15 |
+
|
| 16 |
+
import requests
|
| 17 |
+
import urllib3
|
| 18 |
+
from requests.adapters import HTTPAdapter
|
| 19 |
+
from urllib3.util.retry import Retry
|
| 20 |
+
|
| 21 |
+
from services.account_service import account_service
|
| 22 |
+
from services.register import mail_provider
|
| 23 |
+
|
| 24 |
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
| 25 |
+
base_dir = Path(__file__).resolve().parent
|
| 26 |
+
config = {
|
| 27 |
+
"mail": {
|
| 28 |
+
"request_timeout": 15,
|
| 29 |
+
"wait_timeout": 30,
|
| 30 |
+
"wait_interval": 3,
|
| 31 |
+
"providers": [],
|
| 32 |
+
},
|
| 33 |
+
"proxy": "",
|
| 34 |
+
"total": 20000,
|
| 35 |
+
"threads": 64,
|
| 36 |
+
}
|
| 37 |
+
register_config_file = base_dir.parents[1] / "data" / "register.json"
|
| 38 |
+
try:
|
| 39 |
+
saved_config = json.loads(register_config_file.read_text(encoding="utf-8"))
|
| 40 |
+
config.update({key: saved_config[key] for key in ("mail", "proxy", "total", "threads") if key in saved_config})
|
| 41 |
+
except Exception:
|
| 42 |
+
pass
|
| 43 |
+
|
| 44 |
+
auth_base = "https://auth.openai.com"
|
| 45 |
+
platform_base = "https://platform.openai.com"
|
| 46 |
+
platform_oauth_client_id = "app_2SKx67EdpoN0G6j64rFvigXD"
|
| 47 |
+
platform_oauth_redirect_uri = f"{platform_base}/auth/callback"
|
| 48 |
+
platform_oauth_audience = "https://api.openai.com/v1"
|
| 49 |
+
platform_auth0_client = "eyJuYW1lIjoiYXV0aDAtc3BhLWpzIiwidmVyc2lvbiI6IjEuMjEuMCJ9"
|
| 50 |
+
user_agent = (
|
| 51 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
| 52 |
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
| 53 |
+
"Chrome/145.0.0.0 Safari/537.36"
|
| 54 |
+
)
|
| 55 |
+
sec_ch_ua = '"Google Chrome";v="145", "Not?A_Brand";v="8", "Chromium";v="145"'
|
| 56 |
+
sec_ch_ua_full_version_list = '"Chromium";v="145.0.0.0", "Not:A-Brand";v="99.0.0.0", "Google Chrome";v="145.0.0.0"'
|
| 57 |
+
default_timeout = 30
|
| 58 |
+
print_lock = threading.Lock()
|
| 59 |
+
stats_lock = threading.Lock()
|
| 60 |
+
stats = {"done": 0, "success": 0, "fail": 0, "start_time": 0.0}
|
| 61 |
+
register_log_sink = None
|
| 62 |
+
|
| 63 |
+
common_headers = {
|
| 64 |
+
"accept": "application/json",
|
| 65 |
+
"accept-language": "en-US,en;q=0.9",
|
| 66 |
+
"content-type": "application/json",
|
| 67 |
+
"origin": auth_base,
|
| 68 |
+
"priority": "u=1, i",
|
| 69 |
+
"user-agent": user_agent,
|
| 70 |
+
"sec-ch-ua": sec_ch_ua,
|
| 71 |
+
"sec-ch-ua-arch": '"x86_64"',
|
| 72 |
+
"sec-ch-ua-bitness": '"64"',
|
| 73 |
+
"sec-ch-ua-full-version-list": sec_ch_ua_full_version_list,
|
| 74 |
+
"sec-ch-ua-mobile": "?0",
|
| 75 |
+
"sec-ch-ua-model": '""',
|
| 76 |
+
"sec-ch-ua-platform": '"Windows"',
|
| 77 |
+
"sec-ch-ua-platform-version": '"10.0.0"',
|
| 78 |
+
"sec-fetch-dest": "empty",
|
| 79 |
+
"sec-fetch-mode": "cors",
|
| 80 |
+
"sec-fetch-site": "same-origin",
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
navigate_headers = {
|
| 84 |
+
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
| 85 |
+
"accept-language": "en-US,en;q=0.9",
|
| 86 |
+
"user-agent": user_agent,
|
| 87 |
+
"sec-ch-ua": sec_ch_ua,
|
| 88 |
+
"sec-ch-ua-arch": '"x86_64"',
|
| 89 |
+
"sec-ch-ua-bitness": '"64"',
|
| 90 |
+
"sec-ch-ua-full-version-list": sec_ch_ua_full_version_list,
|
| 91 |
+
"sec-ch-ua-mobile": "?0",
|
| 92 |
+
"sec-ch-ua-model": '""',
|
| 93 |
+
"sec-ch-ua-platform": '"Windows"',
|
| 94 |
+
"sec-ch-ua-platform-version": '"10.0.0"',
|
| 95 |
+
"sec-fetch-dest": "document",
|
| 96 |
+
"sec-fetch-mode": "navigate",
|
| 97 |
+
"sec-fetch-site": "same-origin",
|
| 98 |
+
"sec-fetch-user": "?1",
|
| 99 |
+
"upgrade-insecure-requests": "1",
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def log(text: str, color: str = "") -> None:
|
| 104 |
+
colors = {"red": "\033[31m", "green": "\033[32m", "yellow": "\033[33m"}
|
| 105 |
+
if register_log_sink:
|
| 106 |
+
try:
|
| 107 |
+
register_log_sink(text, color)
|
| 108 |
+
except Exception:
|
| 109 |
+
pass
|
| 110 |
+
with print_lock:
|
| 111 |
+
prefix = colors.get(color, "")
|
| 112 |
+
suffix = "\033[0m" if prefix else ""
|
| 113 |
+
print(f"{prefix}{datetime.now().strftime('%H:%M:%S')} {text}{suffix}")
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def step(index: int, text: str, color: str = "") -> None:
|
| 117 |
+
log(f"[ไปปๅก{index}] {text}", color)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _make_trace_headers() -> dict[str, str]:
|
| 121 |
+
trace_id = str(random.getrandbits(64))
|
| 122 |
+
parent_id = str(random.getrandbits(64))
|
| 123 |
+
return {
|
| 124 |
+
"traceparent": f"00-{uuid.uuid4().hex}-{format(int(parent_id), '016x')}-01",
|
| 125 |
+
"tracestate": "dd=s:1;o:rum",
|
| 126 |
+
"x-datadog-origin": "rum",
|
| 127 |
+
"x-datadog-parent-id": parent_id,
|
| 128 |
+
"x-datadog-sampling-priority": "1",
|
| 129 |
+
"x-datadog-trace-id": trace_id,
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
def _generate_pkce() -> tuple[str, str]:
|
| 134 |
+
code_verifier = base64.urlsafe_b64encode(secrets.token_bytes(64)).rstrip(b"=").decode("ascii")
|
| 135 |
+
code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode("ascii")).digest()).rstrip(b"=").decode("ascii")
|
| 136 |
+
return code_verifier, code_challenge
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _random_password(length: int = 16) -> str:
|
| 140 |
+
chars = string.ascii_letters + string.digits + "!@#$%"
|
| 141 |
+
value = list(
|
| 142 |
+
secrets.choice(string.ascii_uppercase)
|
| 143 |
+
+ secrets.choice(string.ascii_lowercase)
|
| 144 |
+
+ secrets.choice(string.digits)
|
| 145 |
+
+ secrets.choice("!@#$%")
|
| 146 |
+
+ "".join(secrets.choice(chars) for _ in range(max(0, length - 4)))
|
| 147 |
+
)
|
| 148 |
+
random.shuffle(value)
|
| 149 |
+
return "".join(value)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def _random_name() -> tuple[str, str]:
|
| 153 |
+
return random.choice(["James", "Robert", "John", "Michael", "David", "Mary", "Emma", "Olivia"]), random.choice(
|
| 154 |
+
["Smith", "Johnson", "Williams", "Brown", "Jones", "Garcia", "Miller"]
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def _random_birthdate() -> str:
|
| 159 |
+
return f"{random.randint(1996, 2006):04d}-{random.randint(1, 12):02d}-{random.randint(1, 28):02d}"
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
def _response_json(resp) -> dict:
|
| 163 |
+
try:
|
| 164 |
+
data = resp.json()
|
| 165 |
+
return data if isinstance(data, dict) else {}
|
| 166 |
+
except Exception:
|
| 167 |
+
return {}
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def _decode_jwt_payload(token: str) -> dict:
|
| 171 |
+
try:
|
| 172 |
+
payload = token.split(".")[1]
|
| 173 |
+
padding = 4 - len(payload) % 4
|
| 174 |
+
if padding != 4:
|
| 175 |
+
payload += "=" * padding
|
| 176 |
+
return json.loads(base64.urlsafe_b64decode(payload))
|
| 177 |
+
except Exception:
|
| 178 |
+
return {}
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def create_mailbox(username: str | None = None) -> dict:
|
| 182 |
+
return mail_provider.create_mailbox(config["mail"], username)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def wait_for_code(mailbox: dict) -> str | None:
|
| 186 |
+
return mail_provider.wait_for_code(config["mail"], mailbox)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
class SentinelTokenGenerator:
|
| 190 |
+
MAX_ATTEMPTS = 500000
|
| 191 |
+
ERROR_PREFIX = "wQ8Lk5FbGpA2NcR9dShT6gYjU7VxZ4D"
|
| 192 |
+
|
| 193 |
+
def __init__(self, device_id: str, ua: str):
|
| 194 |
+
self.device_id = device_id
|
| 195 |
+
self.user_agent = ua
|
| 196 |
+
self.sid = str(uuid.uuid4())
|
| 197 |
+
|
| 198 |
+
@staticmethod
|
| 199 |
+
def _fnv1a_32(text: str) -> str:
|
| 200 |
+
h = 2166136261
|
| 201 |
+
for ch in text:
|
| 202 |
+
h ^= ord(ch)
|
| 203 |
+
h = (h * 16777619) & 0xFFFFFFFF
|
| 204 |
+
h ^= h >> 16
|
| 205 |
+
h = (h * 2246822507) & 0xFFFFFFFF
|
| 206 |
+
h ^= h >> 13
|
| 207 |
+
h = (h * 3266489909) & 0xFFFFFFFF
|
| 208 |
+
h ^= h >> 16
|
| 209 |
+
return format(h & 0xFFFFFFFF, "08x")
|
| 210 |
+
|
| 211 |
+
def _get_config(self) -> list:
|
| 212 |
+
perf_now = random.uniform(1000, 50000)
|
| 213 |
+
return [
|
| 214 |
+
"1920x1080",
|
| 215 |
+
time.strftime("%a %b %d %Y %H:%M:%S GMT+0000 (Coordinated Universal Time)", time.gmtime()),
|
| 216 |
+
4294705152,
|
| 217 |
+
random.random(),
|
| 218 |
+
self.user_agent,
|
| 219 |
+
"https://sentinel.openai.com/sentinel/20260124ceb8/sdk.js",
|
| 220 |
+
None,
|
| 221 |
+
None,
|
| 222 |
+
"en-US",
|
| 223 |
+
random.random(),
|
| 224 |
+
random.choice(["vendorSub-undefined", "plugins-undefined", "mimeTypes-undefined", "hardwareConcurrency-undefined"]),
|
| 225 |
+
random.choice(["location", "implementation", "URL", "documentURI", "compatMode"]),
|
| 226 |
+
random.choice(["Object", "Function", "Array", "Number", "parseFloat", "undefined"]),
|
| 227 |
+
perf_now,
|
| 228 |
+
self.sid,
|
| 229 |
+
"",
|
| 230 |
+
random.choice([4, 8, 12, 16]),
|
| 231 |
+
time.time() * 1000 - perf_now,
|
| 232 |
+
]
|
| 233 |
+
|
| 234 |
+
@staticmethod
|
| 235 |
+
def _b64(data) -> str:
|
| 236 |
+
return base64.b64encode(json.dumps(data, separators=(",", ":"), ensure_ascii=False).encode("utf-8")).decode("ascii")
|
| 237 |
+
|
| 238 |
+
def generate_requirements_token(self) -> str:
|
| 239 |
+
data = self._get_config()
|
| 240 |
+
data[3] = 1
|
| 241 |
+
data[9] = round(random.uniform(5, 50))
|
| 242 |
+
return "gAAAAAC" + self._b64(data)
|
| 243 |
+
|
| 244 |
+
def generate_token(self, seed: str, difficulty: str) -> str:
|
| 245 |
+
start = time.time()
|
| 246 |
+
data = self._get_config()
|
| 247 |
+
difficulty = str(difficulty or "0")
|
| 248 |
+
for i in range(self.MAX_ATTEMPTS):
|
| 249 |
+
data[3] = i
|
| 250 |
+
data[9] = round((time.time() - start) * 1000)
|
| 251 |
+
payload = self._b64(data)
|
| 252 |
+
if self._fnv1a_32(seed + payload)[: len(difficulty)] <= difficulty:
|
| 253 |
+
return "gAAAAAB" + payload + "~S"
|
| 254 |
+
return "gAAAAAB" + self.ERROR_PREFIX + self._b64(str(None))
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
def build_sentinel_token(session: requests.Session, device_id: str, flow: str) -> str:
|
| 258 |
+
generator = SentinelTokenGenerator(device_id, user_agent)
|
| 259 |
+
resp = session.post(
|
| 260 |
+
"https://sentinel.openai.com/backend-api/sentinel/req",
|
| 261 |
+
data=json.dumps({"p": generator.generate_requirements_token(), "id": device_id, "flow": flow}),
|
| 262 |
+
headers={
|
| 263 |
+
"Content-Type": "text/plain;charset=UTF-8",
|
| 264 |
+
"Referer": "https://sentinel.openai.com/backend-api/sentinel/frame.html",
|
| 265 |
+
"Origin": "https://sentinel.openai.com",
|
| 266 |
+
"User-Agent": user_agent,
|
| 267 |
+
"sec-ch-ua": sec_ch_ua,
|
| 268 |
+
"sec-ch-ua-mobile": "?0",
|
| 269 |
+
"sec-ch-ua-platform": '"Windows"',
|
| 270 |
+
},
|
| 271 |
+
timeout=20,
|
| 272 |
+
verify=False,
|
| 273 |
+
)
|
| 274 |
+
data = _response_json(resp)
|
| 275 |
+
token = str(data.get("token") or "").strip()
|
| 276 |
+
if resp.status_code != 200 or not token:
|
| 277 |
+
raise RuntimeError(f"sentinel_req_failed_{resp.status_code}")
|
| 278 |
+
pow_data = data.get("proofofwork") or {}
|
| 279 |
+
p_value = (
|
| 280 |
+
generator.generate_token(str(pow_data.get("seed") or ""), str(pow_data.get("difficulty") or "0"))
|
| 281 |
+
if pow_data.get("required") and pow_data.get("seed")
|
| 282 |
+
else generator.generate_requirements_token()
|
| 283 |
+
)
|
| 284 |
+
return json.dumps({"p": p_value, "t": "", "c": token, "id": device_id, "flow": flow}, separators=(",", ":"))
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def create_session(proxy: str = "") -> requests.Session:
|
| 288 |
+
session = requests.Session()
|
| 289 |
+
retry = Retry(total=2, connect=2, read=2, backoff_factor=0.5, status_forcelist=(429, 500, 502, 503, 504))
|
| 290 |
+
adapter = HTTPAdapter(max_retries=retry, pool_connections=50, pool_maxsize=50)
|
| 291 |
+
session.mount("http://", adapter)
|
| 292 |
+
session.mount("https://", adapter)
|
| 293 |
+
session.verify = False
|
| 294 |
+
if proxy:
|
| 295 |
+
session.proxies.update({"http": proxy, "https": proxy})
|
| 296 |
+
return session
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def request_with_local_retry(session: requests.Session, method: str, url: str, retry_attempts: int = 3, **kwargs):
|
| 300 |
+
last_error = ""
|
| 301 |
+
for _ in range(max(1, retry_attempts)):
|
| 302 |
+
try:
|
| 303 |
+
return session.request(method.upper(), url, timeout=default_timeout, **kwargs), ""
|
| 304 |
+
except Exception as error:
|
| 305 |
+
last_error = str(error)
|
| 306 |
+
time.sleep(1)
|
| 307 |
+
return None, last_error
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def validate_otp(session: requests.Session, device_id: str, code: str):
|
| 311 |
+
headers = dict(common_headers)
|
| 312 |
+
headers["referer"] = f"{auth_base}/email-verification"
|
| 313 |
+
headers["oai-device-id"] = device_id
|
| 314 |
+
headers.update(_make_trace_headers())
|
| 315 |
+
resp, error = request_with_local_retry(session, "post", f"{auth_base}/api/accounts/email-otp/validate", json={"code": code}, headers=headers, verify=False)
|
| 316 |
+
if resp is not None and resp.status_code == 200:
|
| 317 |
+
return resp, ""
|
| 318 |
+
headers["openai-sentinel-token"] = build_sentinel_token(session, device_id, "authorize_continue")
|
| 319 |
+
resp, error = request_with_local_retry(session, "post", f"{auth_base}/api/accounts/email-otp/validate", json={"code": code}, headers=headers, verify=False)
|
| 320 |
+
return resp, error
|
| 321 |
+
|
| 322 |
+
|
| 323 |
+
def extract_oauth_callback_params_from_url(url: str) -> dict[str, str] | None:
|
| 324 |
+
if not url:
|
| 325 |
+
return None
|
| 326 |
+
try:
|
| 327 |
+
params = parse_qs(urlparse(url).query)
|
| 328 |
+
except Exception:
|
| 329 |
+
return None
|
| 330 |
+
code = str((params.get("code") or [""])[0]).strip()
|
| 331 |
+
if not code:
|
| 332 |
+
return None
|
| 333 |
+
return {"code": code, "state": str((params.get("state") or [""])[0]).strip(), "scope": str((params.get("scope") or [""])[0]).strip()}
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def extract_oauth_callback_params_from_consent_session(session: requests.Session, consent_url: str, device_id: str) -> dict[str, str] | None:
|
| 337 |
+
if consent_url.startswith("/"):
|
| 338 |
+
consent_url = f"{auth_base}{consent_url}"
|
| 339 |
+
current_url = consent_url
|
| 340 |
+
for _ in range(10):
|
| 341 |
+
response = session.get(current_url, headers=navigate_headers, verify=False, timeout=30, allow_redirects=False)
|
| 342 |
+
callback_params = extract_oauth_callback_params_from_url(str(response.url)) or extract_oauth_callback_params_from_url(str(response.headers.get("Location") or "").strip())
|
| 343 |
+
if callback_params:
|
| 344 |
+
return callback_params
|
| 345 |
+
location = str(response.headers.get("Location") or "").strip()
|
| 346 |
+
if response.status_code not in (301, 302, 303, 307, 308) or not location:
|
| 347 |
+
break
|
| 348 |
+
current_url = f"{auth_base}{location}" if location.startswith("/") else location
|
| 349 |
+
raw = session.cookies.get("oai-client-auth-session", domain=".auth.openai.com") or session.cookies.get("oai-client-auth-session")
|
| 350 |
+
if not raw:
|
| 351 |
+
return None
|
| 352 |
+
try:
|
| 353 |
+
first_part = raw.split(".")[0]
|
| 354 |
+
padding = 4 - len(first_part) % 4
|
| 355 |
+
if padding != 4:
|
| 356 |
+
first_part += "=" * padding
|
| 357 |
+
payload = json.loads(base64.urlsafe_b64decode(first_part))
|
| 358 |
+
workspace_id = payload["workspaces"][0]["id"]
|
| 359 |
+
except Exception:
|
| 360 |
+
return None
|
| 361 |
+
headers = dict(common_headers)
|
| 362 |
+
headers["referer"] = consent_url
|
| 363 |
+
headers["oai-device-id"] = device_id
|
| 364 |
+
headers.update(_make_trace_headers())
|
| 365 |
+
ws_resp = session.post(f"{auth_base}/api/accounts/workspace/select", json={"workspace_id": workspace_id}, headers=headers, verify=False, timeout=30, allow_redirects=False)
|
| 366 |
+
callback_params = extract_oauth_callback_params_from_url(str(ws_resp.headers.get("Location") or "").strip())
|
| 367 |
+
if callback_params:
|
| 368 |
+
return callback_params
|
| 369 |
+
ws_data = _response_json(ws_resp)
|
| 370 |
+
orgs = ((ws_data.get("data") or {}).get("orgs") or []) if isinstance(ws_data, dict) else []
|
| 371 |
+
if not orgs:
|
| 372 |
+
return None
|
| 373 |
+
org_id = str((orgs[0] or {}).get("id") or "").strip()
|
| 374 |
+
project_id = str(((orgs[0] or {}).get("projects") or [{}])[0].get("id") or "").strip()
|
| 375 |
+
if not org_id:
|
| 376 |
+
return None
|
| 377 |
+
org_headers = dict(common_headers)
|
| 378 |
+
org_headers["referer"] = str(ws_data.get("continue_url") or consent_url)
|
| 379 |
+
org_headers["oai-device-id"] = device_id
|
| 380 |
+
org_headers.update(_make_trace_headers())
|
| 381 |
+
body = {"org_id": org_id}
|
| 382 |
+
if project_id:
|
| 383 |
+
body["project_id"] = project_id
|
| 384 |
+
org_resp = session.post(f"{auth_base}/api/accounts/organization/select", json=body, headers=org_headers, verify=False, timeout=30, allow_redirects=False)
|
| 385 |
+
return extract_oauth_callback_params_from_url(str(org_resp.headers.get("Location") or "").strip())
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
def exchange_platform_tokens(session: requests.Session, device_id: str, code_verifier: str, consent_url: str) -> dict | None:
|
| 389 |
+
callback_params = extract_oauth_callback_params_from_consent_session(session, consent_url, device_id)
|
| 390 |
+
if not callback_params:
|
| 391 |
+
return None
|
| 392 |
+
code = str(callback_params.get("code") or "").strip()
|
| 393 |
+
if not code:
|
| 394 |
+
return None
|
| 395 |
+
resp = create_session(config["proxy"]).post(
|
| 396 |
+
f"{auth_base}/oauth/token",
|
| 397 |
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
| 398 |
+
data={
|
| 399 |
+
"grant_type": "authorization_code",
|
| 400 |
+
"code": code,
|
| 401 |
+
"redirect_uri": platform_oauth_redirect_uri,
|
| 402 |
+
"client_id": platform_oauth_client_id,
|
| 403 |
+
"code_verifier": code_verifier,
|
| 404 |
+
},
|
| 405 |
+
verify=False,
|
| 406 |
+
timeout=60,
|
| 407 |
+
)
|
| 408 |
+
data = _response_json(resp)
|
| 409 |
+
if resp.status_code != 200 or not data.get("access_token") or not data.get("refresh_token") or not data.get("id_token"):
|
| 410 |
+
return None
|
| 411 |
+
payload = _decode_jwt_payload(str(data.get("id_token") or "")) or _decode_jwt_payload(str(data.get("access_token") or ""))
|
| 412 |
+
return {
|
| 413 |
+
"email": str(payload.get("email") or "").strip(),
|
| 414 |
+
"access_token": str(data.get("access_token") or "").strip(),
|
| 415 |
+
"refresh_token": str(data.get("refresh_token") or "").strip(),
|
| 416 |
+
"id_token": str(data.get("id_token") or "").strip(),
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
|
| 420 |
+
class PlatformRegistrar:
|
| 421 |
+
def __init__(self, proxy: str = "") -> None:
|
| 422 |
+
self.session = create_session(proxy)
|
| 423 |
+
self.device_id = str(uuid.uuid4())
|
| 424 |
+
|
| 425 |
+
def close(self) -> None:
|
| 426 |
+
self.session.close()
|
| 427 |
+
|
| 428 |
+
def _navigate_headers(self, referer: str = "") -> dict[str, str]:
|
| 429 |
+
headers = dict(navigate_headers)
|
| 430 |
+
if referer:
|
| 431 |
+
headers["referer"] = referer
|
| 432 |
+
return headers
|
| 433 |
+
|
| 434 |
+
def _json_headers(self, referer: str) -> dict[str, str]:
|
| 435 |
+
headers = dict(common_headers)
|
| 436 |
+
headers["referer"] = referer
|
| 437 |
+
headers["oai-device-id"] = self.device_id
|
| 438 |
+
headers.update(_make_trace_headers())
|
| 439 |
+
return headers
|
| 440 |
+
|
| 441 |
+
def _platform_authorize(self, email: str, index: int) -> None:
|
| 442 |
+
step(index, "ๅผๅง platform authorize")
|
| 443 |
+
self.session.cookies.set("oai-did", self.device_id, domain=".auth.openai.com")
|
| 444 |
+
self.session.cookies.set("oai-did", self.device_id, domain="auth.openai.com")
|
| 445 |
+
_, code_challenge = _generate_pkce()
|
| 446 |
+
params = {
|
| 447 |
+
"issuer": auth_base,
|
| 448 |
+
"client_id": platform_oauth_client_id,
|
| 449 |
+
"audience": platform_oauth_audience,
|
| 450 |
+
"redirect_uri": platform_oauth_redirect_uri,
|
| 451 |
+
"device_id": self.device_id,
|
| 452 |
+
"screen_hint": "login_or_signup",
|
| 453 |
+
"max_age": "0",
|
| 454 |
+
"login_hint": email,
|
| 455 |
+
"scope": "openid profile email offline_access",
|
| 456 |
+
"response_type": "code",
|
| 457 |
+
"response_mode": "query",
|
| 458 |
+
"state": secrets.token_urlsafe(32),
|
| 459 |
+
"nonce": secrets.token_urlsafe(32),
|
| 460 |
+
"code_challenge": code_challenge,
|
| 461 |
+
"code_challenge_method": "S256",
|
| 462 |
+
"auth0Client": platform_auth0_client,
|
| 463 |
+
}
|
| 464 |
+
resp, error = request_with_local_retry(self.session, "get", f"{auth_base}/api/accounts/authorize?{urlencode(params)}", headers=self._navigate_headers(f"{platform_base}/"), allow_redirects=True, verify=False)
|
| 465 |
+
if resp is None or resp.status_code != 200:
|
| 466 |
+
raise RuntimeError(error or f"platform_authorize_http_{getattr(resp, 'status_code', 'unknown')}")
|
| 467 |
+
step(index, "platform authorize ๅฎๆ")
|
| 468 |
+
|
| 469 |
+
def _register_user(self, email: str, password: str, index: int) -> None:
|
| 470 |
+
step(index, "ๅผๅงๆไบคๆณจๅๅฏ็ ")
|
| 471 |
+
headers = self._json_headers(f"{auth_base}/create-account/password")
|
| 472 |
+
headers["openai-sentinel-token"] = build_sentinel_token(self.session, self.device_id, "username_password_create")
|
| 473 |
+
resp, error = request_with_local_retry(self.session, "post", f"{auth_base}/api/accounts/user/register", json={"username": email, "password": password}, headers=headers, verify=False)
|
| 474 |
+
if resp is None or resp.status_code != 200:
|
| 475 |
+
raise RuntimeError(error or f"user_register_http_{getattr(resp, 'status_code', 'unknown')}")
|
| 476 |
+
step(index, "ๆไบคๆณจๅๅฏ็ ๅฎๆ")
|
| 477 |
+
|
| 478 |
+
def _send_otp(self, index: int) -> None:
|
| 479 |
+
step(index, "ๅผๅงๅ้้ช่ฏ็ ")
|
| 480 |
+
resp, error = request_with_local_retry(self.session, "get", f"{auth_base}/api/accounts/email-otp/send", headers=self._navigate_headers(f"{auth_base}/create-account/password"), allow_redirects=True, verify=False)
|
| 481 |
+
if resp is None or resp.status_code not in (200, 302):
|
| 482 |
+
raise RuntimeError(error or f"send_otp_http_{getattr(resp, 'status_code', 'unknown')}")
|
| 483 |
+
step(index, "ๅ้้ช่ฏ็ ๅฎๆ")
|
| 484 |
+
|
| 485 |
+
def _validate_otp(self, code: str, index: int) -> None:
|
| 486 |
+
step(index, f"ๅผๅงๆ ก้ช้ช่ฏ็ {code}")
|
| 487 |
+
resp, error = validate_otp(self.session, self.device_id, code)
|
| 488 |
+
if resp is None or resp.status_code != 200:
|
| 489 |
+
raise RuntimeError(error or f"validate_otp_http_{getattr(resp, 'status_code', 'unknown')}")
|
| 490 |
+
step(index, "้ช่ฏ็ ๆ ก้ชๅฎๆ")
|
| 491 |
+
|
| 492 |
+
def _create_account(self, name: str, birthdate: str, index: int) -> None:
|
| 493 |
+
step(index, "ๅผๅงๅๅปบ่ดฆๅท่ตๆ")
|
| 494 |
+
headers = self._json_headers(f"{auth_base}/about-you")
|
| 495 |
+
headers["openai-sentinel-token"] = build_sentinel_token(self.session, self.device_id, "oauth_create_account")
|
| 496 |
+
resp, error = request_with_local_retry(self.session, "post", f"{auth_base}/api/accounts/create_account", json={"name": name, "birthdate": birthdate}, headers=headers, verify=False)
|
| 497 |
+
if resp is None or resp.status_code not in (200, 302):
|
| 498 |
+
raise RuntimeError(error or f"create_account_http_{getattr(resp, 'status_code', 'unknown')}")
|
| 499 |
+
step(index, "ๅๅปบ่ดฆๅท่ตๆๅฎๆ")
|
| 500 |
+
|
| 501 |
+
def _login_and_exchange_tokens(self, email: str, password: str, mailbox: dict, index: int) -> dict:
|
| 502 |
+
step(index, "ๅผๅง็ฌ็ซ็ปๅฝๆข token")
|
| 503 |
+
code_verifier, code_challenge = _generate_pkce()
|
| 504 |
+
params = {
|
| 505 |
+
"issuer": auth_base,
|
| 506 |
+
"client_id": platform_oauth_client_id,
|
| 507 |
+
"audience": platform_oauth_audience,
|
| 508 |
+
"redirect_uri": platform_oauth_redirect_uri,
|
| 509 |
+
"device_id": self.device_id,
|
| 510 |
+
"screen_hint": "login_or_signup",
|
| 511 |
+
"max_age": "0",
|
| 512 |
+
"login_hint": email,
|
| 513 |
+
"scope": "openid profile email offline_access",
|
| 514 |
+
"response_type": "code",
|
| 515 |
+
"response_mode": "query",
|
| 516 |
+
"state": secrets.token_urlsafe(32),
|
| 517 |
+
"nonce": secrets.token_urlsafe(32),
|
| 518 |
+
"code_challenge": code_challenge,
|
| 519 |
+
"code_challenge_method": "S256",
|
| 520 |
+
"auth0Client": platform_auth0_client,
|
| 521 |
+
}
|
| 522 |
+
resp, error = request_with_local_retry(self.session, "get", f"{auth_base}/api/accounts/authorize?{urlencode(params)}", headers=self._navigate_headers(f"{platform_base}/"), allow_redirects=True, verify=False)
|
| 523 |
+
if resp is None:
|
| 524 |
+
raise RuntimeError(error or "platform_login_authorize_failed")
|
| 525 |
+
step(index, "็ปๅฝ authorize ๅฎๆ")
|
| 526 |
+
headers = self._json_headers(f"{auth_base}/log-in/password")
|
| 527 |
+
headers["openai-sentinel-token"] = build_sentinel_token(self.session, self.device_id, "password_verify")
|
| 528 |
+
resp, error = request_with_local_retry(self.session, "post", f"{auth_base}/api/accounts/password/verify", json={"password": password}, headers=headers, allow_redirects=False, verify=False)
|
| 529 |
+
if resp is None or resp.status_code != 200:
|
| 530 |
+
raise RuntimeError(error or f"password_verify_http_{getattr(resp, 'status_code', 'unknown')}")
|
| 531 |
+
step(index, "ๅฏ็ ๆ ก้ชๅฎๆ")
|
| 532 |
+
payload = _response_json(resp)
|
| 533 |
+
continue_url = str(payload.get("continue_url") or "").strip()
|
| 534 |
+
page_type = str(((payload.get("page") or {}).get("type")) or "")
|
| 535 |
+
if page_type == "email_otp_verification" or "email-verification" in continue_url or "email-otp" in continue_url:
|
| 536 |
+
step(index, "็ฌ็ซ็ปๅฝ้่ฆ้ฎ็ฎฑ้ช่ฏ็ ")
|
| 537 |
+
code = wait_for_code(mailbox)
|
| 538 |
+
if not code:
|
| 539 |
+
raise RuntimeError("็ฌ็ซ็ปๅฝ็ญๅพ
้ช่ฏ็ ่ถ
ๆถ")
|
| 540 |
+
resp, reason = validate_otp(self.session, self.device_id, code)
|
| 541 |
+
if resp is None or resp.status_code != 200:
|
| 542 |
+
raise RuntimeError(reason or "็ฌ็ซ็ปๅฝ้ช่ฏ็ ๆ ก้ชๅคฑ่ดฅ")
|
| 543 |
+
otp_payload = _response_json(resp)
|
| 544 |
+
continue_url = str(otp_payload.get("continue_url") or continue_url).strip()
|
| 545 |
+
step(index, "็ฌ็ซ็ปๅฝ้ช่ฏ็ ๆ ก้ชๅฎๆ")
|
| 546 |
+
if not continue_url:
|
| 547 |
+
continue_url = f"{auth_base}/sign-in-with-chatgpt/codex/consent"
|
| 548 |
+
tokens = exchange_platform_tokens(self.session, self.device_id, code_verifier, continue_url)
|
| 549 |
+
if not tokens:
|
| 550 |
+
raise RuntimeError("tokenๆขๅๅคฑ่ดฅ")
|
| 551 |
+
step(index, "token ๆขๅๅฎๆ")
|
| 552 |
+
return tokens
|
| 553 |
+
|
| 554 |
+
def register(self, index: int) -> dict:
|
| 555 |
+
step(index, "ๅผๅงๅๅปบ้ฎ็ฎฑ")
|
| 556 |
+
mailbox = create_mailbox()
|
| 557 |
+
email = str(mailbox.get("address") or "").strip()
|
| 558 |
+
if not email:
|
| 559 |
+
raise RuntimeError("้ฎ็ฎฑๆๅกๆช่ฟๅ address")
|
| 560 |
+
step(index, f"้ฎ็ฎฑๅๅปบๅฎๆ: {email}")
|
| 561 |
+
password = _random_password()
|
| 562 |
+
first_name, last_name = _random_name()
|
| 563 |
+
self._platform_authorize(email, index)
|
| 564 |
+
self._register_user(email, password, index)
|
| 565 |
+
self._send_otp(index)
|
| 566 |
+
step(index, "ๅผๅง็ญๅพ
ๆณจๅ้ช่ฏ็ ")
|
| 567 |
+
code = wait_for_code(mailbox)
|
| 568 |
+
if not code:
|
| 569 |
+
raise RuntimeError("็ญๅพ
ๆณจๅ้ช่ฏ็ ่ถ
ๆถ")
|
| 570 |
+
step(index, f"ๆถๅฐๆณจๅ้ช่ฏ็ : {code}")
|
| 571 |
+
self._validate_otp(code, index)
|
| 572 |
+
self._create_account(f"{first_name} {last_name}", _random_birthdate(), index)
|
| 573 |
+
tokens = self._login_and_exchange_tokens(email, password, mailbox, index)
|
| 574 |
+
return {
|
| 575 |
+
"email": email,
|
| 576 |
+
"password": password,
|
| 577 |
+
"access_token": str(tokens.get("access_token") or "").strip(),
|
| 578 |
+
"refresh_token": str(tokens.get("refresh_token") or "").strip(),
|
| 579 |
+
"id_token": str(tokens.get("id_token") or "").strip(),
|
| 580 |
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
def worker(index: int) -> dict:
|
| 585 |
+
start = time.time()
|
| 586 |
+
registrar = PlatformRegistrar(config["proxy"])
|
| 587 |
+
try:
|
| 588 |
+
step(index, "ไปปๅกๅฏๅจ")
|
| 589 |
+
result = registrar.register(index)
|
| 590 |
+
cost = time.time() - start
|
| 591 |
+
access_token = str(result["access_token"])
|
| 592 |
+
account_service.add_accounts([access_token])
|
| 593 |
+
account_service.refresh_accounts([access_token])
|
| 594 |
+
with stats_lock:
|
| 595 |
+
stats["done"] += 1
|
| 596 |
+
stats["success"] += 1
|
| 597 |
+
avg = (time.time() - stats["start_time"]) / stats["success"]
|
| 598 |
+
log(f'{result["email"]} ๆณจๅๆๅ๏ผๆฌๆฌก่ๆถ{cost:.1f}s๏ผๅ
จๅฑๅนณๅๆฏไธชๅทๆณจๅ่ๆถ{avg:.1f}s', "green")
|
| 599 |
+
return {"ok": True, "index": index, "result": result}
|
| 600 |
+
except Exception as e:
|
| 601 |
+
cost = time.time() - start
|
| 602 |
+
with stats_lock:
|
| 603 |
+
stats["done"] += 1
|
| 604 |
+
stats["fail"] += 1
|
| 605 |
+
log(f"ไปปๅก{index} ๆณจๅๅคฑ่ดฅ๏ผๆฌๆฌก่ๆถ{cost:.1f}s๏ผๅๅ : {e}", "red")
|
| 606 |
+
return {"ok": False, "index": index, "error": str(e)}
|
| 607 |
+
finally:
|
| 608 |
+
registrar.close()
|
services/register_service.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import threading
|
| 5 |
+
import time
|
| 6 |
+
import uuid
|
| 7 |
+
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
| 8 |
+
from datetime import datetime, timezone
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
from services.account_service import account_service
|
| 12 |
+
from services.config import DATA_DIR
|
| 13 |
+
from services.register import openai_register
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
REGISTER_FILE = DATA_DIR / "register.json"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _now() -> str:
|
| 20 |
+
return datetime.now(timezone.utc).isoformat()
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _default_config() -> dict:
|
| 24 |
+
return {**openai_register.config, "mode": "total", "target_quota": 100, "target_available": 10, "check_interval": 5, "enabled": False, "stats": {"success": 0, "fail": 0, "done": 0, "running": 0, "threads": openai_register.config["threads"], "elapsed_seconds": 0, "avg_seconds": 0, "success_rate": 0, "current_quota": 0, "current_available": 0}}
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _normalize(raw: dict) -> dict:
|
| 28 |
+
cfg = _default_config()
|
| 29 |
+
cfg.update({k: v for k, v in raw.items() if k not in {"stats", "logs"}})
|
| 30 |
+
cfg["total"] = max(1, int(cfg.get("total") or 1))
|
| 31 |
+
cfg["threads"] = max(1, int(cfg.get("threads") or 1))
|
| 32 |
+
cfg["mode"] = str(cfg.get("mode") or "total").strip() if str(cfg.get("mode") or "total").strip() in {"total", "quota", "available"} else "total"
|
| 33 |
+
cfg["target_quota"] = max(1, int(cfg.get("target_quota") or 1))
|
| 34 |
+
cfg["target_available"] = max(1, int(cfg.get("target_available") or 1))
|
| 35 |
+
cfg["check_interval"] = max(1, int(cfg.get("check_interval") or 5))
|
| 36 |
+
cfg["proxy"] = str(cfg.get("proxy") or "").strip()
|
| 37 |
+
cfg["enabled"] = bool(cfg.get("enabled"))
|
| 38 |
+
stats = {**_default_config()["stats"], **(raw.get("stats") if isinstance(raw.get("stats"), dict) else {}),
|
| 39 |
+
"threads": cfg["threads"]}
|
| 40 |
+
cfg["stats"] = stats
|
| 41 |
+
return cfg
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class RegisterService:
|
| 45 |
+
def __init__(self, store_file: Path):
|
| 46 |
+
self._store_file = store_file
|
| 47 |
+
self._lock = threading.RLock()
|
| 48 |
+
self._runner: threading.Thread | None = None
|
| 49 |
+
self._logs: list[dict] = []
|
| 50 |
+
openai_register.register_log_sink = self._append_log
|
| 51 |
+
self._config = self._load()
|
| 52 |
+
if self._config["enabled"]:
|
| 53 |
+
self.start()
|
| 54 |
+
|
| 55 |
+
def _load(self) -> dict:
|
| 56 |
+
try:
|
| 57 |
+
return _normalize(json.loads(self._store_file.read_text(encoding="utf-8")))
|
| 58 |
+
except Exception:
|
| 59 |
+
return _normalize({})
|
| 60 |
+
|
| 61 |
+
def _save(self) -> None:
|
| 62 |
+
self._store_file.parent.mkdir(parents=True, exist_ok=True)
|
| 63 |
+
self._store_file.write_text(json.dumps(self._config, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
| 64 |
+
|
| 65 |
+
def get(self) -> dict:
|
| 66 |
+
with self._lock:
|
| 67 |
+
return json.loads(json.dumps({**self._config, "logs": self._logs[-300:]}, ensure_ascii=False))
|
| 68 |
+
|
| 69 |
+
def update(self, updates: dict) -> dict:
|
| 70 |
+
with self._lock:
|
| 71 |
+
self._config = _normalize({**self._config, **updates})
|
| 72 |
+
openai_register.config.update({k: self._config[k] for k in ("mail", "proxy", "total", "threads")})
|
| 73 |
+
self._save()
|
| 74 |
+
return self.get()
|
| 75 |
+
|
| 76 |
+
def start(self) -> dict:
|
| 77 |
+
with self._lock:
|
| 78 |
+
if self._runner and self._runner.is_alive():
|
| 79 |
+
self._config["enabled"] = True
|
| 80 |
+
self._save()
|
| 81 |
+
return self.get()
|
| 82 |
+
self._config["enabled"] = True
|
| 83 |
+
self._logs = []
|
| 84 |
+
metrics = self._pool_metrics()
|
| 85 |
+
self._config["stats"] = {"job_id": uuid.uuid4().hex, "success": 0, "fail": 0, "done": 0, "running": 0, "threads": self._config["threads"], **metrics, "started_at": _now(), "updated_at": _now()}
|
| 86 |
+
openai_register.config.update({k: self._config[k] for k in ("mail", "proxy", "total", "threads")})
|
| 87 |
+
with openai_register.stats_lock:
|
| 88 |
+
openai_register.stats.update({"done": 0, "success": 0, "fail": 0, "start_time": time.time()})
|
| 89 |
+
self._save()
|
| 90 |
+
self._runner = threading.Thread(target=self._run, daemon=True, name="openai-register")
|
| 91 |
+
self._runner.start()
|
| 92 |
+
self._append_log(f"ๆณจๅไปปๅกๅฏๅจ๏ผๆจกๅผ={self._config['mode']}๏ผ็บฟ็จๆฐ={self._config['threads']}", "yellow")
|
| 93 |
+
return self.get()
|
| 94 |
+
|
| 95 |
+
def stop(self) -> dict:
|
| 96 |
+
with self._lock:
|
| 97 |
+
self._config["enabled"] = False
|
| 98 |
+
self._config["stats"]["updated_at"] = _now()
|
| 99 |
+
self._save()
|
| 100 |
+
self._append_log("ๅทฒ่ฏทๆฑๅๆญขๆณจๅไปปๅก๏ผๆญฃๅจ็ญๅพ
ๅฝๅ่ฟ่กไปปๅก็ปๆ", "yellow")
|
| 101 |
+
return self.get()
|
| 102 |
+
|
| 103 |
+
def reset(self) -> dict:
|
| 104 |
+
with self._lock:
|
| 105 |
+
self._logs = []
|
| 106 |
+
self._config["stats"] = {"success": 0, "fail": 0, "done": 0, "running": 0, "threads": self._config["threads"], "elapsed_seconds": 0, "avg_seconds": 0, "success_rate": 0, **self._pool_metrics(), "updated_at": _now()}
|
| 107 |
+
with openai_register.stats_lock:
|
| 108 |
+
openai_register.stats.update({"done": 0, "success": 0, "fail": 0, "start_time": 0.0})
|
| 109 |
+
self._save()
|
| 110 |
+
return self.get()
|
| 111 |
+
|
| 112 |
+
def _append_log(self, text: str, color: str = "") -> None:
|
| 113 |
+
with self._lock:
|
| 114 |
+
self._logs.append({"time": _now(), "text": str(text), "level": str(color or "info")})
|
| 115 |
+
self._logs = self._logs[-300:]
|
| 116 |
+
|
| 117 |
+
def _pool_metrics(self) -> dict:
|
| 118 |
+
items = account_service.list_accounts()
|
| 119 |
+
normal = [item for item in items if item.get("status") == "ๆญฃๅธธ"]
|
| 120 |
+
return {
|
| 121 |
+
"current_quota": sum(int(item.get("quota") or 0) for item in normal if not item.get("imageQuotaUnknown")),
|
| 122 |
+
"current_available": len(normal),
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
def _target_reached(self, cfg: dict, submitted: int) -> bool:
|
| 126 |
+
mode = str(cfg.get("mode") or "total")
|
| 127 |
+
metrics = self._pool_metrics()
|
| 128 |
+
self._bump(**metrics)
|
| 129 |
+
if mode == "quota":
|
| 130 |
+
reached = metrics["current_quota"] >= int(cfg.get("target_quota") or 1)
|
| 131 |
+
self._append_log(f"ๆฃๆฅๅทๆฑ ๏ผๅฝๅๆญฃๅธธ่ดฆๅท={metrics['current_available']}๏ผๅฝๅๅฉไฝ้ขๅบฆ={metrics['current_quota']}๏ผ็ฎๆ ้ขๅบฆ={cfg.get('target_quota')}๏ผ{'่ทณ่ฟๆณจๅ' if reached else '็ปง็ปญๆณจๅ'}", "yellow")
|
| 132 |
+
return reached
|
| 133 |
+
if mode == "available":
|
| 134 |
+
reached = metrics["current_available"] >= int(cfg.get("target_available") or 1)
|
| 135 |
+
self._append_log(f"ๆฃๆฅๅทๆฑ ๏ผๅฝๅๆญฃๅธธ่ดฆๅท={metrics['current_available']}๏ผ็ฎๆ ่ดฆๅท={cfg.get('target_available')}๏ผๅฝๅๅฉไฝ้ขๅบฆ={metrics['current_quota']}๏ผ{'่ทณ่ฟๆณจๅ' if reached else '็ปง็ปญๆณจๅ'}", "yellow")
|
| 136 |
+
return reached
|
| 137 |
+
return submitted >= int(cfg.get("total") or 1)
|
| 138 |
+
|
| 139 |
+
def _bump(self, **updates) -> None:
|
| 140 |
+
with self._lock:
|
| 141 |
+
self._config["stats"].update(updates)
|
| 142 |
+
stats = self._config["stats"]
|
| 143 |
+
started_at = str(stats.get("started_at") or "")
|
| 144 |
+
if started_at:
|
| 145 |
+
try:
|
| 146 |
+
elapsed = max(0.0, (datetime.now(timezone.utc) - datetime.fromisoformat(started_at)).total_seconds())
|
| 147 |
+
except Exception:
|
| 148 |
+
elapsed = 0.0
|
| 149 |
+
done = int(stats.get("done") or 0)
|
| 150 |
+
success = int(stats.get("success") or 0)
|
| 151 |
+
fail = int(stats.get("fail") or 0)
|
| 152 |
+
stats["elapsed_seconds"] = round(elapsed, 1)
|
| 153 |
+
stats["avg_seconds"] = round(elapsed / success, 1) if success else 0
|
| 154 |
+
stats["success_rate"] = round(success * 100 / max(1, success + fail), 1)
|
| 155 |
+
self._config["stats"]["updated_at"] = _now()
|
| 156 |
+
self._save()
|
| 157 |
+
|
| 158 |
+
def _run(self) -> None:
|
| 159 |
+
cfg = self.get()
|
| 160 |
+
threads = int(cfg["threads"])
|
| 161 |
+
submitted, done, success, fail = 0, 0, 0, 0
|
| 162 |
+
with ThreadPoolExecutor(max_workers=threads) as executor:
|
| 163 |
+
futures = set()
|
| 164 |
+
while True:
|
| 165 |
+
while self.get()["enabled"] and not self._target_reached(cfg, submitted) and len(futures) < threads:
|
| 166 |
+
submitted += 1
|
| 167 |
+
futures.add(executor.submit(openai_register.worker, submitted))
|
| 168 |
+
self._bump(running=len(futures), done=done, success=success, fail=fail)
|
| 169 |
+
if not futures and (not self.get()["enabled"] or str(cfg.get("mode") or "total") == "total"):
|
| 170 |
+
break
|
| 171 |
+
if not futures:
|
| 172 |
+
time.sleep(max(1, int(cfg.get("check_interval") or 5)))
|
| 173 |
+
continue
|
| 174 |
+
finished, futures = wait(futures, return_when=FIRST_COMPLETED)
|
| 175 |
+
for future in finished:
|
| 176 |
+
done += 1
|
| 177 |
+
try:
|
| 178 |
+
result = future.result()
|
| 179 |
+
success += 1 if result.get("ok") else 0
|
| 180 |
+
fail += 0 if result.get("ok") else 1
|
| 181 |
+
except Exception:
|
| 182 |
+
fail += 1
|
| 183 |
+
self._bump(running=0, done=done, success=success, fail=fail, finished_at=_now())
|
| 184 |
+
with self._lock:
|
| 185 |
+
self._config["enabled"] = False
|
| 186 |
+
self._save()
|
| 187 |
+
self._append_log(f"ๆณจๅไปปๅก็ปๆ๏ผๆๅ{success}๏ผๅคฑ่ดฅ{fail}", "yellow")
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
register_service = RegisterService(REGISTER_FILE)
|
web/src/app/register/components/register-card.tsx
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { AlertTriangle, LoaderCircle, Plus, Play, RotateCcw, Save, Square, Trash2, UserPlus } from "lucide-react";
|
| 4 |
+
|
| 5 |
+
import { Badge } from "@/components/ui/badge";
|
| 6 |
+
import { Button } from "@/components/ui/button";
|
| 7 |
+
import { Checkbox } from "@/components/ui/checkbox";
|
| 8 |
+
import { Input } from "@/components/ui/input";
|
| 9 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
| 10 |
+
import { Textarea } from "@/components/ui/textarea";
|
| 11 |
+
|
| 12 |
+
import { useSettingsStore } from "../../settings/store";
|
| 13 |
+
|
| 14 |
+
export function RegisterCard() {
|
| 15 |
+
const config = useSettingsStore((state) => state.registerConfig);
|
| 16 |
+
const isLoading = useSettingsStore((state) => state.isLoadingRegister);
|
| 17 |
+
const isSaving = useSettingsStore((state) => state.isSavingRegister);
|
| 18 |
+
const setProxy = useSettingsStore((state) => state.setRegisterProxy);
|
| 19 |
+
const setTotal = useSettingsStore((state) => state.setRegisterTotal);
|
| 20 |
+
const setThreads = useSettingsStore((state) => state.setRegisterThreads);
|
| 21 |
+
const setMode = useSettingsStore((state) => state.setRegisterMode);
|
| 22 |
+
const setTargetQuota = useSettingsStore((state) => state.setRegisterTargetQuota);
|
| 23 |
+
const setTargetAvailable = useSettingsStore((state) => state.setRegisterTargetAvailable);
|
| 24 |
+
const setCheckInterval = useSettingsStore((state) => state.setRegisterCheckInterval);
|
| 25 |
+
const setMailField = useSettingsStore((state) => state.setRegisterMailField);
|
| 26 |
+
const addProvider = useSettingsStore((state) => state.addRegisterProvider);
|
| 27 |
+
const updateProvider = useSettingsStore((state) => state.updateRegisterProvider);
|
| 28 |
+
const deleteProvider = useSettingsStore((state) => state.deleteRegisterProvider);
|
| 29 |
+
const save = useSettingsStore((state) => state.saveRegister);
|
| 30 |
+
const toggle = useSettingsStore((state) => state.toggleRegister);
|
| 31 |
+
const reset = useSettingsStore((state) => state.resetRegister);
|
| 32 |
+
|
| 33 |
+
if (isLoading) {
|
| 34 |
+
return (
|
| 35 |
+
<div className="flex items-center justify-center rounded-xl border border-stone-200 bg-white/80 p-10">
|
| 36 |
+
<LoaderCircle className="size-5 animate-spin text-stone-400" />
|
| 37 |
+
</div>
|
| 38 |
+
);
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
if (!config) return null;
|
| 42 |
+
|
| 43 |
+
const stats = config.stats || { success: 0, fail: 0, done: 0, running: 0, threads: config.threads };
|
| 44 |
+
const providers = config.mail.providers || [];
|
| 45 |
+
const logs = config.logs || [];
|
| 46 |
+
const updateProviderType = (index: number, type: string) => {
|
| 47 |
+
updateProvider(index, {
|
| 48 |
+
type,
|
| 49 |
+
enable: true,
|
| 50 |
+
...(type === "cloudflare_temp_email" ? { api_base: "", admin_password: "", domain: [] } : {}),
|
| 51 |
+
...(type === "tempmail_lol" ? { api_key: "", domain: [] } : {}),
|
| 52 |
+
...(type === "duckmail" ? { api_key: "", default_domain: "duckmail.sbs" } : {}),
|
| 53 |
+
...(type === "gptmail" ? { api_key: "", default_domain: "" } : {}),
|
| 54 |
+
});
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
return (
|
| 58 |
+
<div className="grid h-[calc(100vh-132px)] min-h-[640px] items-stretch gap-0 overflow-hidden rounded-xl border border-stone-200 bg-white/70 xl:grid-cols-2">
|
| 59 |
+
<section className="space-y-4 overflow-y-auto border-b border-stone-200 p-4 xl:border-r xl:border-b-0">
|
| 60 |
+
<div className="flex items-start justify-between gap-3">
|
| 61 |
+
<div className="flex items-center gap-3">
|
| 62 |
+
<div className="flex size-9 items-center justify-center rounded-md bg-stone-100">
|
| 63 |
+
<UserPlus className="size-5 text-stone-600" />
|
| 64 |
+
</div>
|
| 65 |
+
<div>
|
| 66 |
+
<h2 className="text-lg font-semibold tracking-tight">ๆณจๅ้
็ฝฎ</h2>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
<Button className="h-9 rounded-xl bg-stone-950 px-4 text-white hover:bg-stone-800" onClick={() => void save()} disabled={isSaving || config.enabled}>
|
| 70 |
+
{isSaving ? <LoaderCircle className="size-4 animate-spin" /> : <Save className="size-4" />}
|
| 71 |
+
ไฟๅญ้
็ฝฎ
|
| 72 |
+
</Button>
|
| 73 |
+
</div>
|
| 74 |
+
|
| 75 |
+
<div className="grid gap-4 md:grid-cols-3">
|
| 76 |
+
<div className="space-y-2">
|
| 77 |
+
<label className="text-sm text-stone-700">ๆณจๅๆจกๅผ</label>
|
| 78 |
+
<Select value={config.mode || "total"} onValueChange={(value) => setMode(value as "total" | "quota" | "available")} disabled={config.enabled}>
|
| 79 |
+
<SelectTrigger className="h-10 rounded-xl border-stone-200 bg-white">
|
| 80 |
+
<SelectValue />
|
| 81 |
+
</SelectTrigger>
|
| 82 |
+
<SelectContent>
|
| 83 |
+
<SelectItem value="total">ๆณจๅๆปๆฐ</SelectItem>
|
| 84 |
+
<SelectItem value="quota">ๅทๆฑ ๅฉไฝ้ขๅบฆ</SelectItem>
|
| 85 |
+
<SelectItem value="available">ๅฏ็จ่ดฆๅทๆฐ้</SelectItem>
|
| 86 |
+
</SelectContent>
|
| 87 |
+
</Select>
|
| 88 |
+
</div>
|
| 89 |
+
<div className="space-y-2">
|
| 90 |
+
<label className="text-sm text-stone-700">ๆณจๅๆปๆฐ</label>
|
| 91 |
+
<Input value={String(config.total)} onChange={(event) => setTotal(event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled || config.mode !== "total"} />
|
| 92 |
+
</div>
|
| 93 |
+
<div className="space-y-2">
|
| 94 |
+
<label className="text-sm text-stone-700">็บฟ็จๆฐ</label>
|
| 95 |
+
<Input value={String(config.threads)} onChange={(event) => setThreads(event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 96 |
+
</div>
|
| 97 |
+
<div className="space-y-2">
|
| 98 |
+
<label className="text-sm text-stone-700">ๆณจๅไปฃ็</label>
|
| 99 |
+
<Input value={config.proxy} onChange={(event) => setProxy(event.target.value)} placeholder="http://127.0.0.1:7890" className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 100 |
+
</div>
|
| 101 |
+
<div className="space-y-2">
|
| 102 |
+
<label className="text-sm text-stone-700">็ฎๆ ๅฉไฝ้ขๅบฆ</label>
|
| 103 |
+
<Input value={String(config.target_quota || "")} onChange={(event) => setTargetQuota(event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled || config.mode !== "quota"} />
|
| 104 |
+
</div>
|
| 105 |
+
<div className="space-y-2">
|
| 106 |
+
<label className="text-sm text-stone-700">็ฎๆ ๅฏ็จ่ดฆๅท</label>
|
| 107 |
+
<Input value={String(config.target_available || "")} onChange={(event) => setTargetAvailable(event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled || config.mode !== "available"} />
|
| 108 |
+
</div>
|
| 109 |
+
<div className="space-y-2">
|
| 110 |
+
<label className="text-sm text-stone-700">ๆฃๆฅ้ด้๏ผ็ง๏ผ</label>
|
| 111 |
+
<Input value={String(config.check_interval || "")} onChange={(event) => setCheckInterval(event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled || config.mode === "total"} />
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div className="space-y-3 border-t border-stone-200 pt-3">
|
| 116 |
+
<div className="flex items-center justify-between gap-3">
|
| 117 |
+
<div>
|
| 118 |
+
<h3 className="text-sm font-semibold text-stone-800">้ฎ็ฎฑ้
็ฝฎ</h3>
|
| 119 |
+
<p className="mt-1 text-xs text-stone-500">ๅฏ้
็ฝฎๅคไธช provider๏ผๆๅฏ็จ้กบๅบ่ฝฎๆขใ</p>
|
| 120 |
+
</div>
|
| 121 |
+
<Button type="button" variant="outline" className="h-9 rounded-xl border-stone-200 bg-white px-3 text-stone-700" onClick={addProvider} disabled={config.enabled}>
|
| 122 |
+
<Plus className="size-4" />
|
| 123 |
+
ๆทปๅ
|
| 124 |
+
</Button>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
<div className="grid gap-4 md:grid-cols-3">
|
| 128 |
+
<div className="space-y-2">
|
| 129 |
+
<label className="text-sm text-stone-700">่ฏทๆฑ่ถ
ๆถ</label>
|
| 130 |
+
<Input value={String(config.mail.request_timeout || "")} onChange={(event) => setMailField("request_timeout", event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 131 |
+
</div>
|
| 132 |
+
<div className="space-y-2">
|
| 133 |
+
<label className="text-sm text-stone-700">็ญๅพ
้ช่ฏ็ ่ถ
ๆถ</label>
|
| 134 |
+
<Input value={String(config.mail.wait_timeout || "")} onChange={(event) => setMailField("wait_timeout", event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 135 |
+
</div>
|
| 136 |
+
<div className="space-y-2">
|
| 137 |
+
<label className="text-sm text-stone-700">่ฝฎ่ฏข้ด้</label>
|
| 138 |
+
<Input value={String(config.mail.wait_interval || "")} onChange={(event) => setMailField("wait_interval", event.target.value)} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 139 |
+
</div>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<div className="space-y-3">
|
| 143 |
+
{providers.map((provider, index) => {
|
| 144 |
+
const type = String(provider.type || "tempmail_lol");
|
| 145 |
+
const domains = Array.isArray(provider.domain) ? provider.domain.map(String).join("\n") : "";
|
| 146 |
+
return (
|
| 147 |
+
<div key={index} className="space-y-3 border-t border-stone-200 pt-3 first:border-t-0 first:pt-0">
|
| 148 |
+
<div className="flex items-center justify-between gap-3">
|
| 149 |
+
<label className="flex items-center gap-3 text-sm text-stone-700">
|
| 150 |
+
<Checkbox checked={Boolean(provider.enable)} onCheckedChange={(checked) => updateProvider(index, { enable: Boolean(checked) })} disabled={config.enabled} />
|
| 151 |
+
ๅฏ็จ
|
| 152 |
+
</label>
|
| 153 |
+
<button type="button" className="rounded-lg p-2 text-stone-400 transition hover:bg-rose-50 hover:text-rose-500 disabled:opacity-50" onClick={() => deleteProvider(index)} disabled={config.enabled || providers.length <= 1} title="ๅ ้ค provider">
|
| 154 |
+
<Trash2 className="size-4" />
|
| 155 |
+
</button>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<div className="grid gap-4 md:grid-cols-2">
|
| 159 |
+
<div className="space-y-2">
|
| 160 |
+
<label className="text-sm text-stone-700">็ฑปๅ</label>
|
| 161 |
+
<Select value={type} onValueChange={(value) => updateProviderType(index, value)} disabled={config.enabled}>
|
| 162 |
+
<SelectTrigger className="h-10 rounded-xl border-stone-200 bg-white">
|
| 163 |
+
<SelectValue />
|
| 164 |
+
</SelectTrigger>
|
| 165 |
+
<SelectContent>
|
| 166 |
+
<SelectItem value="cloudflare_temp_email">cloudflare_temp_email</SelectItem>
|
| 167 |
+
<SelectItem value="tempmail_lol">tempmail_lol</SelectItem>
|
| 168 |
+
<SelectItem value="duckmail">duckmail</SelectItem>
|
| 169 |
+
<SelectItem value="gptmail">gptmail(ๆชๆต่ฏ)</SelectItem>
|
| 170 |
+
</SelectContent>
|
| 171 |
+
</Select>
|
| 172 |
+
</div>
|
| 173 |
+
{type === "cloudflare_temp_email" ? (
|
| 174 |
+
<>
|
| 175 |
+
<div className="space-y-2">
|
| 176 |
+
<label className="text-sm text-stone-700">API Base</label>
|
| 177 |
+
<Input value={String(provider.api_base || "")} onChange={(event) => updateProvider(index, { api_base: event.target.value })} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 178 |
+
</div>
|
| 179 |
+
<div className="space-y-2">
|
| 180 |
+
<label className="text-sm text-stone-700">Admin Password</label>
|
| 181 |
+
<Input value={String(provider.admin_password || "")} onChange={(event) => updateProvider(index, { admin_password: event.target.value })} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 182 |
+
</div>
|
| 183 |
+
</>
|
| 184 |
+
) : null}
|
| 185 |
+
{type === "tempmail_lol" || type === "duckmail" || type === "gptmail" ? (
|
| 186 |
+
<div className="space-y-2">
|
| 187 |
+
<label className="text-sm text-stone-700">API Key</label>
|
| 188 |
+
<Input value={String(provider.api_key || "")} onChange={(event) => updateProvider(index, { api_key: event.target.value })} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 189 |
+
</div>
|
| 190 |
+
) : null}
|
| 191 |
+
{type === "duckmail" || type === "gptmail" ? (
|
| 192 |
+
<div className="space-y-2">
|
| 193 |
+
<label className="text-sm text-stone-700">Default Domain</label>
|
| 194 |
+
<Input value={String(provider.default_domain || "")} onChange={(event) => updateProvider(index, { default_domain: event.target.value })} placeholder={type === "duckmail" ? "duckmail.sbs" : ""} className="h-10 rounded-xl border-stone-200 bg-white" disabled={config.enabled} />
|
| 195 |
+
</div>
|
| 196 |
+
) : null}
|
| 197 |
+
</div>
|
| 198 |
+
|
| 199 |
+
{type === "tempmail_lol" || type === "cloudflare_temp_email" ? (
|
| 200 |
+
<div className="space-y-2">
|
| 201 |
+
<label className="text-sm text-stone-700">Domain</label>
|
| 202 |
+
<Textarea value={domains} onChange={(event) => updateProvider(index, { domain: event.target.value.split(/[\n,]/).map((item) => item.trim()).filter(Boolean) })} placeholder="ๆฏ่กไธไธชๅๅ๏ผ็็ฉบๅไฝฟ็จๆๅก้ป่ฎคๅๅ" className="min-h-20 rounded-xl border-stone-200 bg-white font-mono text-xs" disabled={config.enabled} />
|
| 203 |
+
</div>
|
| 204 |
+
) : null}
|
| 205 |
+
</div>
|
| 206 |
+
);
|
| 207 |
+
})}
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
|
| 211 |
+
</section>
|
| 212 |
+
|
| 213 |
+
<section className="flex min-h-0 flex-col p-4">
|
| 214 |
+
<div className="space-y-3">
|
| 215 |
+
<div className="flex items-start justify-between gap-3">
|
| 216 |
+
<div>
|
| 217 |
+
<h2 className="text-lg font-semibold tracking-tight">่ฟ่ก็ปๆ</h2>
|
| 218 |
+
<p className="mt-1 text-sm text-stone-500">SSE ๅฎๆถๆจ้ๅฝๅ็ถๆใ</p>
|
| 219 |
+
</div>
|
| 220 |
+
<Badge variant={config.enabled ? "success" : "secondary"} className="rounded-md">
|
| 221 |
+
{config.enabled ? "่ฟ่กไธญ" : "ๅทฒๅๆญข"}
|
| 222 |
+
</Badge>
|
| 223 |
+
</div>
|
| 224 |
+
<div className="grid grid-cols-4 gap-2">
|
| 225 |
+
{[
|
| 226 |
+
["ๆๅ / ๆๅ็", `${stats.success} / ${stats.success_rate || 0}%`],
|
| 227 |
+
["ๅคฑ่ดฅ", stats.fail],
|
| 228 |
+
["ๅฎๆ", stats.done],
|
| 229 |
+
["่ฟ่ก / ็บฟ็จ", `${stats.running} / ${stats.threads}`],
|
| 230 |
+
["่ฟ่กๆถ้ด", `${stats.elapsed_seconds || 0}s`],
|
| 231 |
+
["ๅนณๅๆณจๅๅไธช", `${stats.avg_seconds || 0}s`],
|
| 232 |
+
["ๅฝๅ้ขๅบฆ", stats.current_quota || 0],
|
| 233 |
+
["ๆญฃๅธธ่ดฆๅท", stats.current_available || 0],
|
| 234 |
+
].map(([label, value]) => (
|
| 235 |
+
<div key={label} className="border border-stone-200 bg-white/70 px-3 py-2">
|
| 236 |
+
<div className="text-xs text-stone-400">{label}</div>
|
| 237 |
+
<div className="mt-1 text-base font-semibold text-stone-800">{value}</div>
|
| 238 |
+
</div>
|
| 239 |
+
))}
|
| 240 |
+
</div>
|
| 241 |
+
<div className="grid grid-cols-3 gap-2">
|
| 242 |
+
<Button className="h-10 rounded-xl bg-stone-950 px-3 text-white hover:bg-stone-800" onClick={() => void toggle()} disabled={isSaving}>
|
| 243 |
+
{isSaving ? <LoaderCircle className="size-4 animate-spin" /> : config.enabled ? <Square className="size-4" /> : <Play className="size-4" />}
|
| 244 |
+
{config.enabled ? "ๅๆญข" : "ๅฏๅจ"}
|
| 245 |
+
</Button>
|
| 246 |
+
<Button variant="outline" className="h-10 rounded-xl border-stone-200 bg-white px-3 text-stone-700" onClick={() => void reset()} disabled={isSaving || config.enabled}>
|
| 247 |
+
<RotateCcw className="size-4" />
|
| 248 |
+
้็ฝฎ
|
| 249 |
+
</Button>
|
| 250 |
+
<Button variant="outline" className="h-10 rounded-xl border-stone-200 bg-white px-3 text-stone-700" onClick={() => void save()} disabled={isSaving || config.enabled}>
|
| 251 |
+
<Save className="size-4" />
|
| 252 |
+
ไฟๅญ
|
| 253 |
+
</Button>
|
| 254 |
+
</div>
|
| 255 |
+
<div className="flex items-center gap-2 border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
|
| 256 |
+
<AlertTriangle className="size-4 shrink-0" />
|
| 257 |
+
ๅฏๅจไนๅๆณจๆๅ
ไฟๅญ้
็ฝฎใ
|
| 258 |
+
</div>
|
| 259 |
+
</div>
|
| 260 |
+
|
| 261 |
+
<div className="mt-4 flex min-h-0 flex-1 flex-col space-y-3 overflow-hidden border-t border-stone-200 pt-4">
|
| 262 |
+
<div className="flex items-center justify-between">
|
| 263 |
+
<div>
|
| 264 |
+
<h3 className="text-sm font-semibold text-stone-900">ๅฎๆถๆฅๅฟ</h3>
|
| 265 |
+
<p className="mt-1 text-xs text-stone-500">ๅชไฟ็ๅ
ๅญไธญ็ๆ่ฟ 300 ๆกใ</p>
|
| 266 |
+
</div>
|
| 267 |
+
<Badge variant="secondary" className="rounded-md">
|
| 268 |
+
{logs.length}
|
| 269 |
+
</Badge>
|
| 270 |
+
</div>
|
| 271 |
+
<div className="min-h-0 flex-1 overflow-y-auto border border-stone-200 bg-white/70 p-3 font-mono text-xs leading-6">
|
| 272 |
+
{logs.length === 0 ? (
|
| 273 |
+
<div className="text-stone-500">ๆๆ ๆฅๅฟ</div>
|
| 274 |
+
) : (
|
| 275 |
+
logs.slice().reverse().map((item, index) => (
|
| 276 |
+
<div key={`${item.time}-${index}`} className={item.level === "red" ? "text-rose-600" : item.level === "green" ? "text-emerald-700" : item.level === "yellow" ? "text-amber-700" : "text-stone-700"}>
|
| 277 |
+
<span className="text-stone-400">{new Date(item.time).toLocaleTimeString()}</span>
|
| 278 |
+
<span className="pl-2">{item.text}</span>
|
| 279 |
+
</div>
|
| 280 |
+
))
|
| 281 |
+
)}
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
</section>
|
| 285 |
+
</div>
|
| 286 |
+
);
|
| 287 |
+
}
|
web/src/app/register/page.tsx
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useEffect, useRef } from "react";
|
| 4 |
+
import { LoaderCircle } from "lucide-react";
|
| 5 |
+
|
| 6 |
+
import webConfig from "@/constants/common-env";
|
| 7 |
+
import { useAuthGuard } from "@/lib/use-auth-guard";
|
| 8 |
+
import type { RegisterConfig } from "@/lib/api";
|
| 9 |
+
import { getStoredAuthKey } from "@/store/auth";
|
| 10 |
+
|
| 11 |
+
import { useSettingsStore } from "../settings/store";
|
| 12 |
+
import { RegisterCard } from "./components/register-card";
|
| 13 |
+
|
| 14 |
+
function RegisterDataController() {
|
| 15 |
+
const didLoadRef = useRef(false);
|
| 16 |
+
const loadRegister = useSettingsStore((state) => state.loadRegister);
|
| 17 |
+
const setRegisterConfig = useSettingsStore((state) => state.setRegisterConfig);
|
| 18 |
+
|
| 19 |
+
useEffect(() => {
|
| 20 |
+
if (didLoadRef.current) return;
|
| 21 |
+
didLoadRef.current = true;
|
| 22 |
+
void loadRegister();
|
| 23 |
+
}, [loadRegister]);
|
| 24 |
+
|
| 25 |
+
useEffect(() => {
|
| 26 |
+
let source: EventSource | null = null;
|
| 27 |
+
let closed = false;
|
| 28 |
+
void getStoredAuthKey().then((token) => {
|
| 29 |
+
if (closed || !token) return;
|
| 30 |
+
const baseUrl = webConfig.apiUrl.replace(/\/$/, "");
|
| 31 |
+
source = new EventSource(`${baseUrl}/api/register/events?token=${encodeURIComponent(token)}`);
|
| 32 |
+
source.onmessage = (event) => {
|
| 33 |
+
setRegisterConfig(JSON.parse(event.data) as RegisterConfig);
|
| 34 |
+
};
|
| 35 |
+
});
|
| 36 |
+
return () => {
|
| 37 |
+
closed = true;
|
| 38 |
+
source?.close();
|
| 39 |
+
};
|
| 40 |
+
}, [setRegisterConfig]);
|
| 41 |
+
|
| 42 |
+
return null;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
function RegisterPageContent() {
|
| 46 |
+
return (
|
| 47 |
+
<>
|
| 48 |
+
<RegisterDataController />
|
| 49 |
+
<section className="mb-2 flex flex-col gap-1 lg:flex-row lg:items-start lg:justify-between">
|
| 50 |
+
<div className="space-y-1">
|
| 51 |
+
<div className="text-xs font-semibold tracking-[0.18em] text-stone-500 uppercase">Register</div>
|
| 52 |
+
<h1 className="text-2xl font-semibold tracking-tight">ChatGPTๆณจๅๆบ</h1>
|
| 53 |
+
</div>
|
| 54 |
+
</section>
|
| 55 |
+
<section>
|
| 56 |
+
<RegisterCard />
|
| 57 |
+
</section>
|
| 58 |
+
</>
|
| 59 |
+
);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
export default function RegisterPage() {
|
| 63 |
+
const { isCheckingAuth, session } = useAuthGuard(["admin"]);
|
| 64 |
+
|
| 65 |
+
if (isCheckingAuth || !session || session.role !== "admin") {
|
| 66 |
+
return (
|
| 67 |
+
<div className="flex min-h-[40vh] items-center justify-center">
|
| 68 |
+
<LoaderCircle className="size-5 animate-spin text-stone-400" />
|
| 69 |
+
</div>
|
| 70 |
+
);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
return <RegisterPageContent />;
|
| 74 |
+
}
|
web/src/app/settings/store.ts
CHANGED
|
@@ -8,12 +8,18 @@ import {
|
|
| 8 |
deleteCPAPool,
|
| 9 |
fetchCPAPoolFiles,
|
| 10 |
fetchCPAPools,
|
|
|
|
|
|
|
| 11 |
fetchSettingsConfig,
|
|
|
|
| 12 |
startCPAImport,
|
|
|
|
| 13 |
updateCPAPool,
|
|
|
|
| 14 |
updateSettingsConfig,
|
| 15 |
type CPAPool,
|
| 16 |
type CPARemoteFile,
|
|
|
|
| 17 |
type SettingsConfig,
|
| 18 |
} from "@/lib/api";
|
| 19 |
|
|
@@ -55,6 +61,10 @@ type SettingsStore = {
|
|
| 55 |
isLoadingConfig: boolean;
|
| 56 |
isSavingConfig: boolean;
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
pools: CPAPool[];
|
| 59 |
isLoadingPools: boolean;
|
| 60 |
deletingId: string | null;
|
|
@@ -87,6 +97,23 @@ type SettingsStore = {
|
|
| 87 |
setProxy: (value: string) => void;
|
| 88 |
setBaseUrl: (value: string) => void;
|
| 89 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
loadPools: (silent?: boolean) => Promise<void>;
|
| 91 |
openAddDialog: () => void;
|
| 92 |
openEditDialog: (pool: CPAPool) => void;
|
|
@@ -113,6 +140,10 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|
| 113 |
isLoadingConfig: true,
|
| 114 |
isSavingConfig: false,
|
| 115 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
pools: [],
|
| 117 |
isLoadingPools: true,
|
| 118 |
deletingId: null,
|
|
@@ -240,6 +271,159 @@ export const useSettingsStore = create<SettingsStore>((set, get) => ({
|
|
| 240 |
});
|
| 241 |
},
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
loadPools: async (silent = false) => {
|
| 244 |
if (!silent) {
|
| 245 |
set({ isLoadingPools: true });
|
|
|
|
| 8 |
deleteCPAPool,
|
| 9 |
fetchCPAPoolFiles,
|
| 10 |
fetchCPAPools,
|
| 11 |
+
fetchRegisterConfig,
|
| 12 |
+
resetRegister as resetRegisterApi,
|
| 13 |
fetchSettingsConfig,
|
| 14 |
+
startRegister,
|
| 15 |
startCPAImport,
|
| 16 |
+
stopRegister,
|
| 17 |
updateCPAPool,
|
| 18 |
+
updateRegisterConfig,
|
| 19 |
updateSettingsConfig,
|
| 20 |
type CPAPool,
|
| 21 |
type CPARemoteFile,
|
| 22 |
+
type RegisterConfig,
|
| 23 |
type SettingsConfig,
|
| 24 |
} from "@/lib/api";
|
| 25 |
|
|
|
|
| 61 |
isLoadingConfig: boolean;
|
| 62 |
isSavingConfig: boolean;
|
| 63 |
|
| 64 |
+
registerConfig: RegisterConfig | null;
|
| 65 |
+
isLoadingRegister: boolean;
|
| 66 |
+
isSavingRegister: boolean;
|
| 67 |
+
|
| 68 |
pools: CPAPool[];
|
| 69 |
isLoadingPools: boolean;
|
| 70 |
deletingId: string | null;
|
|
|
|
| 97 |
setProxy: (value: string) => void;
|
| 98 |
setBaseUrl: (value: string) => void;
|
| 99 |
|
| 100 |
+
loadRegister: (silent?: boolean) => Promise<void>;
|
| 101 |
+
setRegisterConfig: (config: RegisterConfig) => void;
|
| 102 |
+
setRegisterProxy: (value: string) => void;
|
| 103 |
+
setRegisterTotal: (value: string) => void;
|
| 104 |
+
setRegisterThreads: (value: string) => void;
|
| 105 |
+
setRegisterMode: (value: "total" | "quota" | "available") => void;
|
| 106 |
+
setRegisterTargetQuota: (value: string) => void;
|
| 107 |
+
setRegisterTargetAvailable: (value: string) => void;
|
| 108 |
+
setRegisterCheckInterval: (value: string) => void;
|
| 109 |
+
setRegisterMailField: (key: "request_timeout" | "wait_timeout" | "wait_interval", value: string) => void;
|
| 110 |
+
addRegisterProvider: () => void;
|
| 111 |
+
updateRegisterProvider: (index: number, updates: Record<string, unknown>) => void;
|
| 112 |
+
deleteRegisterProvider: (index: number) => void;
|
| 113 |
+
saveRegister: () => Promise<void>;
|
| 114 |
+
toggleRegister: () => Promise<void>;
|
| 115 |
+
resetRegister: () => Promise<void>;
|
| 116 |
+
|
| 117 |
loadPools: (silent?: boolean) => Promise<void>;
|
| 118 |
openAddDialog: () => void;
|
| 119 |
openEditDialog: (pool: CPAPool) => void;
|
|
|
|
| 140 |
isLoadingConfig: true,
|
| 141 |
isSavingConfig: false,
|
| 142 |
|
| 143 |
+
registerConfig: null,
|
| 144 |
+
isLoadingRegister: true,
|
| 145 |
+
isSavingRegister: false,
|
| 146 |
+
|
| 147 |
pools: [],
|
| 148 |
isLoadingPools: true,
|
| 149 |
deletingId: null,
|
|
|
|
| 271 |
});
|
| 272 |
},
|
| 273 |
|
| 274 |
+
loadRegister: async (silent = false) => {
|
| 275 |
+
if (!silent) set({ isLoadingRegister: true });
|
| 276 |
+
try {
|
| 277 |
+
const data = await fetchRegisterConfig();
|
| 278 |
+
set({ registerConfig: data.register });
|
| 279 |
+
} catch (error) {
|
| 280 |
+
if (!silent) toast.error(error instanceof Error ? error.message : "ๅ ่ฝฝๆณจๅ้
็ฝฎๅคฑ่ดฅ");
|
| 281 |
+
} finally {
|
| 282 |
+
if (!silent) set({ isLoadingRegister: false });
|
| 283 |
+
}
|
| 284 |
+
},
|
| 285 |
+
|
| 286 |
+
setRegisterConfig: (config) => {
|
| 287 |
+
set({ registerConfig: config, isLoadingRegister: false });
|
| 288 |
+
},
|
| 289 |
+
|
| 290 |
+
setRegisterProxy: (value) => {
|
| 291 |
+
set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, proxy: value } } : {});
|
| 292 |
+
},
|
| 293 |
+
|
| 294 |
+
setRegisterTotal: (value) => {
|
| 295 |
+
set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, total: Number(value) || 0 } } : {});
|
| 296 |
+
},
|
| 297 |
+
|
| 298 |
+
setRegisterThreads: (value) => {
|
| 299 |
+
set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, threads: Number(value) || 0 } } : {});
|
| 300 |
+
},
|
| 301 |
+
|
| 302 |
+
setRegisterMode: (value) => {
|
| 303 |
+
set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, mode: value } } : {});
|
| 304 |
+
},
|
| 305 |
+
|
| 306 |
+
setRegisterTargetQuota: (value) => {
|
| 307 |
+
set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, target_quota: Number(value) || 0 } } : {});
|
| 308 |
+
},
|
| 309 |
+
|
| 310 |
+
setRegisterTargetAvailable: (value) => {
|
| 311 |
+
set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, target_available: Number(value) || 0 } } : {});
|
| 312 |
+
},
|
| 313 |
+
|
| 314 |
+
setRegisterCheckInterval: (value) => {
|
| 315 |
+
set((state) => state.registerConfig ? { registerConfig: { ...state.registerConfig, check_interval: Number(value) || 0 } } : {});
|
| 316 |
+
},
|
| 317 |
+
|
| 318 |
+
setRegisterMailField: (key, value) => {
|
| 319 |
+
set((state) => state.registerConfig ? {
|
| 320 |
+
registerConfig: {
|
| 321 |
+
...state.registerConfig,
|
| 322 |
+
mail: { ...state.registerConfig.mail, [key]: Number(value) || 0 },
|
| 323 |
+
},
|
| 324 |
+
} : {});
|
| 325 |
+
},
|
| 326 |
+
|
| 327 |
+
addRegisterProvider: () => {
|
| 328 |
+
set((state) => state.registerConfig ? {
|
| 329 |
+
registerConfig: {
|
| 330 |
+
...state.registerConfig,
|
| 331 |
+
mail: {
|
| 332 |
+
...state.registerConfig.mail,
|
| 333 |
+
providers: [
|
| 334 |
+
...(state.registerConfig.mail.providers || []),
|
| 335 |
+
{ enable: true, type: "tempmail_lol", api_key: "", domain: [] },
|
| 336 |
+
],
|
| 337 |
+
},
|
| 338 |
+
},
|
| 339 |
+
} : {});
|
| 340 |
+
},
|
| 341 |
+
|
| 342 |
+
updateRegisterProvider: (index, updates) => {
|
| 343 |
+
set((state) => {
|
| 344 |
+
if (!state.registerConfig) return {};
|
| 345 |
+
const providers = [...(state.registerConfig.mail.providers || [])];
|
| 346 |
+
providers[index] = { ...(providers[index] || {}), ...updates };
|
| 347 |
+
return { registerConfig: { ...state.registerConfig, mail: { ...state.registerConfig.mail, providers } } };
|
| 348 |
+
});
|
| 349 |
+
},
|
| 350 |
+
|
| 351 |
+
deleteRegisterProvider: (index) => {
|
| 352 |
+
set((state) => state.registerConfig ? {
|
| 353 |
+
registerConfig: {
|
| 354 |
+
...state.registerConfig,
|
| 355 |
+
mail: {
|
| 356 |
+
...state.registerConfig.mail,
|
| 357 |
+
providers: (state.registerConfig.mail.providers || []).filter((_, itemIndex) => itemIndex !== index),
|
| 358 |
+
},
|
| 359 |
+
},
|
| 360 |
+
} : {});
|
| 361 |
+
},
|
| 362 |
+
|
| 363 |
+
saveRegister: async () => {
|
| 364 |
+
const { registerConfig } = get();
|
| 365 |
+
if (!registerConfig) return;
|
| 366 |
+
try {
|
| 367 |
+
set({ isSavingRegister: true });
|
| 368 |
+
const data = await updateRegisterConfig({
|
| 369 |
+
mail: registerConfig.mail,
|
| 370 |
+
proxy: registerConfig.proxy.trim(),
|
| 371 |
+
total: Math.max(1, Number(registerConfig.total) || 1),
|
| 372 |
+
threads: Math.max(1, Number(registerConfig.threads) || 1),
|
| 373 |
+
mode: registerConfig.mode,
|
| 374 |
+
target_quota: Math.max(1, Number(registerConfig.target_quota) || 1),
|
| 375 |
+
target_available: Math.max(1, Number(registerConfig.target_available) || 1),
|
| 376 |
+
check_interval: Math.max(1, Number(registerConfig.check_interval) || 5),
|
| 377 |
+
});
|
| 378 |
+
set({ registerConfig: data.register });
|
| 379 |
+
toast.success("ๆณจๅ้
็ฝฎๅทฒไฟๅญ");
|
| 380 |
+
} catch (error) {
|
| 381 |
+
toast.error(error instanceof Error ? error.message : "ไฟๅญๆณจๅ้
็ฝฎๅคฑ่ดฅ");
|
| 382 |
+
} finally {
|
| 383 |
+
set({ isSavingRegister: false });
|
| 384 |
+
}
|
| 385 |
+
},
|
| 386 |
+
|
| 387 |
+
toggleRegister: async () => {
|
| 388 |
+
const { registerConfig } = get();
|
| 389 |
+
if (!registerConfig) return;
|
| 390 |
+
set({ isSavingRegister: true });
|
| 391 |
+
try {
|
| 392 |
+
if (!registerConfig.enabled) {
|
| 393 |
+
await updateRegisterConfig({
|
| 394 |
+
mail: registerConfig.mail,
|
| 395 |
+
proxy: registerConfig.proxy.trim(),
|
| 396 |
+
total: Math.max(1, Number(registerConfig.total) || 1),
|
| 397 |
+
threads: Math.max(1, Number(registerConfig.threads) || 1),
|
| 398 |
+
mode: registerConfig.mode,
|
| 399 |
+
target_quota: Math.max(1, Number(registerConfig.target_quota) || 1),
|
| 400 |
+
target_available: Math.max(1, Number(registerConfig.target_available) || 1),
|
| 401 |
+
check_interval: Math.max(1, Number(registerConfig.check_interval) || 5),
|
| 402 |
+
});
|
| 403 |
+
}
|
| 404 |
+
const data = registerConfig.enabled ? await stopRegister() : await startRegister();
|
| 405 |
+
set({ registerConfig: data.register });
|
| 406 |
+
toast.success(registerConfig.enabled ? "ๆณจๅไปปๅกๅทฒๅๆญข" : "ๆณจๅไปปๅกๅทฒๅฏๅจ");
|
| 407 |
+
} catch (error) {
|
| 408 |
+
toast.error(error instanceof Error ? error.message : "ๅๆขๆณจๅ็ถๆๅคฑ่ดฅ");
|
| 409 |
+
} finally {
|
| 410 |
+
set({ isSavingRegister: false });
|
| 411 |
+
}
|
| 412 |
+
},
|
| 413 |
+
|
| 414 |
+
resetRegister: async () => {
|
| 415 |
+
set({ isSavingRegister: true });
|
| 416 |
+
try {
|
| 417 |
+
const data = await resetRegisterApi();
|
| 418 |
+
set({ registerConfig: data.register });
|
| 419 |
+
toast.success("ๆณจๅ็ป่ฎกๅทฒ้็ฝฎ");
|
| 420 |
+
} catch (error) {
|
| 421 |
+
toast.error(error instanceof Error ? error.message : "้็ฝฎๆณจๅ็ป่ฎกๅคฑ่ดฅ");
|
| 422 |
+
} finally {
|
| 423 |
+
set({ isSavingRegister: false });
|
| 424 |
+
}
|
| 425 |
+
},
|
| 426 |
+
|
| 427 |
loadPools: async (silent = false) => {
|
| 428 |
if (!silent) {
|
| 429 |
set({ isLoadingPools: true });
|
web/src/components/top-nav.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { cn } from "@/lib/utils";
|
|
| 12 |
const adminNavItems = [
|
| 13 |
{ href: "/image", label: "็ปๅพ" },
|
| 14 |
{ href: "/accounts", label: "ๅทๆฑ ็ฎก็" },
|
|
|
|
| 15 |
{ href: "/image-manager", label: "ๅพ็็ฎก็" },
|
| 16 |
{ href: "/logs", label: "ๆฅๅฟ็ฎก็" },
|
| 17 |
{ href: "/settings", label: "่ฎพ็ฝฎ" },
|
|
|
|
| 12 |
const adminNavItems = [
|
| 13 |
{ href: "/image", label: "็ปๅพ" },
|
| 14 |
{ href: "/accounts", label: "ๅทๆฑ ็ฎก็" },
|
| 15 |
+
{ href: "/register", label: "ๆณจๅๆบ" },
|
| 16 |
{ href: "/image-manager", label: "ๅพ็็ฎก็" },
|
| 17 |
{ href: "/logs", label: "ๆฅๅฟ็ฎก็" },
|
| 18 |
{ href: "/settings", label: "่ฎพ็ฝฎ" },
|
web/src/lib/api.ts
CHANGED
|
@@ -98,6 +98,44 @@ export type UserKey = {
|
|
| 98 |
last_used_at: string | null;
|
| 99 |
};
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
export async function login(authKey: string) {
|
| 102 |
const normalizedAuthKey = String(authKey || "").trim();
|
| 103 |
return httpRequest<LoginResponse>("/auth/login", {
|
|
@@ -245,6 +283,29 @@ export async function deleteUserKey(keyId: string) {
|
|
| 245 |
});
|
| 246 |
}
|
| 247 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
// โโ CPA (CLIProxyAPI) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 249 |
|
| 250 |
export type CPAPool = {
|
|
|
|
| 98 |
last_used_at: string | null;
|
| 99 |
};
|
| 100 |
|
| 101 |
+
export type RegisterConfig = {
|
| 102 |
+
enabled: boolean;
|
| 103 |
+
mail: {
|
| 104 |
+
request_timeout: number;
|
| 105 |
+
wait_timeout: number;
|
| 106 |
+
wait_interval: number;
|
| 107 |
+
providers: Array<Record<string, unknown>>;
|
| 108 |
+
};
|
| 109 |
+
proxy: string;
|
| 110 |
+
total: number;
|
| 111 |
+
threads: number;
|
| 112 |
+
mode: "total" | "quota" | "available";
|
| 113 |
+
target_quota: number;
|
| 114 |
+
target_available: number;
|
| 115 |
+
check_interval: number;
|
| 116 |
+
stats: {
|
| 117 |
+
job_id?: string;
|
| 118 |
+
success: number;
|
| 119 |
+
fail: number;
|
| 120 |
+
done: number;
|
| 121 |
+
running: number;
|
| 122 |
+
threads: number;
|
| 123 |
+
elapsed_seconds?: number;
|
| 124 |
+
avg_seconds?: number;
|
| 125 |
+
success_rate?: number;
|
| 126 |
+
current_quota?: number;
|
| 127 |
+
current_available?: number;
|
| 128 |
+
started_at?: string;
|
| 129 |
+
updated_at?: string;
|
| 130 |
+
finished_at?: string;
|
| 131 |
+
};
|
| 132 |
+
logs?: Array<{
|
| 133 |
+
time: string;
|
| 134 |
+
text: string;
|
| 135 |
+
level: string;
|
| 136 |
+
}>;
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
export async function login(authKey: string) {
|
| 140 |
const normalizedAuthKey = String(authKey || "").trim();
|
| 141 |
return httpRequest<LoginResponse>("/auth/login", {
|
|
|
|
| 283 |
});
|
| 284 |
}
|
| 285 |
|
| 286 |
+
export async function fetchRegisterConfig() {
|
| 287 |
+
return httpRequest<{ register: RegisterConfig }>("/api/register");
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
export async function updateRegisterConfig(updates: Partial<RegisterConfig>) {
|
| 291 |
+
return httpRequest<{ register: RegisterConfig }>("/api/register", {
|
| 292 |
+
method: "POST",
|
| 293 |
+
body: updates,
|
| 294 |
+
});
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
export async function startRegister() {
|
| 298 |
+
return httpRequest<{ register: RegisterConfig }>("/api/register/start", { method: "POST" });
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
export async function stopRegister() {
|
| 302 |
+
return httpRequest<{ register: RegisterConfig }>("/api/register/stop", { method: "POST" });
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
export async function resetRegister() {
|
| 306 |
+
return httpRequest<{ register: RegisterConfig }>("/api/register/reset", { method: "POST" });
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
// โโ CPA (CLIProxyAPI) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 310 |
|
| 311 |
export type CPAPool = {
|