Skip to content

Commit c039df5

Browse files
committed
fix(register): harden OTP stage gating and adult profile generation
1 parent 0743b1c commit c039df5

File tree

5 files changed

+219
-43
lines changed

5 files changed

+219
-43
lines changed

src/config/constants.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
import random
6-
from datetime import datetime
6+
from datetime import datetime, date, timedelta
77
from enum import Enum
88
from typing import Dict, List, Tuple
99

@@ -282,20 +282,26 @@ def generate_random_user_info() -> dict:
282282
# 注册阶段使用更像真人资料的姓名格式,降低单名触发风控的概率。
283283
name = f"{random.choice(FIRST_NAMES)} {random.choice(LAST_NAMES)}"
284284

285-
# 生成随机生日(18-45岁)
286-
current_year = datetime.now().year
287-
birth_year = random.randint(current_year - 45, current_year - 18)
288-
birth_month = random.randint(1, 12)
289-
# 根据月份确定天数
290-
if birth_month in [1, 3, 5, 7, 8, 10, 12]:
291-
birth_day = random.randint(1, 31)
292-
elif birth_month in [4, 6, 9, 11]:
293-
birth_day = random.randint(1, 30)
294-
else:
295-
# 2月,简化处理
296-
birth_day = random.randint(1, 28)
297-
298-
birthdate = f"{birth_year}-{birth_month:02d}-{birth_day:02d}"
285+
# 生成随机生日(按“当天年龄”精确计算,避免出现未满 18 周岁的边界值)。
286+
# 这里默认 21~45 岁,降低生日边界导致的注册风险。
287+
min_age = 21
288+
max_age = 45
289+
290+
today = date.today()
291+
292+
def _years_ago(base: date, years: int) -> date:
293+
try:
294+
return base.replace(year=base.year - years)
295+
except ValueError:
296+
# 兼容 2/29
297+
return base.replace(month=2, day=28, year=base.year - years)
298+
299+
latest_birth = _years_ago(today, min_age) # 最年轻(也已满 min_age)
300+
oldest_birth = _years_ago(today, max_age) # 最年长
301+
span_days = max((latest_birth - oldest_birth).days, 0)
302+
random_offset = random.randint(0, span_days)
303+
birth_day = oldest_birth + timedelta(days=random_offset)
304+
birthdate = birth_day.strftime("%Y-%m-%d")
299305

300306
return {
301307
"name": name,

src/core/register.py

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1741,8 +1741,10 @@ def _complete_chatgpt_bridge_stage(self, result: RegistrationResult, *, final_ur
17411741
"warning" if is_chatgpt_stage else "info",
17421742
)
17431743
if is_chatgpt_stage:
1744-
self._log(f"{log_prefix}先主动重发一次 OTP,确保当前 bridge 阶段拿到新验证码...", "warning")
1745-
self._send_verification_code(referer="https://auth.openai.com/email-verification")
1744+
self._log(
1745+
f"{log_prefix}优先等待当前阶段系统自动发送的 OTP,避免过早重发导致旧验证码混入...",
1746+
"warning",
1747+
)
17461748
self._prepare_expected_login_otp(grace_seconds=OTP_CREATED_AT_GRACE_SECONDS)
17471749
elif "auth.openai.com/log-in/password" in current_url:
17481750
self._log(f"{log_prefix}已直达密码页,先提交密码再继续...")
@@ -2641,12 +2643,6 @@ def _complete_token_exchange_native_backup(self, result: RegistrationResult) ->
26412643
原生入口对齐备份版收尾链路:
26422644
登录验证码 -> Workspace -> redirect -> OAuth callback -> token 入袋。
26432645
"""
2644-
def _is_registration_gate_url(url: str) -> bool:
2645-
u = str(url or "").strip().lower()
2646-
if not u:
2647-
return False
2648-
return ("auth.openai.com/about-you" in u) or ("auth.openai.com/add-phone" in u)
2649-
26502646
if not self._complete_login_otp_once(result, fetch_timeout=120):
26512647
return False
26522648

@@ -2667,12 +2663,12 @@ def _is_registration_gate_url(url: str) -> bool:
26672663
result.workspace_id = workspace_id
26682664

26692665
continue_url = ""
2670-
if otp_continue and _is_registration_gate_url(otp_continue):
2666+
if otp_continue and self._is_registration_gate_url(otp_continue):
26712667
self._log("OTP 返回 continue_url 指向注册门页(about-you/add-phone),本轮收尾忽略该地址")
26722668
otp_continue = ""
26732669

26742670
cached_continue = str(self._create_account_continue_url or "").strip()
2675-
if cached_continue and _is_registration_gate_url(cached_continue):
2671+
if cached_continue and self._is_registration_gate_url(cached_continue):
26762672
self._log("create_account 缓存 continue_url 指向注册门页(about-you/add-phone),本轮收尾忽略该地址")
26772673
cached_continue = ""
26782674

@@ -3669,6 +3665,35 @@ def _verify_email_otp_with_retry(
36693665
attempted_codes.add(code)
36703666

36713667
if self._validate_verification_code(code):
3668+
if chatgpt_stage:
3669+
continue_url = str(self._last_validate_otp_continue_url or "").strip()
3670+
lowered_continue = continue_url.lower()
3671+
3672+
# ChatGPT 补会话阶段命中注册门页通常是旧会话链路,不应视为成功。
3673+
if self._is_registration_gate_url(continue_url):
3674+
if (not resend_after_invalid_triggered) and attempt < max_attempts:
3675+
resend_after_invalid_triggered = True
3676+
self._log(
3677+
f"{stage_label}命中注册门页 continue_url(about-you/add-phone),疑似旧会话验证码;主动重发一次 OTP...",
3678+
"warning",
3679+
)
3680+
self._send_verification_code(referer="https://auth.openai.com/email-verification")
3681+
self._prepare_expected_login_otp(grace_seconds=OTP_CREATED_AT_GRACE_SECONDS)
3682+
3683+
if attempt < max_attempts:
3684+
self._log(
3685+
f"{stage_label}校验成功但 continue_url 仍指向注册门页,继续等待当前登录阶段新验证码...",
3686+
"warning",
3687+
)
3688+
time.sleep(2)
3689+
continue
3690+
return False
3691+
3692+
# 回调明确 access_denied 时直接结束当前阶段,避免无意义循环。
3693+
if ("chatgpt.com/api/auth/callback/openai" in lowered_continue) and ("error=access_denied" in lowered_continue):
3694+
self._log(f"{stage_label}回调返回 access_denied,终止当前验证码阶段重试", "warning")
3695+
return False
3696+
36723697
return True
36733698

36743699
if chatgpt_stage:
@@ -3909,16 +3934,19 @@ def _resolve_workspace_selection_referer(self, referer_url: Optional[str] = None
39093934
or "https://auth.openai.com/sign-in-with-chatgpt/codex/consent"
39103935
).strip()
39113936

3937+
@staticmethod
3938+
def _is_registration_gate_url(url: str) -> bool:
3939+
lowered = str(url or "").strip().lower()
3940+
if not lowered:
3941+
return False
3942+
return ("auth.openai.com/about-you" in lowered) or ("auth.openai.com/add-phone" in lowered)
3943+
39123944
def _fetch_workspace_selection_context(
39133945
self,
39143946
referer_url: Optional[str] = None,
39153947
*,
39163948
force_probe: bool = False,
39173949
) -> Tuple[str, str, Dict[str, Any]]:
3918-
def _is_registration_gate_url(url: str) -> bool:
3919-
lowered = str(url or "").strip().lower()
3920-
return bool(lowered) and ("about-you" in lowered or "add-phone" in lowered)
3921-
39223950
raw_referer = str(
39233951
referer_url
39243952
or self._last_validate_otp_continue_url
@@ -3927,7 +3955,7 @@ def _is_registration_gate_url(url: str) -> bool:
39273955
).strip()
39283956
consent_page_url = raw_referer or self._resolve_workspace_selection_referer(referer_url)
39293957
has_explicit_consent_url = _looks_like_codex_consent_url(raw_referer)
3930-
should_probe_default_consent = _is_registration_gate_url(consent_page_url) or (
3958+
should_probe_default_consent = self._is_registration_gate_url(consent_page_url) or (
39313959
force_probe and not has_explicit_consent_url
39323960
)
39333961
if (not consent_page_url) or should_probe_default_consent:
@@ -3945,7 +3973,7 @@ def _is_registration_gate_url(url: str) -> bool:
39453973
timeout=20,
39463974
)
39473975
response_url = str(getattr(response, "url", request_url) or request_url).strip()
3948-
if response_url and ("localhost:1455" not in response_url.lower()) and (not _is_registration_gate_url(response_url)):
3976+
if response_url and ("localhost:1455" not in response_url.lower()) and (not self._is_registration_gate_url(response_url)):
39493977
consent_page_url = response_url
39503978
consent_text = str(getattr(response, "text", "") or "")
39513979
except Exception as exc:

src/services/luckmail_mail.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ def __init__(self, config: Dict[str, Any] = None, name: str = None):
124124
self._orders_by_email: Dict[str, Dict[str, Any]] = {}
125125
# 记录每个订单/Token 最近返回过的验证码,避免后续阶段反复拿到旧码。
126126
self._recent_codes_by_order: Dict[str, Dict[str, float]] = {}
127+
# 记录每个订单/Token 最近一次成功使用的 message_id,避免重复消费旧邮件。
128+
self._last_used_message_ids: Dict[str, str] = {}
129+
# 最近一次成功取码的元信息,供上层做验证码去重。
130+
self.last_verification_meta: Dict[str, Any] = {}
127131
self._data_dir = Path(__file__).resolve().parents[2] / "data"
128132
self._registered_file = self._data_dir / "luckmail_registered_emails.json"
129133
self._failed_file = self._data_dir / "luckmail_failed_emails.json"
@@ -198,6 +202,53 @@ def _remember_code(self, order_key: str, code: str, now: Optional[float] = None)
198202
for key in stale:
199203
order_cache.pop(key, None)
200204

205+
@staticmethod
206+
def _parse_mail_timestamp(value: Any) -> Optional[float]:
207+
text = str(value or "").strip()
208+
if not text:
209+
return None
210+
# epoch / epoch_ms
211+
try:
212+
numeric = float(text)
213+
if numeric > 10_000_000_000: # ms
214+
numeric = numeric / 1000.0
215+
if numeric > 0:
216+
return numeric
217+
except Exception:
218+
pass
219+
220+
# ISO datetime
221+
normalized = text.replace("Z", "+00:00")
222+
try:
223+
dt = datetime.fromisoformat(normalized)
224+
if dt.tzinfo is None:
225+
dt = dt.replace(tzinfo=timezone.utc)
226+
return dt.timestamp()
227+
except Exception:
228+
return None
229+
230+
def _extract_token_mail_meta(self, result: Any) -> Dict[str, Any]:
231+
mail_payload = self._extract_field(result, "mail") or {}
232+
if not isinstance(mail_payload, dict):
233+
mail_payload = {}
234+
message_id = str(
235+
mail_payload.get("message_id")
236+
or mail_payload.get("id")
237+
or mail_payload.get("mail_id")
238+
or ""
239+
).strip()
240+
received_at_raw = (
241+
mail_payload.get("received_at")
242+
or mail_payload.get("created_at")
243+
or mail_payload.get("timestamp")
244+
or ""
245+
)
246+
mail_ts = self._parse_mail_timestamp(received_at_raw)
247+
return {
248+
"message_id": message_id,
249+
"mail_ts": mail_ts,
250+
}
251+
201252
def _now_iso(self) -> str:
202253
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
203254

@@ -904,6 +955,7 @@ def get_verification_code(
904955
pattern: str = OTP_CODE_PATTERN,
905956
otp_sent_at: Optional[float] = None,
906957
) -> Optional[str]:
958+
self.last_verification_meta = {}
907959
order_info = self._find_order(email=email, email_id=email_id)
908960

909961
token = ""
@@ -970,6 +1022,21 @@ def get_verification_code(
9701022
return None
9711023

9721024
now_ts = time.time()
1025+
mail_meta = self._extract_token_mail_meta(result)
1026+
message_id = str(mail_meta.get("message_id") or "").strip()
1027+
mail_ts = mail_meta.get("mail_ts")
1028+
1029+
if otp_sent_at and (mail_ts is not None) and (mail_ts + 2 < float(otp_sent_at)):
1030+
# 明确早于本轮发码时间窗口,判定旧邮件。
1031+
time.sleep(poll_interval)
1032+
continue
1033+
1034+
if message_id:
1035+
last_message_id = str(self._last_used_message_ids.get(code_key) or "").strip()
1036+
if last_message_id and message_id == last_message_id:
1037+
time.sleep(poll_interval)
1038+
continue
1039+
9731040
if otp_guard_until and now_ts < otp_guard_until and self._is_recent_code(code_key, code, now_ts):
9741041
time.sleep(poll_interval)
9751042
continue
@@ -980,11 +1047,20 @@ def get_verification_code(
9801047
continue
9811048

9821049
self._remember_code(code_key, code, now_ts)
1050+
if message_id:
1051+
self._last_used_message_ids[code_key] = message_id
1052+
self.last_verification_meta = {
1053+
"mail_id": message_id or code_key,
1054+
"mail_ts": mail_ts if mail_ts is not None else now_ts,
1055+
"code": code,
1056+
"fingerprint": f"{mail_ts if mail_ts is not None else now_ts}|{message_id or code_key}|{code}",
1057+
}
9831058
self.update_status(True)
9841059
return code
9851060

9861061
time.sleep(poll_interval)
9871062

1063+
self.last_verification_meta = {}
9881064
return None
9891065

9901066
def list_emails(self, **kwargs) -> List[Dict[str, Any]]:

src/web/routes/email.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -325,18 +325,6 @@ async def get_service_types():
325325
{"name": "domain", "label": "邮箱域名", "required": False, "placeholder": "example.com"},
326326
]
327327
},
328-
{
329-
"value": "cloudmail",
330-
"label": "CloudMail",
331-
"description": "CloudMail 自部署 Cloudflare Worker 邮箱服务,使用管理口令创建邮箱并轮询验证码",
332-
"config_fields": [
333-
{"name": "base_url", "label": "API 地址", "required": True, "placeholder": "https://cloudmail.example.com"},
334-
{"name": "admin_password", "label": "Admin 密码", "required": True, "secret": True},
335-
{"name": "domain", "label": "邮箱域名", "required": True, "placeholder": "example.com"},
336-
{"name": "enable_prefix", "label": "启用前缀", "required": False, "default": True},
337-
{"name": "timeout", "label": "超时时间", "required": False, "default": 30},
338-
]
339-
},
340328
{
341329
"value": "imap_mail",
342330
"label": "IMAP 邮箱",

0 commit comments

Comments
 (0)