test(dingtalk): cover group reply routing
This commit is contained in:
@@ -70,19 +70,24 @@ class NanobotDingTalkHandler(CallbackHandler):
|
|||||||
sender_id = chatbot_msg.sender_staff_id or chatbot_msg.sender_id
|
sender_id = chatbot_msg.sender_staff_id or chatbot_msg.sender_id
|
||||||
sender_name = chatbot_msg.sender_nick or "Unknown"
|
sender_name = chatbot_msg.sender_nick or "Unknown"
|
||||||
|
|
||||||
# Extract conversation info
|
|
||||||
conversation_type = message.data.get("conversationType")
|
conversation_type = message.data.get("conversationType")
|
||||||
conversation_id = message.data.get("conversationId") or message.data.get("openConversationId")
|
conversation_id = (
|
||||||
|
message.data.get("conversationId")
|
||||||
if conversation_type == "2" and conversation_id:
|
or message.data.get("openConversationId")
|
||||||
sender_id = f"group:{conversation_id}"
|
)
|
||||||
|
|
||||||
logger.info("Received DingTalk message from {} ({}): {}", sender_name, sender_id, content)
|
logger.info("Received DingTalk message from {} ({}): {}", sender_name, sender_id, content)
|
||||||
|
|
||||||
# Forward to Nanobot via _on_message (non-blocking).
|
# Forward to Nanobot via _on_message (non-blocking).
|
||||||
# Store reference to prevent GC before task completes.
|
# Store reference to prevent GC before task completes.
|
||||||
task = asyncio.create_task(
|
task = asyncio.create_task(
|
||||||
self.channel._on_message(content, sender_id, sender_name)
|
self.channel._on_message(
|
||||||
|
content,
|
||||||
|
sender_id,
|
||||||
|
sender_name,
|
||||||
|
conversation_type,
|
||||||
|
conversation_id,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.channel._background_tasks.add(task)
|
self.channel._background_tasks.add(task)
|
||||||
task.add_done_callback(self.channel._background_tasks.discard)
|
task.add_done_callback(self.channel._background_tasks.discard)
|
||||||
@@ -102,8 +107,8 @@ class DingTalkChannel(BaseChannel):
|
|||||||
Uses WebSocket to receive events via `dingtalk-stream` SDK.
|
Uses WebSocket to receive events via `dingtalk-stream` SDK.
|
||||||
Uses direct HTTP API to send messages (SDK is mainly for receiving).
|
Uses direct HTTP API to send messages (SDK is mainly for receiving).
|
||||||
|
|
||||||
Note: Currently only supports private (1:1) chat. Group messages are
|
Supports both private (1:1) and group chats.
|
||||||
received but replies are sent back as private messages to the sender.
|
Group chat_id is stored with a "group:" prefix to route replies back.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name = "dingtalk"
|
name = "dingtalk"
|
||||||
@@ -435,7 +440,14 @@ class DingTalkChannel(BaseChannel):
|
|||||||
f"[Attachment send failed: {filename}]",
|
f"[Attachment send failed: {filename}]",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _on_message(self, content: str, sender_id: str, sender_name: str) -> None:
|
async def _on_message(
|
||||||
|
self,
|
||||||
|
content: str,
|
||||||
|
sender_id: str,
|
||||||
|
sender_name: str,
|
||||||
|
conversation_type: str | None = None,
|
||||||
|
conversation_id: str | None = None,
|
||||||
|
) -> None:
|
||||||
"""Handle incoming message (called by NanobotDingTalkHandler).
|
"""Handle incoming message (called by NanobotDingTalkHandler).
|
||||||
|
|
||||||
Delegates to BaseChannel._handle_message() which enforces allow_from
|
Delegates to BaseChannel._handle_message() which enforces allow_from
|
||||||
@@ -443,13 +455,16 @@ class DingTalkChannel(BaseChannel):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info("DingTalk inbound: {} from {}", content, sender_name)
|
logger.info("DingTalk inbound: {} from {}", content, sender_name)
|
||||||
|
is_group = conversation_type == "2" and conversation_id
|
||||||
|
chat_id = f"group:{conversation_id}" if is_group else sender_id
|
||||||
await self._handle_message(
|
await self._handle_message(
|
||||||
sender_id=sender_id,
|
sender_id=sender_id,
|
||||||
chat_id=sender_id, # For private chat, chat_id == sender_id
|
chat_id=chat_id,
|
||||||
content=str(content),
|
content=str(content),
|
||||||
metadata={
|
metadata={
|
||||||
"sender_name": sender_name,
|
"sender_name": sender_name,
|
||||||
"platform": "dingtalk",
|
"platform": "dingtalk",
|
||||||
|
"conversation_type": conversation_type,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
66
tests/test_dingtalk_channel.py
Normal file
66
tests/test_dingtalk_channel.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from nanobot.bus.queue import MessageBus
|
||||||
|
from nanobot.channels.dingtalk import DingTalkChannel
|
||||||
|
from nanobot.config.schema import DingTalkConfig
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeResponse:
|
||||||
|
def __init__(self, status_code: int = 200, json_body: dict | None = None) -> None:
|
||||||
|
self.status_code = status_code
|
||||||
|
self._json_body = json_body or {}
|
||||||
|
self.text = "{}"
|
||||||
|
|
||||||
|
def json(self) -> dict:
|
||||||
|
return self._json_body
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeHttp:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.calls: list[dict] = []
|
||||||
|
|
||||||
|
async def post(self, url: str, json=None, headers=None):
|
||||||
|
self.calls.append({"url": url, "json": json, "headers": headers})
|
||||||
|
return _FakeResponse()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_group_message_keeps_sender_id_and_routes_chat_id() -> None:
|
||||||
|
config = DingTalkConfig(client_id="app", client_secret="secret", allow_from=["user1"])
|
||||||
|
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.sender_id == "user1"
|
||||||
|
assert msg.chat_id == "group:conv123"
|
||||||
|
assert msg.metadata["conversation_type"] == "2"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_group_send_uses_group_messages_api() -> None:
|
||||||
|
config = DingTalkConfig(client_id="app", client_secret="secret", allow_from=["*"])
|
||||||
|
channel = DingTalkChannel(config, MessageBus())
|
||||||
|
channel._http = _FakeHttp()
|
||||||
|
|
||||||
|
ok = await channel._send_batch_message(
|
||||||
|
"token",
|
||||||
|
"group:conv123",
|
||||||
|
"sampleMarkdown",
|
||||||
|
{"text": "hello", "title": "Nanobot Reply"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ok is True
|
||||||
|
call = channel._http.calls[0]
|
||||||
|
assert call["url"] == "https://api.dingtalk.com/v1.0/robot/groupMessages/send"
|
||||||
|
assert call["json"]["openConversationId"] == "conv123"
|
||||||
|
assert call["json"]["msgKey"] == "sampleMarkdown"
|
||||||
Reference in New Issue
Block a user