该提交实现了完整的抖音开放平台IM渠道插件,包含: 1. 基础配置与账号管理能力 2. 消息去重、安全策略校验 3. 流式回复、多媒体消息发送 4. Webhook回调处理与事件解析 5. 配对认证与流量限流机制
107 lines
3.6 KiB
Python
107 lines
3.6 KiB
Python
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)
|