|
@@ -1417,35 +1417,15 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
logger.error("Chatto: upload error: %s", e)
|
|
logger.error("Chatto: upload error: %s", e)
|
|
|
return None
|
|
return None
|
|
|
|
|
|
|
|
- async def send_image_file(
|
|
|
|
|
|
|
+ async def _post_attachment_message(
|
|
|
self,
|
|
self,
|
|
|
chat_id: str,
|
|
chat_id: str,
|
|
|
- file_path: str,
|
|
|
|
|
- caption: Optional[str] = None,
|
|
|
|
|
- reply_to: Optional[str] = None,
|
|
|
|
|
- metadata: Optional[Dict[str, Any]] = None,
|
|
|
|
|
|
|
+ asset_ids: List[str],
|
|
|
|
|
+ caption: Optional[str],
|
|
|
|
|
+ reply_to: Optional[str],
|
|
|
|
|
+ metadata: Optional[Dict[str, Any]],
|
|
|
) -> SendResult:
|
|
) -> SendResult:
|
|
|
- """Send a local image file via the chunked upload API. Do not change signature.
|
|
|
|
|
-
|
|
|
|
|
- BasePlatformAdapter override
|
|
|
|
|
- """
|
|
|
|
|
- # Validate the path is safe
|
|
|
|
|
- safe_path = self.validate_media_delivery_path(file_path)
|
|
|
|
|
- if not safe_path:
|
|
|
|
|
- logger.warning("Chatto: send_image_file — unsafe path %s", file_path)
|
|
|
|
|
- text = "⚠️ Couldn't deliver the image attachment."
|
|
|
|
|
- if caption:
|
|
|
|
|
- text = f"{caption}\n{text}"
|
|
|
|
|
- return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata)
|
|
|
|
|
-
|
|
|
|
|
- asset_id = await self._upload_asset(str(chat_id), safe_path)
|
|
|
|
|
- if not asset_id:
|
|
|
|
|
- # Fallback to a notice
|
|
|
|
|
- text = "⚠️ Couldn't deliver the image attachment."
|
|
|
|
|
- if caption:
|
|
|
|
|
- text = f"{caption}\n{text}"
|
|
|
|
|
- return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata)
|
|
|
|
|
-
|
|
|
|
|
|
|
+ """Post one message carrying already-uploaded assets."""
|
|
|
thread_id = (metadata or {}).get("thread_id")
|
|
thread_id = (metadata or {}).get("thread_id")
|
|
|
if reply_to:
|
|
if reply_to:
|
|
|
thread_id = reply_to
|
|
thread_id = reply_to
|
|
@@ -1457,17 +1437,143 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
return SendResult(success=False, error="Chatto client not available", retryable=True)
|
|
return SendResult(success=False, error="Chatto client not available", retryable=True)
|
|
|
msg = await client.post_message(
|
|
msg = await client.post_message(
|
|
|
room_id=str(chat_id),
|
|
room_id=str(chat_id),
|
|
|
- body=caption or "",
|
|
|
|
|
- attachment_asset_ids=[asset_id],
|
|
|
|
|
|
|
+ body=self.format_message(caption) if caption else "",
|
|
|
|
|
+ attachment_asset_ids=asset_ids,
|
|
|
thread_root_event_id=str(thread_id) if thread_id else "",
|
|
thread_root_event_id=str(thread_id) if thread_id else "",
|
|
|
)
|
|
)
|
|
|
self._mark_seen(msg.id)
|
|
self._mark_seen(msg.id)
|
|
|
|
|
+ self._our_message_ids.add(msg.id)
|
|
|
return SendResult(success=True, message_id=msg.id, raw_response=msg)
|
|
return SendResult(success=True, message_id=msg.id, raw_response=msg)
|
|
|
except ChattoError as e:
|
|
except ChattoError as e:
|
|
|
return SendResult(success=False, error=str(e), retryable=True)
|
|
return SendResult(success=False, error=str(e), retryable=True)
|
|
|
except Exception as e:
|
|
except Exception as e:
|
|
|
return SendResult(success=False, error=str(e), retryable=False)
|
|
return SendResult(success=False, error=str(e), retryable=False)
|
|
|
|
|
|
|
|
|
|
+ async def _send_local_attachment(
|
|
|
|
|
+ self,
|
|
|
|
|
+ chat_id: str,
|
|
|
|
|
+ file_path: str,
|
|
|
|
|
+ caption: Optional[str],
|
|
|
|
|
+ reply_to: Optional[str],
|
|
|
|
|
+ metadata: Optional[Dict[str, Any]],
|
|
|
|
|
+ *,
|
|
|
|
|
+ kind: str,
|
|
|
|
|
+ ) -> SendResult:
|
|
|
|
|
+ """Upload a local file and post it as a native Chatto attachment.
|
|
|
|
|
+
|
|
|
|
|
+ Shared by ``send_image_file``/``send_document``/``send_video``/
|
|
|
|
|
+ ``send_voice`` — the upload mechanics are identical, only the wording of
|
|
|
|
|
+ the failure notice differs. On failure we send that notice as text and
|
|
|
|
|
+ never the host path (it leaks the Hermes home layout).
|
|
|
|
|
+ """
|
|
|
|
|
+ notice = f"⚠️ Couldn't deliver the {kind} attachment."
|
|
|
|
|
+
|
|
|
|
|
+ safe_path = self.validate_media_delivery_path(file_path)
|
|
|
|
|
+ if not safe_path:
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "[%s] send %s: unsafe path %s", self.name, kind, file_path,
|
|
|
|
|
+ )
|
|
|
|
|
+ text = f"{caption}\n{notice}" if caption else notice
|
|
|
|
|
+ return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata)
|
|
|
|
|
+
|
|
|
|
|
+ asset_id = await self._upload_asset(str(chat_id), safe_path)
|
|
|
|
|
+ if not asset_id:
|
|
|
|
|
+ logger.warning(
|
|
|
|
|
+ "[%s] send %s: upload failed for %s", self.name, kind, safe_path,
|
|
|
|
|
+ )
|
|
|
|
|
+ text = f"{caption}\n{notice}" if caption else notice
|
|
|
|
|
+ return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata)
|
|
|
|
|
+
|
|
|
|
|
+ return await self._post_attachment_message(
|
|
|
|
|
+ chat_id, [asset_id], caption, reply_to, metadata,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ 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:
|
|
|
|
|
+ """Send a local image file via the chunked upload API.
|
|
|
|
|
+
|
|
|
|
|
+ The parameter is ``image_path``, not ``file_path``: every caller passes
|
|
|
|
|
+ it by keyword (``gateway/run.py:22354``, ``:22470``, and the base class's
|
|
|
|
|
+ own ``send_multiple_images`` file:// branch), so a renamed parameter
|
|
|
|
|
+ makes each of those raise TypeError and silently degrade to a text
|
|
|
|
|
+ notice.
|
|
|
|
|
+
|
|
|
|
|
+ BasePlatformAdapter override
|
|
|
|
|
+ """
|
|
|
|
|
+ return await self._send_local_attachment(
|
|
|
|
|
+ chat_id, image_path, caption, reply_to, metadata, kind="image",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ 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:
|
|
|
|
|
+ """Send a local file as a native Chatto attachment.
|
|
|
|
|
+
|
|
|
|
|
+ ``file_name`` is the user-facing name the agent chose; Chatto takes the
|
|
|
|
|
+ filename from the upload session, so it only matters for the failure
|
|
|
|
|
+ notice.
|
|
|
|
|
+
|
|
|
|
|
+ BasePlatformAdapter override
|
|
|
|
|
+ """
|
|
|
|
|
+ result = await self._send_local_attachment(
|
|
|
|
|
+ chat_id, file_path, caption, reply_to, metadata, kind="file",
|
|
|
|
|
+ )
|
|
|
|
|
+ if not result.success and file_name:
|
|
|
|
|
+ logger.debug("Chatto: document delivery failed for %s", file_name)
|
|
|
|
|
+ return result
|
|
|
|
|
+
|
|
|
|
|
+ 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:
|
|
|
|
|
+ """Send a local video as a native Chatto attachment (Chatto transcodes
|
|
|
|
|
+ and plays it inline).
|
|
|
|
|
+
|
|
|
|
|
+ BasePlatformAdapter override
|
|
|
|
|
+ """
|
|
|
|
|
+ return await self._send_local_attachment(
|
|
|
|
|
+ chat_id, video_path, caption, reply_to, metadata, kind="video",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ 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:
|
|
|
|
|
+ """Send a local audio file as a native Chatto attachment.
|
|
|
|
|
+
|
|
|
|
|
+ Chatto has no dedicated voice-bubble type, so this is an ordinary audio
|
|
|
|
|
+ attachment — still far better than the base class's text notice.
|
|
|
|
|
+
|
|
|
|
|
+ BasePlatformAdapter override
|
|
|
|
|
+ """
|
|
|
|
|
+ return await self._send_local_attachment(
|
|
|
|
|
+ chat_id, audio_path, caption, reply_to, metadata, kind="audio",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
|
|
|
async def send_image(
|
|
async def send_image(
|
|
|
self,
|
|
self,
|