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
|
allow_remote_media_redirects: bool = False
|
||||||
remote_media_redirect_allowed_hosts: list[str] = Field(default_factory=list)
|
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
|
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):
|
class DingTalkChannel(BaseChannel):
|
||||||
@@ -750,6 +751,21 @@ class DingTalkChannel(BaseChannel):
|
|||||||
session_key = None
|
session_key = None
|
||||||
if is_group and self.config.group_user_isolation:
|
if is_group and self.config.group_user_isolation:
|
||||||
session_key = f"{self.name}:group:{conversation_id}:{sender_id}"
|
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(
|
await self._handle_message(
|
||||||
sender_id=sender_id,
|
sender_id=sender_id,
|
||||||
chat_id=chat_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"
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_group_send_uses_group_messages_api() -> None:
|
async def test_group_send_uses_group_messages_api() -> None:
|
||||||
config = DingTalkConfig(client_id="app", client_secret="secret", allow_from=["*"])
|
config = DingTalkConfig(client_id="app", client_secret="secret", allow_from=["*"])
|
||||||
|
|||||||
Reference in New Issue
Block a user