Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 121 additions & 22 deletions plugins/platforms/buzz/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,33 +675,132 @@ async def send_image(
"""Send an image: local files upload via --file, URLs go as a link."""
local = Path(image_url).expanduser() if not image_url.startswith(("http://", "https://")) else None
if local is not None and local.is_file():
args = [
"messages", "send",
"--channel", str(chat_id),
"--file", str(local),
"--content", "-",
]
if reply_to:
args += ["--reply-to", str(reply_to)]
code, out, err = await self._run_cli(args, input_text=caption or "")
if code != 0:
return SendResult(success=False, error=_cli_error_message(err, code), retryable=code == 2)
try:
data = json.loads(out or "{}")
except ValueError:
data = {}
event_id = data.get("event_id")
if event_id:
self._mark_seen(str(chat_id), str(event_id))
return SendResult(
success=bool(data.get("accepted", True)),
message_id=str(event_id) if event_id else None,
raw_response=data,
return await self._send_file_attachment(
chat_id,
local,
caption=caption,
reply_to=reply_to,
metadata=metadata,
)
# Markdown renders in Buzz, so a URL arrives as a clickable image link.
text = f"{caption}\n{image_url}" if caption else image_url
return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata)

async def _send_file_attachment(
self,
chat_id: str,
file_path: Path,
*,
caption: Optional[str] = None,
reply_to: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
) -> SendResult:
"""Upload a local file and publish it as a native Buzz attachment."""
local = Path(file_path).expanduser()
if not local.is_file():
return SendResult(success=False, error=f"File not found: {local}")
args = [
"messages", "send",
"--channel", str(chat_id),
"--file", str(local),
"--content", "-",
]
reply_target = reply_to or (metadata or {}).get("thread_id")
if reply_target:
args += ["--reply-to", str(reply_target)]
code, out, err = await self._run_cli(args, input_text=caption or "")
if code != 0:
return SendResult(
success=False,
error=_cli_error_message(err, code),
retryable=code == 2,
)
try:
data = json.loads(out or "{}")
except ValueError:
data = {}
event_id = data.get("event_id")
if event_id:
self._mark_seen(str(chat_id), str(event_id))
return SendResult(
success=bool(data.get("accepted", True)),
message_id=str(event_id) if event_id else None,
raw_response=data,
)

async def send_image_file(
self,
chat_id: str,
image_path: str,
caption: Optional[str] = None,
reply_to: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
**kwargs,
) -> SendResult:
"""Upload a local image through Buzz's native ``--file`` path."""
return await self._send_file_attachment(
chat_id,
Path(image_path),
caption=caption,
reply_to=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,
**kwargs,
) -> SendResult:
"""Upload a local document through Buzz's native ``--file`` path."""
return await self._send_file_attachment(
chat_id,
Path(file_path),
caption=caption,
reply_to=reply_to,
metadata=metadata,
)

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,
**kwargs,
) -> SendResult:
"""Upload a local video through Buzz's native ``--file`` path."""
return await self._send_file_attachment(
chat_id,
Path(video_path),
caption=caption,
reply_to=reply_to,
metadata=metadata,
)

async def send_voice(
self,
chat_id: str,
audio_path: str,
caption: Optional[str] = None,
reply_to: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
**kwargs,
) -> SendResult:
"""Upload a local audio file through Buzz's native ``--file`` path."""
return await self._send_file_attachment(
chat_id,
Path(audio_path),
caption=caption,
reply_to=reply_to,
metadata=metadata,
)

async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
chat_id = str(chat_id)
state = self._channel_state.get(chat_id)
Expand Down
38 changes: 38 additions & 0 deletions tests/gateway/test_buzz_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,44 @@ async def test_send_image_local_file_uses_file_flag(self, tmp_path):
args, _stdin = cli.calls[0]
assert args[args.index("--file") + 1] == str(img)

@pytest.mark.asyncio
async def test_send_image_file_uses_native_file_flag_and_thread_metadata(self, tmp_path):
img = tmp_path / "preview.jpg"
img.write_bytes(b"\xff\xd8\xff fake")
adapter = _make_adapter()
cli = _ScriptedCli()
cli.script("messages", "send", {"accepted": True, "event_id": "evt127", "message": ""})
adapter._run_cli = cli

result = await adapter.send_image_file(
CHANNEL,
str(img),
caption="preview",
metadata={"thread_id": "root-event"},
)

assert result.success is True
args, stdin_text = cli.calls[0]
assert args[args.index("--file") + 1] == str(img)
assert args[args.index("--reply-to") + 1] == "root-event"
assert stdin_text == "preview"

@pytest.mark.asyncio
async def test_send_document_uses_native_file_flag(self, tmp_path):
document = tmp_path / "package.zip"
document.write_bytes(b"PK fake")
adapter = _make_adapter()
cli = _ScriptedCli()
cli.script("messages", "send", {"accepted": True, "event_id": "evt128", "message": ""})
adapter._run_cli = cli

result = await adapter.send_document(CHANNEL, str(document), caption="files")

assert result.success is True
args, stdin_text = cli.calls[0]
assert args[args.index("--file") + 1] == str(document)
assert stdin_text == "files"


# ── Lifecycle ─────────────────────────────────────────────────────────────

Expand Down