adapter.py was a 3,493-line monolith. Split it into focused modules with clear separation of responsibilities, bringing it down to ~857 lines: - Module-level helpers: hooks, classify, pickers, commands, setup, defaults, secrets - Frame-handler mixins: inbound, tool_frames, push_frames, media_frames, picker_frames, channel_frames, query_frames - mixin_base: IrisAdapterBase (declaration-only base for shared attrs) - adapter.py now holds only IrisAdapter (the composition of the 7 mixins + BasePlatformAdapter), register(), and test-facing re-exports The mixins come before BasePlatformAdapter in the MRO so their methods override the base; super() calls (e.g. send_image) still resolve to BasePlatformAdapter. No circular imports; dispatch.py and http_server.py (instance-method callers) are unaffected. Ruff complexity ceilings (PLR0911/0912/0913/0915) restored to Ruff's built-in defaults (12/50/6/5) instead of "just above the current maxima", which ratchets the bar down as code grows. The existing genuinely-complex functions (frame builders mirroring the wire schema, the QR matrix builder, the dispatch table) carry an explicit `# noqa: PLR09xx` marking them as reviewed, frozen exceptions; new code is held to the default ceilings. All 125 tests green (94 test_android + 31 test_android_http); no new ruff errors introduced.
128 lines
4.4 KiB
Python
128 lines
4.4 KiB
Python
"""M4: outbound media (agent -> app): ``media.offer`` emission.
|
|
|
|
Mixin for ``adapter.IrisAdapter``. The gateway's dispatch partition
|
|
(gateway/run.py) extracts MEDIA: tags / image URLs from the final response,
|
|
filters them through ``filter_media_delivery_paths``, then calls the
|
|
``send_*`` overrides with local file paths. We re-validate each path
|
|
(defense in depth), register it in the media registry, mint a ``media_id``,
|
|
and emit ``media.offer``; the app fetches the bytes via ``media.pull``.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from typing import Any
|
|
|
|
from gateway.platforms.base import SendResult, validate_media_delivery_path
|
|
|
|
from . import media as media_bridge
|
|
from . import protocol
|
|
from .classify import _thread_id_from_metadata
|
|
from .mixin_base import IrisAdapterBase
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class MediaHandlers(IrisAdapterBase):
|
|
"""Outbound media (see module docstring)."""
|
|
|
|
async def _offer_media(
|
|
self,
|
|
chat_id: str,
|
|
path: str,
|
|
kind: str,
|
|
filename: str | None,
|
|
metadata: dict[str, Any] | None,
|
|
) -> SendResult:
|
|
safe = validate_media_delivery_path(path)
|
|
if safe is None:
|
|
logger.warning("iris: media path failed delivery validation: %s", path)
|
|
return SendResult(success=False, error="iris: media path not deliverable")
|
|
try:
|
|
size = os.path.getsize(safe)
|
|
except OSError as e:
|
|
logger.warning("iris: media file unreadable %s: %s", safe, e)
|
|
return SendResult(success=False, error="iris: media file unreadable")
|
|
entry = self._media.register_outbound(
|
|
safe, kind, media_bridge.mime_for_path(safe), filename or os.path.basename(safe), size
|
|
)
|
|
thread_id = _thread_id_from_metadata(metadata)
|
|
frame = protocol.media_offer(
|
|
entry.media_id,
|
|
entry.kind,
|
|
entry.mime,
|
|
entry.size,
|
|
entry.filename,
|
|
chat_id=chat_id,
|
|
thread_id=thread_id,
|
|
message_id=self._last_message_id.get(chat_id),
|
|
)
|
|
await self._broadcast_or_log(chat_id, frame)
|
|
return SendResult(success=True, message_id=entry.media_id)
|
|
|
|
async def send_image(
|
|
self,
|
|
chat_id: str,
|
|
image_url: str,
|
|
caption: str | None = None,
|
|
reply_to: str | None = None,
|
|
metadata: dict[str, Any] | None = None,
|
|
) -> SendResult:
|
|
"""Send an image (M4: local files offered over WS; remote URLs fall
|
|
back to the base text rendering)."""
|
|
if image_url.startswith("file://"):
|
|
from urllib.parse import unquote
|
|
|
|
return await self._offer_media(chat_id, unquote(image_url[7:]), "image", None, metadata)
|
|
return await super().send_image(
|
|
chat_id, image_url, caption=caption, reply_to=reply_to, metadata=metadata
|
|
)
|
|
|
|
async def send_image_file(
|
|
self,
|
|
chat_id: str,
|
|
image_path: str,
|
|
caption: str | None = None,
|
|
reply_to: str | None = None,
|
|
metadata: dict[str, Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> SendResult:
|
|
"""Send a local image file (M4)."""
|
|
return await self._offer_media(chat_id, image_path, "image", None, metadata)
|
|
|
|
async def send_video(
|
|
self,
|
|
chat_id: str,
|
|
video_path: str,
|
|
caption: str | None = None,
|
|
reply_to: str | None = None,
|
|
metadata: dict[str, Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> SendResult:
|
|
"""Send a video (M4)."""
|
|
return await self._offer_media(chat_id, video_path, "video", None, metadata)
|
|
|
|
async def send_voice(
|
|
self,
|
|
chat_id: str,
|
|
audio_path: str,
|
|
caption: str | None = None,
|
|
reply_to: str | None = None,
|
|
metadata: dict[str, Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> SendResult:
|
|
"""Send a voice note / audio file (M4)."""
|
|
return await self._offer_media(chat_id, audio_path, "voice", None, metadata)
|
|
|
|
async def send_document( # noqa: PLR0913
|
|
self,
|
|
chat_id: str,
|
|
file_path: str,
|
|
caption: str | None = None,
|
|
file_name: str | None = None,
|
|
reply_to: str | None = None,
|
|
metadata: dict[str, Any] | None = None,
|
|
**kwargs: Any,
|
|
) -> SendResult:
|
|
"""Send a document (M4)."""
|
|
return await self._offer_media(chat_id, file_path, "document", file_name, metadata)
|