import asyncio import logging import time import aiohttp logger = logging.getLogger(__name__) RETRY_DELAYS = [1, 2, 4] class MatrixClient: def __init__(self, homeserver: str, token: str, room_id: str): self.homeserver = homeserver.rstrip("/") self.token = token self.room_id = room_id self.headers = {"Authorization": f"Bearer {token}"} async def _request(self, method: str, url: str, **kwargs) -> dict | None: for attempt, delay in enumerate(RETRY_DELAYS): try: async with aiohttp.ClientSession() as session: async with session.request( method, url, headers=self.headers, **kwargs ) as resp: if resp.status < 300: return await resp.json() body = await resp.text() logger.warning( "Matrix API %s (attempt %d): %s", resp.status, attempt + 1, body, ) except Exception: logger.exception("Matrix request failed (attempt %d)", attempt + 1) if attempt < len(RETRY_DELAYS) - 1: await asyncio.sleep(delay) logger.error("All Matrix retries exhausted for %s", url) return None async def upload_image(self, image_bytes: bytes, filename: str) -> str | None: url = f"{self.homeserver}/_matrix/media/v3/upload?filename={filename}" headers = {**self.headers, "Content-Type": "image/jpeg"} try: async with aiohttp.ClientSession() as session: async with session.post(url, headers=headers, data=image_bytes) as resp: if resp.status < 300: data = await resp.json() mxc_uri = data.get("content_uri") logger.info("Uploaded image: %s", mxc_uri) return mxc_uri body = await resp.text() logger.error("Image upload failed %s: %s", resp.status, body) except Exception: logger.exception("Image upload failed") return None async def send_image(self, mxc_uri: str, filename: str, size: int) -> dict | None: txn_id = str(int(time.time() * 1000)) url = ( f"{self.homeserver}/_matrix/client/r0/rooms/" f"{self.room_id}/send/m.room.message/{txn_id}" ) body = { "msgtype": "m.image", "body": filename, "url": mxc_uri, "info": {"mimetype": "image/jpeg", "size": size}, } return await self._request("PUT", url, json=body) async def send_text(self, plain: str, html: str) -> dict | None: txn_id = str(int(time.time() * 1000) + 1) url = ( f"{self.homeserver}/_matrix/client/r0/rooms/" f"{self.room_id}/send/m.room.message/{txn_id}" ) body = { "msgtype": "m.text", "body": plain, "format": "org.matrix.custom.html", "formatted_body": html, } return await self._request("PUT", url, json=body)