Files
microdao-daarion/services/city-service/matrix_client.py
Apple 7108985b55 feat: Matrix Gateway integration for Rooms Layer
Matrix Gateway:
- Add POST /internal/matrix/room/join endpoint
- Add POST /internal/matrix/message/send endpoint
- Add GET /internal/matrix/rooms/{room_id}/messages endpoint

City Service:
- Add POST /rooms/sync/matrix endpoint for bulk sync
- Update get_agent_chat_room to auto-create Matrix rooms
- Update get_node_chat_room to auto-create Matrix rooms
- Update get_microdao_chat_room to auto-create Matrix rooms
- Add join_user_to_room, send_message_to_room, get_room_messages to matrix_client
- Add ensure_room_has_matrix helper function

This enables:
- Automatic Matrix room creation for all entity types
- chat_available = true when Matrix room exists
- Real-time messaging via Matrix
2025-11-30 10:12:27 -08:00

209 lines
6.8 KiB
Python

"""
Matrix Gateway Client for City Service
"""
import os
import httpx
import logging
from typing import Optional, Tuple, List, Dict, Any
logger = logging.getLogger(__name__)
MATRIX_GATEWAY_URL = os.getenv("MATRIX_GATEWAY_URL", "http://daarion-matrix-gateway:7025")
async def create_matrix_room(slug: str, name: str, visibility: str = "public", topic: str = None) -> Tuple[Optional[str], Optional[str]]:
"""
Create a Matrix room via Matrix Gateway.
Returns:
Tuple of (matrix_room_id, matrix_room_alias) or (None, None) on failure
"""
async with httpx.AsyncClient(timeout=30.0) as client:
try:
payload = {
"slug": slug,
"name": name,
"visibility": visibility
}
if topic:
payload["topic"] = topic
response = await client.post(
f"{MATRIX_GATEWAY_URL}/internal/matrix/rooms/create",
json=payload
)
if response.status_code == 200:
data = response.json()
logger.info(f"Matrix room created: {data['matrix_room_id']}")
return data["matrix_room_id"], data["matrix_room_alias"]
else:
logger.error(f"Failed to create Matrix room: {response.text}")
return None, None
except httpx.RequestError as e:
logger.error(f"Matrix Gateway request error: {e}")
return None, None
except Exception as e:
logger.error(f"Matrix room creation error: {e}")
return None, None
async def find_matrix_room_by_alias(alias: str) -> Tuple[Optional[str], Optional[str]]:
"""
Find a Matrix room by alias via Matrix Gateway.
Returns:
Tuple of (matrix_room_id, matrix_room_alias) or (None, None) if not found
"""
async with httpx.AsyncClient(timeout=10.0) as client:
try:
response = await client.get(
f"{MATRIX_GATEWAY_URL}/internal/matrix/rooms/find-by-alias",
params={"alias": alias}
)
if response.status_code == 200:
data = response.json()
return data["matrix_room_id"], data["matrix_room_alias"]
elif response.status_code == 404:
return None, None
else:
logger.error(f"Failed to find Matrix room: {response.text}")
return None, None
except httpx.RequestError as e:
logger.error(f"Matrix Gateway request error: {e}")
return None, None
async def join_user_to_room(room_id: str, user_id: str) -> bool:
"""
Join a Matrix user to a room.
Args:
room_id: Matrix room ID (!abc:daarion.space)
user_id: Matrix user ID (@user:daarion.space)
Returns:
True if joined successfully, False otherwise
"""
async with httpx.AsyncClient(timeout=30.0) as client:
try:
response = await client.post(
f"{MATRIX_GATEWAY_URL}/internal/matrix/room/join",
json={
"room_id": room_id,
"user_id": user_id
}
)
if response.status_code == 200:
data = response.json()
logger.info(f"User {user_id} joined room {room_id}")
return data.get("ok", False)
else:
logger.error(f"Failed to join room: {response.text}")
return False
except httpx.RequestError as e:
logger.error(f"Matrix Gateway request error: {e}")
return False
async def send_message_to_room(room_id: str, body: str, sender: str = None) -> Optional[str]:
"""
Send a message to a Matrix room.
Args:
room_id: Matrix room ID
body: Message text
sender: Optional sender Matrix user ID
Returns:
Event ID if sent successfully, None otherwise
"""
async with httpx.AsyncClient(timeout=30.0) as client:
try:
payload = {
"room_id": room_id,
"body": body,
"sender": sender or "@daarion-bot:daarion.space"
}
response = await client.post(
f"{MATRIX_GATEWAY_URL}/internal/matrix/message/send",
json=payload
)
if response.status_code == 200:
data = response.json()
logger.info(f"Message sent to {room_id}: {data.get('event_id')}")
return data.get("event_id")
else:
logger.error(f"Failed to send message: {response.text}")
return None
except httpx.RequestError as e:
logger.error(f"Matrix Gateway request error: {e}")
return None
async def get_room_messages(room_id: str, limit: int = 50) -> List[Dict[str, Any]]:
"""
Get recent messages from a Matrix room.
Args:
room_id: Matrix room ID
limit: Max number of messages to fetch
Returns:
List of message dicts with event_id, sender, body, timestamp
"""
async with httpx.AsyncClient(timeout=30.0) as client:
try:
response = await client.get(
f"{MATRIX_GATEWAY_URL}/internal/matrix/rooms/{room_id}/messages",
params={"limit": limit}
)
if response.status_code == 200:
data = response.json()
return data.get("messages", [])
else:
logger.error(f"Failed to get messages: {response.text}")
return []
except httpx.RequestError as e:
logger.error(f"Matrix Gateway request error: {e}")
return []
async def check_matrix_gateway_health() -> bool:
"""Check if Matrix Gateway is available."""
async with httpx.AsyncClient(timeout=5.0) as client:
try:
response = await client.get(f"{MATRIX_GATEWAY_URL}/healthz")
return response.status_code == 200
except Exception:
return False
async def ensure_room_has_matrix(room_slug: str, room_name: str, visibility: str = "public") -> Tuple[Optional[str], Optional[str]]:
"""
Ensure a room has a Matrix room ID. Creates one if it doesn't exist.
Returns:
Tuple of (matrix_room_id, matrix_room_alias) or (None, None) on failure
"""
# First try to find existing room
alias = f"#city_{room_slug}:daarion.space"
matrix_room_id, matrix_room_alias = await find_matrix_room_by_alias(alias)
if matrix_room_id:
return matrix_room_id, matrix_room_alias
# Create new room
return await create_matrix_room(room_slug, room_name, visibility)