|
|
@@ -1635,6 +1635,105 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
text = f"{caption}\n{image_url}"
|
|
|
return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata)
|
|
|
|
|
|
+ async def _materialise_image(self, image_url: str) -> Tuple[Optional[str], bool]:
|
|
|
+ """Resolve one ``send_multiple_images`` entry to a local file path.
|
|
|
+
|
|
|
+ Accepts ``http(s)://`` URLs (downloaded to a temp file), ``file://``
|
|
|
+ URIs and bare paths. Returns ``(path, is_temp)`` — the caller unlinks
|
|
|
+ when ``is_temp``. ``(None, False)`` means the entry is unusable.
|
|
|
+ """
|
|
|
+ import tempfile
|
|
|
+ from urllib.parse import unquote as _unquote
|
|
|
+
|
|
|
+ if image_url.startswith(("http://", "https://")):
|
|
|
+ parsed = urlsplit(image_url)
|
|
|
+ ext = os.path.splitext(parsed.path)[1] or ".png"
|
|
|
+ tmp_fd, tmp_path = tempfile.mkstemp(suffix=ext, prefix="chatto_img_")
|
|
|
+ os.close(tmp_fd)
|
|
|
+ try:
|
|
|
+ data = await self._download_attachment_bytes(image_url)
|
|
|
+ with open(tmp_path, "wb") as f:
|
|
|
+ f.write(data)
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning("Chatto: image download failed for %s: %s", image_url, e)
|
|
|
+ try:
|
|
|
+ os.unlink(tmp_path)
|
|
|
+ except OSError:
|
|
|
+ pass
|
|
|
+ return None, False
|
|
|
+ return tmp_path, True
|
|
|
+
|
|
|
+ local = image_url
|
|
|
+ if local.startswith("file://"):
|
|
|
+ local = _unquote(urlsplit(local).path)
|
|
|
+ return self.validate_media_delivery_path(local), False
|
|
|
+
|
|
|
+ async def send_multiple_images(
|
|
|
+ self,
|
|
|
+ chat_id: str,
|
|
|
+ images: List[Tuple[str, str]],
|
|
|
+ metadata: Optional[Dict[str, Any]] = None,
|
|
|
+ human_delay: float = 0.0,
|
|
|
+ ) -> None:
|
|
|
+ """Send a batch of images as ONE message with several attachments.
|
|
|
+
|
|
|
+ The base implementation posts each image separately; a Chatto message
|
|
|
+ carries a list of attachment assets, so a batch belongs in a single
|
|
|
+ message (and a single notification).
|
|
|
+
|
|
|
+ ``human_delay`` is ignored deliberately — there is only one outbound
|
|
|
+ call to pace. Entries that can't be fetched are dropped with a warning;
|
|
|
+ if nothing survives, we fall back to the base class so the user still
|
|
|
+ gets the links.
|
|
|
+
|
|
|
+ BasePlatformAdapter override
|
|
|
+ """
|
|
|
+ if len(images or []) < 2:
|
|
|
+ await super().send_multiple_images(
|
|
|
+ chat_id, images, metadata=metadata, human_delay=human_delay,
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ asset_ids: List[str] = []
|
|
|
+ captions: List[str] = []
|
|
|
+ for image_url, alt_text in images:
|
|
|
+ path, is_temp = await self._materialise_image(image_url)
|
|
|
+ if not path:
|
|
|
+ logger.warning("Chatto: skipping unusable image %s", image_url)
|
|
|
+ continue
|
|
|
+ try:
|
|
|
+ asset_id = await self._upload_asset(str(chat_id), path)
|
|
|
+ finally:
|
|
|
+ if is_temp:
|
|
|
+ try:
|
|
|
+ os.unlink(path)
|
|
|
+ except OSError:
|
|
|
+ pass
|
|
|
+ if not asset_id:
|
|
|
+ logger.warning("Chatto: upload failed for image %s", image_url)
|
|
|
+ continue
|
|
|
+ asset_ids.append(asset_id)
|
|
|
+ if alt_text:
|
|
|
+ captions.append(alt_text)
|
|
|
+
|
|
|
+ if not asset_ids:
|
|
|
+ logger.warning(
|
|
|
+ "Chatto: no image survived upload, falling back to per-image delivery",
|
|
|
+ )
|
|
|
+ await super().send_multiple_images(
|
|
|
+ chat_id, images, metadata=metadata, human_delay=human_delay,
|
|
|
+ )
|
|
|
+ return
|
|
|
+
|
|
|
+ if len(asset_ids) < len(images):
|
|
|
+ logger.warning(
|
|
|
+ "Chatto: sending %d of %d images — the rest could not be uploaded",
|
|
|
+ len(asset_ids), len(images),
|
|
|
+ )
|
|
|
+
|
|
|
+ await self._post_attachment_message(
|
|
|
+ chat_id, asset_ids, "\n".join(captions) or None, None, metadata,
|
|
|
+ )
|
|
|
|
|
|
|
|
|
|