de1d9e1d93
Replaces initial Nomad/Consul/Terraform skeleton with current Docker-based infrastructure across all three nodes (hf-pdocker-01, hf-pdocker-02, bart). Key changes: - CLAUDE.md: updated service distribution reflecting hf-pdocker-01 restored - docs/plans/2026-03-08-hf-pdocker-01-rebuild.md: new rebuild plan doc - compose/bart/: ai-agents, guacamole, matrix, media, openvas, smarthome, watchtower - compose/hf-pdocker-02/: traefik, monitoring, adguard, gitea, semaphore, wikijs, unifi, vaultwarden, actual-budget, mcpo, netclaw, trivy, watchtower, n8n - compose/hf-pdocker-01/: frigate, frigate-notify, farm-agent - stacks/: all Swarm stack definitions (monitoring, traefik, smarthome, matrix, guacamole, media, ai-agents, frigate, gitea, semaphore, wikijs, unifi, etc.) - .gitignore: exclude .env credential files and acme.json certs - .env.example: placeholder credential files for all services - .claude/agents/: gitea-backup and other infrastructure agent configs - scripts/: frigate media cleanup and NAS sync scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
85 lines
3.2 KiB
Python
85 lines
3.2 KiB
Python
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)
|