feat(dingtalk): add disable_private_chat to reject 1:1 DMs
Add a `disable_private_chat` config flag (JSON alias `disablePrivateChat`,
default False) to the DingTalk channel. When enabled, any non-group (1:1)
message is rejected with a Chinese notice directing the user to group chat
("该机器人未开启私聊,请在群聊中与我对话。") before permission/pairing logic
runs, so even allowlisted senders are redirected. Group messages are
unaffected.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
committed by
Xubin Ren
co-authored by
Claude
parent
68717937e8
commit
14e692e40d
@@ -175,6 +175,7 @@ class DingTalkConfig(Base):
|
||||
allow_remote_media_redirects: bool = False
|
||||
remote_media_redirect_allowed_hosts: list[str] = Field(default_factory=list)
|
||||
group_user_isolation: bool = False # If True, each user in group chat gets their own session
|
||||
disable_private_chat: bool = False # If True, reject 1:1 DMs with a notice; group chats only
|
||||
|
||||
|
||||
class DingTalkChannel(BaseChannel):
|
||||
@@ -750,6 +751,21 @@ class DingTalkChannel(BaseChannel):
|
||||
session_key = None
|
||||
if is_group and self.config.group_user_isolation:
|
||||
session_key = f"{self.name}:group:{conversation_id}:{sender_id}"
|
||||
|
||||
if not is_group and self.config.disable_private_chat:
|
||||
# Private chat is disabled: reply with a notice and drop the
|
||||
# message before any permission/pairing logic runs, so even
|
||||
# allowlisted users are redirected to group chat.
|
||||
self.logger.info("private chat disabled; rejecting DM from {}", sender_name)
|
||||
await self.send(
|
||||
OutboundMessage(
|
||||
channel=self.name,
|
||||
chat_id=str(chat_id),
|
||||
content="该机器人未开启私聊,请在群聊中与我对话。",
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
await self._handle_message(
|
||||
sender_id=sender_id,
|
||||
chat_id=chat_id,
|
||||
|
||||
@@ -153,6 +153,85 @@ async def test_group_user_isolation_true_separates_sessions() -> None:
|
||||
assert msg1.chat_id == msg2.chat_id == "group:conv123"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dm_rejected_when_private_chat_disabled(monkeypatch) -> None:
|
||||
"""With disable_private_chat=True, a 1:1 DM is rejected: nothing reaches the
|
||||
bus (no session is created) and the bot replies with a notice directing the
|
||||
user to group chat. Even allowlisted senders are blocked in DMs."""
|
||||
config = DingTalkConfig(
|
||||
client_id="app",
|
||||
client_secret="secret",
|
||||
allow_from=["*"], # even allowlisted senders are blocked in DMs
|
||||
disable_private_chat=True,
|
||||
)
|
||||
bus = MessageBus()
|
||||
channel = DingTalkChannel(config, bus)
|
||||
|
||||
async def fake_get_token():
|
||||
return "test-token"
|
||||
|
||||
monkeypatch.setattr(channel, "_get_access_token", fake_get_token)
|
||||
channel._http = _FakeHttp()
|
||||
|
||||
await channel._on_message(
|
||||
"hello",
|
||||
sender_id="user1",
|
||||
sender_name="Alice",
|
||||
conversation_type="1",
|
||||
)
|
||||
|
||||
# No inbound message was published -> no session created
|
||||
assert bus.inbound.empty()
|
||||
|
||||
# A notice was sent back to the DM user via the private-chat API
|
||||
assert len(channel._http.calls) == 1
|
||||
call = channel._http.calls[0]
|
||||
assert call["url"] == "https://api.dingtalk.com/v1.0/robot/oToMessages/batchSend"
|
||||
assert call["json"]["msgKey"] == "sampleMarkdown"
|
||||
assert call["json"]["userIds"] == ["user1"]
|
||||
assert "该机器人未开启私聊,请在群聊中与我对话。" in call["json"]["msgParam"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dm_allowed_when_private_chat_not_disabled() -> None:
|
||||
"""By default (disable_private_chat=False), a 1:1 DM still reaches the bus."""
|
||||
config = DingTalkConfig(client_id="app", client_secret="secret", allow_from=["*"])
|
||||
bus = MessageBus()
|
||||
channel = DingTalkChannel(config, bus)
|
||||
|
||||
await channel._on_message(
|
||||
"hello",
|
||||
sender_id="user1",
|
||||
sender_name="Alice",
|
||||
conversation_type="1",
|
||||
)
|
||||
|
||||
msg = await bus.consume_inbound()
|
||||
assert msg.chat_id == "user1"
|
||||
assert msg.metadata["conversation_type"] == "1"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_group_message_allowed_when_private_chat_disabled() -> None:
|
||||
"""Disabling private chat must not affect group messages."""
|
||||
config = DingTalkConfig(
|
||||
client_id="app", client_secret="secret", allow_from=["*"], disable_private_chat=True
|
||||
)
|
||||
bus = MessageBus()
|
||||
channel = DingTalkChannel(config, bus)
|
||||
|
||||
await channel._on_message(
|
||||
"hello",
|
||||
sender_id="user1",
|
||||
sender_name="Alice",
|
||||
conversation_type="2",
|
||||
conversation_id="conv123",
|
||||
)
|
||||
|
||||
msg = await bus.consume_inbound()
|
||||
assert msg.chat_id == "group:conv123"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_group_send_uses_group_messages_api() -> None:
|
||||
config = DingTalkConfig(client_id="app", client_secret="secret", allow_from=["*"])
|
||||
|
||||
Reference in New Issue
Block a user