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
209 lines
6.8 KiB
Python
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)
|
|
|