diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index eaf457fc0..aebae49b4 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -669,6 +669,7 @@ class DiscordAdapter(BasePlatformAdapter): chat_id: str, file_path: str, caption: Optional[str] = None, + file_name: Optional[str] = None, ) -> SendResult: """Send a local file as a Discord attachment.""" if not self._client: @@ -680,7 +681,7 @@ class DiscordAdapter(BasePlatformAdapter): if not channel: return SendResult(success=False, error=f"Channel {chat_id} not found") - filename = os.path.basename(file_path) + filename = file_name or os.path.basename(file_path) with open(file_path, "rb") as fh: file = discord.File(fh, filename=filename) msg = await channel.send(content=caption if caption else None, file=file) @@ -1141,6 +1142,41 @@ class DiscordAdapter(BasePlatformAdapter): exc_info=True, ) return await super().send_image(chat_id, image_url, caption, reply_to) + + async def send_video( + self, + chat_id: str, + video_path: str, + caption: Optional[str] = None, + reply_to: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + ) -> SendResult: + """Send a local video file natively as a Discord attachment.""" + try: + return await self._send_file_attachment(chat_id, video_path, caption) + except FileNotFoundError: + return SendResult(success=False, error=f"Video file not found: {video_path}") + except Exception as e: # pragma: no cover - defensive logging + logger.error("[%s] Failed to send local video, falling back to base adapter: %s", self.name, e, exc_info=True) + return await super().send_video(chat_id, video_path, caption, reply_to, metadata=metadata) + + async def send_document( + self, + chat_id: str, + file_path: str, + caption: Optional[str] = None, + file_name: Optional[str] = None, + reply_to: Optional[str] = None, + metadata: Optional[Dict[str, Any]] = None, + ) -> SendResult: + """Send an arbitrary file natively as a Discord attachment.""" + try: + return await self._send_file_attachment(chat_id, file_path, caption, file_name=file_name) + except FileNotFoundError: + return SendResult(success=False, error=f"File not found: {file_path}") + except Exception as e: # pragma: no cover - defensive logging + logger.error("[%s] Failed to send document, falling back to base adapter: %s", self.name, e, exc_info=True) + return await super().send_document(chat_id, file_path, caption, file_name, reply_to, metadata=metadata) async def send_typing(self, chat_id: str, metadata=None) -> None: """Send typing indicator.""" diff --git a/tests/gateway/test_send_image_file.py b/tests/gateway/test_send_image_file.py index bf2437345..847ede90e 100644 --- a/tests/gateway/test_send_image_file.py +++ b/tests/gateway/test_send_image_file.py @@ -199,6 +199,57 @@ class TestDiscordSendImageFile: assert result.message_id == "99" mock_channel.send.assert_awaited_once() + def test_send_document_uploads_file_attachment(self, adapter, tmp_path): + """send_document should upload a native Discord attachment.""" + pdf = tmp_path / "sample.pdf" + pdf.write_bytes(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n") + + mock_channel = MagicMock() + mock_msg = MagicMock() + mock_msg.id = 100 + mock_channel.send = AsyncMock(return_value=mock_msg) + adapter._client.get_channel = MagicMock(return_value=mock_channel) + + with patch.object(discord_mod_ref, "File", MagicMock()) as file_cls: + result = _run( + adapter.send_document( + chat_id="67890", + file_path=str(pdf), + file_name="renamed.pdf", + metadata={"thread_id": "123"}, + ) + ) + + assert result.success + assert result.message_id == "100" + assert "file" in mock_channel.send.call_args.kwargs + assert file_cls.call_args.kwargs["filename"] == "renamed.pdf" + + def test_send_video_uploads_file_attachment(self, adapter, tmp_path): + """send_video should upload a native Discord attachment.""" + video = tmp_path / "clip.mp4" + video.write_bytes(b"\x00\x00\x00\x18ftypmp42" + b"\x00" * 50) + + mock_channel = MagicMock() + mock_msg = MagicMock() + mock_msg.id = 101 + mock_channel.send = AsyncMock(return_value=mock_msg) + adapter._client.get_channel = MagicMock(return_value=mock_channel) + + with patch.object(discord_mod_ref, "File", MagicMock()) as file_cls: + result = _run( + adapter.send_video( + chat_id="67890", + video_path=str(video), + metadata={"thread_id": "123"}, + ) + ) + + assert result.success + assert result.message_id == "101" + assert "file" in mock_channel.send.call_args.kwargs + assert file_cls.call_args.kwargs["filename"] == "clip.mp4" + def test_returns_error_when_file_missing(self, adapter): result = _run( adapter.send_image_file(chat_id="67890", image_path="/nonexistent.png")