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
This commit is contained in:
@@ -91,6 +91,32 @@ class SetPresenceResponse(BaseModel):
|
||||
status: str
|
||||
|
||||
|
||||
# NEW: Room Join Request/Response
|
||||
class RoomJoinRequest(BaseModel):
|
||||
room_id: str # Matrix room ID (!abc:daarion.space)
|
||||
user_id: str # Matrix user ID (@user:daarion.space)
|
||||
|
||||
|
||||
class RoomJoinResponse(BaseModel):
|
||||
ok: bool
|
||||
room_id: str
|
||||
user_id: str
|
||||
|
||||
|
||||
# NEW: Send Message Request/Response
|
||||
class SendMessageRequest(BaseModel):
|
||||
room_id: str # Matrix room ID
|
||||
sender: str # Matrix user ID (must be bot or have permissions)
|
||||
body: str # Message text
|
||||
msgtype: str = "m.text" # Message type
|
||||
|
||||
|
||||
class SendMessageResponse(BaseModel):
|
||||
ok: bool
|
||||
event_id: str
|
||||
room_id: str
|
||||
|
||||
|
||||
async def get_admin_token() -> str:
|
||||
"""Get or create admin access token for Matrix operations."""
|
||||
global _admin_token
|
||||
@@ -441,6 +467,150 @@ async def get_user_token(request: UserTokenRequest):
|
||||
raise HTTPException(status_code=503, detail="Matrix unavailable")
|
||||
|
||||
|
||||
@app.post("/internal/matrix/room/join", response_model=RoomJoinResponse)
|
||||
async def join_room(request: RoomJoinRequest):
|
||||
"""
|
||||
Join a user to a Matrix room.
|
||||
|
||||
Uses admin token to invite and join the user.
|
||||
"""
|
||||
admin_token = await get_admin_token()
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
# First, invite the user to the room (admin action)
|
||||
invite_resp = await client.post(
|
||||
f"{settings.synapse_url}/_matrix/client/v3/rooms/{request.room_id}/invite",
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"user_id": request.user_id}
|
||||
)
|
||||
|
||||
# 200 = invited, 403 = already member (OK), 400 = already invited (OK)
|
||||
if invite_resp.status_code not in (200, 403, 400):
|
||||
logger.warning(f"Invite response: {invite_resp.status_code} - {invite_resp.text}")
|
||||
|
||||
# Now join the user to the room via admin API
|
||||
# Use the synapse admin API to force-join
|
||||
join_resp = await client.post(
|
||||
f"{settings.synapse_url}/_synapse/admin/v1/join/{request.room_id}",
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={"user_id": request.user_id}
|
||||
)
|
||||
|
||||
if join_resp.status_code == 200:
|
||||
logger.info(f"User {request.user_id} joined room {request.room_id}")
|
||||
return RoomJoinResponse(
|
||||
ok=True,
|
||||
room_id=request.room_id,
|
||||
user_id=request.user_id
|
||||
)
|
||||
elif join_resp.status_code == 400:
|
||||
# Already in room
|
||||
logger.info(f"User {request.user_id} already in room {request.room_id}")
|
||||
return RoomJoinResponse(
|
||||
ok=True,
|
||||
room_id=request.room_id,
|
||||
user_id=request.user_id
|
||||
)
|
||||
else:
|
||||
error = join_resp.json() if join_resp.text else {}
|
||||
logger.error(f"Failed to join room: {join_resp.status_code} - {join_resp.text}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to join room: {error.get('error', 'Unknown')}"
|
||||
)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Matrix request error: {e}")
|
||||
raise HTTPException(status_code=503, detail="Matrix unavailable")
|
||||
|
||||
|
||||
@app.post("/internal/matrix/message/send", response_model=SendMessageResponse)
|
||||
async def send_message(request: SendMessageRequest):
|
||||
"""
|
||||
Send a message to a Matrix room.
|
||||
|
||||
Uses admin token to send on behalf of the bot.
|
||||
"""
|
||||
admin_token = await get_admin_token()
|
||||
|
||||
import time
|
||||
txn_id = f"daarion_{int(time.time() * 1000)}"
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
# Send message via admin token (as the bot user)
|
||||
send_resp = await client.put(
|
||||
f"{settings.synapse_url}/_matrix/client/v3/rooms/{request.room_id}/send/m.room.message/{txn_id}",
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
json={
|
||||
"msgtype": request.msgtype,
|
||||
"body": request.body
|
||||
}
|
||||
)
|
||||
|
||||
if send_resp.status_code == 200:
|
||||
result = send_resp.json()
|
||||
event_id = result.get("event_id", "")
|
||||
logger.info(f"Message sent to {request.room_id}: {event_id}")
|
||||
return SendMessageResponse(
|
||||
ok=True,
|
||||
event_id=event_id,
|
||||
room_id=request.room_id
|
||||
)
|
||||
else:
|
||||
error = send_resp.json() if send_resp.text else {}
|
||||
logger.error(f"Failed to send message: {send_resp.status_code} - {send_resp.text}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to send message: {error.get('error', 'Unknown')}"
|
||||
)
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Matrix request error: {e}")
|
||||
raise HTTPException(status_code=503, detail="Matrix unavailable")
|
||||
|
||||
|
||||
@app.get("/internal/matrix/rooms/{room_id}/messages")
|
||||
async def get_room_messages(room_id: str, limit: int = 50):
|
||||
"""
|
||||
Get recent messages from a Matrix room.
|
||||
"""
|
||||
admin_token = await get_admin_token()
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
resp = await client.get(
|
||||
f"{settings.synapse_url}/_matrix/client/v3/rooms/{room_id}/messages",
|
||||
headers={"Authorization": f"Bearer {admin_token}"},
|
||||
params={
|
||||
"dir": "b", # backwards from end
|
||||
"limit": limit
|
||||
}
|
||||
)
|
||||
|
||||
if resp.status_code == 200:
|
||||
data = resp.json()
|
||||
messages = []
|
||||
for event in data.get("chunk", []):
|
||||
if event.get("type") == "m.room.message":
|
||||
content = event.get("content", {})
|
||||
messages.append({
|
||||
"event_id": event.get("event_id"),
|
||||
"sender": event.get("sender"),
|
||||
"body": content.get("body", ""),
|
||||
"msgtype": content.get("msgtype", "m.text"),
|
||||
"timestamp": event.get("origin_server_ts", 0)
|
||||
})
|
||||
return {"messages": messages, "room_id": room_id}
|
||||
else:
|
||||
raise HTTPException(status_code=resp.status_code, detail="Failed to get messages")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Matrix request error: {e}")
|
||||
raise HTTPException(status_code=503, detail="Matrix unavailable")
|
||||
|
||||
|
||||
@app.post("/internal/matrix/presence/online", response_model=SetPresenceResponse)
|
||||
async def set_presence_online(request: SetPresenceRequest):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user