import logging import time logger = logging.getLogger(__name__) _WINDOW_SECONDS = 24 * 3600 _MAX_MSG_PER_WINDOW = 6 _ENTER_DM_REPLY_WINDOW = 30 _ENTER_DM_MAX_REPLIES_PER_WEBHOOK = 3 _ENTER_DM_MAX_REPLIES_PER_DAY = 3 _ENTER_DM_COOLDOWN_SECONDS = 3600 class DouyinWindowTracker: def __init__(self): self._last_msg_time: dict[str, float] = {} self._msg_count: dict[str, int] = {} self._last_send_time: dict[str, float] = {} self._enter_dm_reply_count: dict[str, int] = {} self._enter_dm_last_reply_time: dict[str, float] = {} def record(self, open_id: str) -> None: self._last_msg_time[open_id] = time.time() def record_send(self, open_id: str) -> None: now = time.time() if open_id not in self._last_msg_time: self._msg_count[open_id] = 0 return if now - self._last_msg_time[open_id] > _WINDOW_SECONDS: self._msg_count[open_id] = 0 self._msg_count[open_id] = self._msg_count.get(open_id, 0) + 1 self._last_send_time[open_id] = now def can_reply(self, open_id: str) -> bool: last = self._last_msg_time.get(open_id, 0) now = time.time() if now - last > _WINDOW_SECONDS: logger.info( "Douyin 24h reply window expired for user %s, last_msg_at=%s", open_id, last, ) return False count = self._msg_count.get(open_id, 0) if count >= _MAX_MSG_PER_WINDOW: logger.warning( "Douyin max message count reached for user %s: %d/%d", open_id, count, _MAX_MSG_PER_WINDOW, ) return False return True def can_enter_dm_reply(self, open_id: str) -> bool: now = time.time() day_key = f"{open_id}:{int(now / 86400)}" daily_count = self._enter_dm_reply_count.get(day_key, 0) if daily_count >= _ENTER_DM_MAX_REPLIES_PER_DAY: logger.info( "Douyin enter-dm daily limit reached for user %s: %d", open_id, daily_count, ) return False last_reply = self._enter_dm_last_reply_time.get(open_id, 0) if last_reply and now - last_reply < _ENTER_DM_COOLDOWN_SECONDS: logger.info( "Douyin enter-dm cooldown active for user %s, remaining=%ds", open_id, int(_ENTER_DM_COOLDOWN_SECONDS - (now - last_reply)), ) return False return True def record_enter_dm_reply(self, open_id: str) -> None: now = time.time() day_key = f"{open_id}:{int(now / 86400)}" self._enter_dm_reply_count[day_key] = self._enter_dm_reply_count.get(day_key, 0) + 1 self._enter_dm_last_reply_time[open_id] = now def remaining_seconds(self, open_id: str) -> float: last = self._last_msg_time.get(open_id, 0) if last == 0: return 0 elapsed = time.time() - last remaining = _WINDOW_SECONDS - elapsed return max(0.0, remaining) def cleanup(self): now = time.time() expired = [k for k, ts in self._last_msg_time.items() if now - ts > _WINDOW_SECONDS] for k in expired: del self._last_msg_time[k] self._msg_count.pop(k, None) self._last_send_time.pop(k, None) stale_enter = [ k for k, ts in self._enter_dm_last_reply_time.items() if now - ts > _ENTER_DM_COOLDOWN_SECONDS * 2 ] for k in stale_enter: self._enter_dm_last_reply_time.pop(k, None)