This commit is contained in:
Your Name
2026-09-01 15:58:34 +08:00
parent 2fc864cc00
commit 89cae5b8cc
8 changed files with 295 additions and 63 deletions
+54 -7
View File
@@ -703,6 +703,35 @@ class DouyinImHttpClient:
return resolved
return int(sess.my_uid or 0)
def _peer_matches_expectation(
self,
conversation_id: str,
peer_uid,
expected_peer_uid: str,
) -> bool:
"""收件人必须与调用方指定的 UID 一致,否则拒发。"""
expected = str(expected_peer_uid or "").strip()
if not expected:
return True
actual = str(peer_uid or "").strip()
if actual == expected:
return True
detail = (
f"发送目标与预期不一致:会话 {conversation_id} 的对方是 {actual or '未知'}"
f"调用方指定的收件人是 {expected},已拒绝发送以免发错人。"
)
self._set_error(detail)
self.last_send_channel_retryable = False
self._log_send_failure(conversation_id, detail)
logger.error(
"Account %s refused send: peer mismatch conv=%s actual=%s expected=%s",
self.account_id,
conversation_id,
actual,
expected,
)
return False
def _log_send_failure(self, conversation_id: str, detail: str) -> None:
system_logger.record(
"私信发送失败",
@@ -1281,11 +1310,17 @@ class DouyinImHttpClient:
conversation_id: str,
content: str,
conversation_short_id: str = "",
expected_peer_uid: str = "",
_bypass_global_queue: bool = False,
) -> bool:
"""通过 IM API 发送 Protobuf 编码的私信(带接口签名)
content 可为纯文本,或 JSON 格式的结构化回复(文本/网址/卡片)。
``expected_peer_uid`` 是调用方认定的收件人 UID。会话归属校验只能保证
「这条会话是本账号的」,保证不了「这个人就是用户想发的人」——界面按昵称
兜底匹配会话时可能选中同名的另一个人。带上它,写入点就能在发出去之前
确认收件人确实是调用方指定的那个。
"""
if not _bypass_global_queue:
# This is the common write entry point used by automatic replies,
@@ -1327,6 +1362,7 @@ class DouyinImHttpClient:
conversation_id,
content,
conversation_short_id=conversation_short_id,
expected_peer_uid=expected_peer_uid,
_bypass_global_queue=True,
)
self.last_send_meta = dict(queued_http.last_send_meta)
@@ -1417,6 +1453,17 @@ class DouyinImHttpClient:
)
return False
conversation_id = normalize_conversation_id(conversation_id, my_uid)
peer_uid = resolve_peer_uid(conversation_id, my_uid)
if not peer_uid:
self._set_error("无法解析对方用户 ID")
self._log_send_failure(conversation_id, "无法从会话 ID 解析对方用户 ID")
return False
if not self._peer_matches_expectation(
conversation_id, peer_uid, expected_peer_uid
):
return False
if not auth.is_sign_ready():
self._set_error("缺少 IM 签名密钥,请用浏览器登录补全 localStorage")
self._log_send_failure(
@@ -1425,13 +1472,6 @@ class DouyinImHttpClient:
)
return False
conversation_id = normalize_conversation_id(conversation_id, my_uid)
peer_uid = resolve_peer_uid(conversation_id, my_uid)
if not peer_uid:
self._set_error("无法解析对方用户 ID")
self._log_send_failure(conversation_id, "无法从会话 ID 解析对方用户 ID")
return False
cached = self.last_send_meta.get(conversation_id, {})
conv_short_id = str(conversation_short_id or "").strip()
@@ -1442,6 +1482,13 @@ class DouyinImHttpClient:
)
if resolved_id:
conversation_id = resolved_id
# 抖音回来的会话 ID 才是真正会被写入的那条:再确认一次收件人没被换掉。
if not self._peer_matches_expectation(
conversation_id,
resolve_peer_uid(conversation_id, my_uid) or peer_uid,
expected_peer_uid,
):
return False
conv_short_id = resolved_short_id or conv_short_id or str(cached.get("conversation_short_id") or "")
ticket = resolved_ticket or str(cached.get("ticket") or "")
+16 -2
View File
@@ -1556,12 +1556,14 @@ class DouyinImService:
conversation_id: str,
content: str,
conversation_short_id: str = "",
expected_peer_uid: str = "",
) -> tuple[bool, Optional[dict]]:
async with self._session_lock:
return await self._send_text_unlocked(
conversation_id,
content,
conversation_short_id=conversation_short_id,
expected_peer_uid=expected_peer_uid,
)
async def _send_text_unlocked(
@@ -1569,6 +1571,7 @@ class DouyinImService:
conversation_id: str,
content: str,
conversation_short_id: str = "",
expected_peer_uid: str = "",
) -> tuple[bool, Optional[dict]]:
"""发送一条私信;若因签名凭证失效(7911)失败,刷新 web_protect 后自动重试一次。
@@ -1580,6 +1583,7 @@ class DouyinImService:
conversation_id,
content,
conversation_short_id=conversation_short_id,
expected_peer_uid=expected_peer_uid,
)
self.last_error = http.last_error
needs_refresh = http.last_send_needs_refresh
@@ -1681,8 +1685,17 @@ class DouyinImService:
except Exception as e:
logger.error(f"on_session_invalid handler error: {e}")
async def send_message(self, conversation_id: str, content: str) -> bool:
"""手动发送私信"""
async def send_message(
self,
conversation_id: str,
content: str,
expected_peer_uid: str = "",
) -> bool:
"""手动发送私信。
``expected_peer_uid`` 由调用方(消息页)指定收件人,写入点会在发出去
之前核对,避免界面按昵称匹配到同名的另一个人。
"""
from .conv_util import normalize_conversation_id
from .auth import DouyinAuth
from .dy_util import DEFAULT_USER_AGENT
@@ -1705,6 +1718,7 @@ class DouyinImService:
conversation_id,
content,
conversation_short_id=str(meta.get("conversation_short_id") or ""),
expected_peer_uid=expected_peer_uid,
)
if sent and resolved:
self._conv_meta[conversation_id] = {