220 lines
6.1 KiB
Python
220 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import mimetypes
|
|
import uuid
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from urllib.parse import quote
|
|
|
|
import httpx
|
|
|
|
from app.matrix_token_manager import MatrixTokenManager
|
|
|
|
|
|
@dataclass
|
|
class MatrixSendResult:
|
|
ok: bool
|
|
status_code: int | None
|
|
event_id: str | None
|
|
error: str | None
|
|
content_uri: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class MatrixUploadResult:
|
|
ok: bool
|
|
status_code: int | None
|
|
content_uri: str | None
|
|
error: str | None
|
|
|
|
|
|
class MatrixNotifier:
|
|
def __init__(
|
|
self,
|
|
homeserver_url: str,
|
|
room_id: str,
|
|
token_manager: MatrixTokenManager,
|
|
message_type: str = "m.notice",
|
|
timeout_seconds: float = 10,
|
|
verify_tls: bool = True,
|
|
) -> None:
|
|
self.homeserver_url = homeserver_url.rstrip("/")
|
|
self.room_id = room_id
|
|
self.token_manager = token_manager
|
|
self.message_type = message_type
|
|
self.client = httpx.AsyncClient(
|
|
timeout=timeout_seconds,
|
|
verify=verify_tls,
|
|
)
|
|
|
|
async def close(self) -> None:
|
|
await self.client.aclose()
|
|
|
|
async def send_message(self, body: str) -> MatrixSendResult:
|
|
payload = {
|
|
"msgtype": self.message_type,
|
|
"body": body,
|
|
}
|
|
return await self._send_room_message(payload)
|
|
|
|
async def send_image(
|
|
self,
|
|
file_path: str,
|
|
body: str | None = None,
|
|
) -> MatrixSendResult:
|
|
path = Path(file_path)
|
|
if not path.exists() or not path.is_file():
|
|
return MatrixSendResult(
|
|
ok=False,
|
|
status_code=None,
|
|
event_id=None,
|
|
error=f"Image file not found: {file_path}",
|
|
content_uri=None,
|
|
)
|
|
|
|
mime_type, _ = mimetypes.guess_type(path.name)
|
|
mime_type = mime_type or "image/png"
|
|
data = path.read_bytes()
|
|
|
|
upload_result = await self._upload_bytes(
|
|
filename=path.name,
|
|
data=data,
|
|
content_type=mime_type,
|
|
)
|
|
if not upload_result.ok:
|
|
return MatrixSendResult(
|
|
ok=False,
|
|
status_code=upload_result.status_code,
|
|
event_id=None,
|
|
error=upload_result.error,
|
|
content_uri=None,
|
|
)
|
|
|
|
payload = {
|
|
"msgtype": "m.image",
|
|
"body": body or path.name,
|
|
"url": upload_result.content_uri,
|
|
"info": {
|
|
"mimetype": mime_type,
|
|
"size": len(data),
|
|
},
|
|
}
|
|
|
|
send_result = await self._send_room_message(payload)
|
|
send_result.content_uri = upload_result.content_uri
|
|
return send_result
|
|
|
|
async def _send_room_message(self, payload: dict) -> MatrixSendResult:
|
|
txn_id = str(uuid.uuid4())
|
|
room_id_encoded = quote(self.room_id, safe="")
|
|
url = (
|
|
f"{self.homeserver_url}"
|
|
f"/_matrix/client/v3/rooms/{room_id_encoded}"
|
|
f"/send/m.room.message/{txn_id}"
|
|
)
|
|
|
|
try:
|
|
response = await self._request_with_refresh(
|
|
method="PUT",
|
|
url=url,
|
|
json=payload,
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
data = response.json() if response.content else {}
|
|
|
|
if response.is_success:
|
|
return MatrixSendResult(
|
|
ok=True,
|
|
status_code=response.status_code,
|
|
event_id=data.get("event_id"),
|
|
error=None,
|
|
content_uri=None,
|
|
)
|
|
|
|
return MatrixSendResult(
|
|
ok=False,
|
|
status_code=response.status_code,
|
|
event_id=None,
|
|
error=str(data),
|
|
content_uri=None,
|
|
)
|
|
except Exception as exc:
|
|
return MatrixSendResult(
|
|
ok=False,
|
|
status_code=None,
|
|
event_id=None,
|
|
error=str(exc),
|
|
content_uri=None,
|
|
)
|
|
|
|
async def _upload_bytes(
|
|
self,
|
|
filename: str,
|
|
data: bytes,
|
|
content_type: str,
|
|
) -> MatrixUploadResult:
|
|
url = f"{self.homeserver_url}/_matrix/media/v3/upload"
|
|
|
|
try:
|
|
response = await self._request_with_refresh(
|
|
method="POST",
|
|
url=url,
|
|
params={"filename": filename},
|
|
content=data,
|
|
headers={"Content-Type": content_type},
|
|
)
|
|
payload = response.json() if response.content else {}
|
|
|
|
if response.is_success:
|
|
return MatrixUploadResult(
|
|
ok=True,
|
|
status_code=response.status_code,
|
|
content_uri=payload.get("content_uri"),
|
|
error=None,
|
|
)
|
|
|
|
return MatrixUploadResult(
|
|
ok=False,
|
|
status_code=response.status_code,
|
|
content_uri=None,
|
|
error=str(payload),
|
|
)
|
|
except Exception as exc:
|
|
return MatrixUploadResult(
|
|
ok=False,
|
|
status_code=None,
|
|
content_uri=None,
|
|
error=str(exc),
|
|
)
|
|
|
|
async def _request_with_refresh(
|
|
self,
|
|
method: str,
|
|
url: str,
|
|
**kwargs,
|
|
) -> httpx.Response:
|
|
response = await self._request_once(method, url, **kwargs)
|
|
|
|
if response.status_code == 401:
|
|
await self.token_manager.refresh_if_needed(force=True)
|
|
response = await self._request_once(method, url, **kwargs)
|
|
|
|
return response
|
|
|
|
async def _request_once(
|
|
self,
|
|
method: str,
|
|
url: str,
|
|
**kwargs,
|
|
) -> httpx.Response:
|
|
token = await self.token_manager.get_access_token()
|
|
|
|
headers = dict(kwargs.pop("headers", {}) or {})
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
|
|
return await self.client.request(
|
|
method=method,
|
|
url=url,
|
|
headers=headers,
|
|
**kwargs,
|
|
) |