|
|
@@ -28,8 +28,6 @@ import hashlib
|
|
|
import logging
|
|
|
import mimetypes
|
|
|
import os
|
|
|
-import ssl
|
|
|
-import urllib.request as _urllib_request
|
|
|
from datetime import UTC, datetime
|
|
|
from enum import StrEnum
|
|
|
from typing import Any, cast
|
|
|
@@ -216,17 +214,6 @@ def _write_file_bytes(path: str, data: bytes) -> None:
|
|
|
f.write(data)
|
|
|
|
|
|
|
|
|
-def _download_to_file(url: str, path: str, timeout: float) -> None:
|
|
|
- """Download ``url`` to ``path`` synchronously (run via asyncio.to_thread)."""
|
|
|
- req = _urllib_request.Request(url, headers={"User-Agent": "Hermes/1.0"})
|
|
|
- try:
|
|
|
- ctx = ssl.create_default_context()
|
|
|
- except Exception:
|
|
|
- ctx = None
|
|
|
- with _urllib_request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
|
|
- _write_file_bytes(path, resp.read())
|
|
|
-
|
|
|
-
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
# Adapter
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
@@ -2270,7 +2257,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
except Exception as e:
|
|
|
return SendResult(success=False, error=str(e), retryable=False)
|
|
|
|
|
|
- async def _send_local_attachment(
|
|
|
+ async def _send_local_file_as_attachment(
|
|
|
self,
|
|
|
chat_id: str,
|
|
|
file_path: str,
|
|
|
@@ -2338,7 +2325,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
|
|
|
BasePlatformAdapter override
|
|
|
"""
|
|
|
- return await self._send_local_attachment(
|
|
|
+ return await self._send_local_file_as_attachment(
|
|
|
chat_id,
|
|
|
image_path,
|
|
|
caption,
|
|
|
@@ -2362,11 +2349,11 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
``file_name`` exists in the base-class signature and is accepted for
|
|
|
compatibility, but Chatto takes the recipient-visible filename from
|
|
|
the upload session (derived from the local path); failures are logged
|
|
|
- and noticed by ``_send_local_attachment`` itself.
|
|
|
+ and noticed by ``_send_local_file_as_attachment`` itself.
|
|
|
|
|
|
BasePlatformAdapter override
|
|
|
"""
|
|
|
- return await self._send_local_attachment(
|
|
|
+ return await self._send_local_file_as_attachment(
|
|
|
chat_id,
|
|
|
file_path,
|
|
|
caption,
|
|
|
@@ -2390,7 +2377,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
|
|
|
BasePlatformAdapter override
|
|
|
"""
|
|
|
- return await self._send_local_attachment(
|
|
|
+ return await self._send_local_file_as_attachment(
|
|
|
chat_id,
|
|
|
video_path,
|
|
|
caption,
|
|
|
@@ -2415,7 +2402,7 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
|
|
|
BasePlatformAdapter override
|
|
|
"""
|
|
|
- return await self._send_local_attachment(
|
|
|
+ return await self._send_local_file_as_attachment(
|
|
|
chat_id,
|
|
|
audio_path,
|
|
|
caption,
|
|
|
@@ -2434,55 +2421,32 @@ class ChattoAdapter(BasePlatformAdapter):
|
|
|
) -> SendResult:
|
|
|
"""Send an image to a Chatto room.
|
|
|
|
|
|
- Tries to download the image from the URL and upload it as a native
|
|
|
- attachment. Falls back to sending the URL as a link (Chatto renders
|
|
|
- link previews) if the download fails.
|
|
|
+ Materialises the URL (size-capped download, like every inbound
|
|
|
+ attachment) and uploads it as a native attachment. Falls back to the
|
|
|
+ plain URL as a link — Chatto renders link previews — when the URL
|
|
|
+ cannot be materialised or the post after a successful upload fails.
|
|
|
+ An upload failure needs no fallback on top: the text notice of
|
|
|
+ ``_send_local_file_as_attachment`` has already gone out.
|
|
|
|
|
|
BasePlatformAdapter override
|
|
|
"""
|
|
|
- # Try downloading and uploading as attachment
|
|
|
- try:
|
|
|
- import tempfile
|
|
|
-
|
|
|
- # Download to a temp file
|
|
|
- parsed = urlsplit(image_url)
|
|
|
- url_path = parsed.path
|
|
|
- ext = os.path.splitext(url_path)[1] or ".png"
|
|
|
- tmp_fd, tmp_path = tempfile.mkstemp(suffix=ext, prefix="chatto_img_")
|
|
|
+ link_text = f"{caption}\n{image_url}" if caption else image_url
|
|
|
+ path, is_temp = await self._materialise_image(image_url)
|
|
|
+ if path is not None:
|
|
|
try:
|
|
|
- os.close(tmp_fd)
|
|
|
- await asyncio.to_thread(
|
|
|
- _download_to_file,
|
|
|
- image_url,
|
|
|
- tmp_path,
|
|
|
- ChattoConstants.HTTP_TIMEOUT,
|
|
|
+ result = await self._send_local_file_as_attachment(
|
|
|
+ chat_id, path, caption, reply_to, metadata, kind="image"
|
|
|
)
|
|
|
-
|
|
|
- # Upload as attachment
|
|
|
- result = await self.send_image_file(
|
|
|
- chat_id,
|
|
|
- tmp_path,
|
|
|
- caption=caption,
|
|
|
- reply_to=reply_to,
|
|
|
- metadata=metadata,
|
|
|
- )
|
|
|
- if result.success:
|
|
|
- return result
|
|
|
finally:
|
|
|
- try:
|
|
|
- os.unlink(tmp_path)
|
|
|
- except OSError:
|
|
|
- pass
|
|
|
- except Exception as e:
|
|
|
- logger.debug(
|
|
|
- "Chatto: send_image download/upload failed, falling back to link: %s", e
|
|
|
- )
|
|
|
+ if is_temp:
|
|
|
+ try:
|
|
|
+ os.unlink(path)
|
|
|
+ except OSError:
|
|
|
+ pass
|
|
|
+ if result.success:
|
|
|
+ return result
|
|
|
|
|
|
- # Fallback: send as link (Chatto renders link previews)
|
|
|
- text = image_url
|
|
|
- if caption:
|
|
|
- text = f"{caption}\n{image_url}"
|
|
|
- return await self.send(chat_id, text, reply_to=reply_to, metadata=metadata)
|
|
|
+ return await self.send(chat_id, link_text, reply_to=reply_to, metadata=metadata)
|
|
|
|
|
|
async def _materialise_image(self, image_url: str) -> tuple[str | None, bool]:
|
|
|
"""Resolve one ``send_multiple_images`` entry to a local file path.
|