feat: Implement Matrix Chat Client
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
City Backend API Routes
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Body
|
||||
from fastapi import APIRouter, HTTPException, Depends, Body, Header, Query
|
||||
from typing import List, Optional
|
||||
import logging
|
||||
import httpx
|
||||
import os
|
||||
|
||||
from models_city import (
|
||||
CityRoomRead,
|
||||
@@ -20,6 +22,10 @@ from matrix_client import create_matrix_room, find_matrix_room_by_alias
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# JWT validation (simplified for MVP)
|
||||
AUTH_SERVICE_URL = os.getenv("AUTH_SERVICE_URL", "http://daarion-auth:7020")
|
||||
MATRIX_GATEWAY_URL = os.getenv("MATRIX_GATEWAY_URL", "http://daarion-matrix-gateway:7025")
|
||||
|
||||
router = APIRouter(prefix="/city", tags=["city"])
|
||||
|
||||
|
||||
@@ -293,6 +299,101 @@ async def backfill_matrix_rooms():
|
||||
raise HTTPException(status_code=500, detail=f"Backfill failed: {str(e)}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Chat Bootstrap API (Matrix Integration)
|
||||
# =============================================================================
|
||||
|
||||
async def validate_jwt_token(authorization: str) -> Optional[dict]:
|
||||
"""Validate JWT token via auth-service introspect endpoint."""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
return None
|
||||
|
||||
token = authorization.replace("Bearer ", "")
|
||||
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
try:
|
||||
resp = await client.get(
|
||||
f"{AUTH_SERVICE_URL}/api/auth/introspect",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
return resp.json()
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"JWT validation error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/chat/bootstrap")
|
||||
async def chat_bootstrap(
|
||||
room_slug: str = Query(..., description="City room slug"),
|
||||
authorization: Optional[str] = Header(None)
|
||||
):
|
||||
"""
|
||||
Bootstrap Matrix chat for a city room.
|
||||
|
||||
Returns Matrix credentials and room info for the authenticated user.
|
||||
"""
|
||||
# Validate JWT
|
||||
user_info = await validate_jwt_token(authorization)
|
||||
if not user_info:
|
||||
raise HTTPException(status_code=401, detail="Invalid or missing authorization token")
|
||||
|
||||
user_id = user_info.get("user_id")
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=401, detail="Invalid token: missing user_id")
|
||||
|
||||
# Get room by slug
|
||||
room = await repo_city.get_room_by_slug(room_slug)
|
||||
if not room:
|
||||
raise HTTPException(status_code=404, detail=f"Room '{room_slug}' not found")
|
||||
|
||||
# Check if room has Matrix integration
|
||||
matrix_room_id = room.get("matrix_room_id")
|
||||
matrix_room_alias = room.get("matrix_room_alias")
|
||||
|
||||
if not matrix_room_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Room does not have Matrix integration. Run /city/matrix/backfill first."
|
||||
)
|
||||
|
||||
# Get Matrix user token from matrix-gateway
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
token_resp = await client.post(
|
||||
f"{MATRIX_GATEWAY_URL}/internal/matrix/users/token",
|
||||
json={"user_id": user_id}
|
||||
)
|
||||
|
||||
if token_resp.status_code != 200:
|
||||
error = token_resp.json()
|
||||
logger.error(f"Failed to get Matrix token: {error}")
|
||||
raise HTTPException(status_code=500, detail="Failed to get Matrix credentials")
|
||||
|
||||
matrix_creds = token_resp.json()
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Matrix gateway request error: {e}")
|
||||
raise HTTPException(status_code=503, detail="Matrix service unavailable")
|
||||
|
||||
# Return bootstrap data
|
||||
return {
|
||||
"matrix_hs_url": f"https://app.daarion.space", # Through nginx proxy
|
||||
"matrix_user_id": matrix_creds["matrix_user_id"],
|
||||
"matrix_access_token": matrix_creds["access_token"],
|
||||
"matrix_device_id": matrix_creds["device_id"],
|
||||
"matrix_room_id": matrix_room_id,
|
||||
"matrix_room_alias": matrix_room_alias,
|
||||
"room": {
|
||||
"id": room["id"],
|
||||
"slug": room["slug"],
|
||||
"name": room["name"],
|
||||
"description": room.get("description")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# City Feed API
|
||||
# =============================================================================
|
||||
|
||||
@@ -67,6 +67,17 @@ class HealthResponse(BaseModel):
|
||||
server_name: str
|
||||
|
||||
|
||||
class UserTokenRequest(BaseModel):
|
||||
user_id: str # DAARION user_id (UUID)
|
||||
|
||||
|
||||
class UserTokenResponse(BaseModel):
|
||||
matrix_user_id: str
|
||||
access_token: str
|
||||
device_id: str
|
||||
home_server: str
|
||||
|
||||
|
||||
async def get_admin_token() -> str:
|
||||
"""Get or create admin access token for Matrix operations."""
|
||||
global _admin_token
|
||||
@@ -318,6 +329,105 @@ async def get_room_info(room_id: str):
|
||||
raise HTTPException(status_code=503, detail="Matrix unavailable")
|
||||
|
||||
|
||||
@app.post("/internal/matrix/users/token", response_model=UserTokenResponse)
|
||||
async def get_user_token(request: UserTokenRequest):
|
||||
"""
|
||||
Get or create Matrix access token for a DAARION user.
|
||||
|
||||
This is used for chat bootstrap - allows frontend to connect to Matrix
|
||||
on behalf of the user.
|
||||
"""
|
||||
# Generate Matrix username from DAARION user_id
|
||||
user_id_short = request.user_id[:8].replace('-', '')
|
||||
matrix_username = f"daarion_{user_id_short}"
|
||||
matrix_user_id = f"@{matrix_username}:{settings.matrix_server_name}"
|
||||
|
||||
# Generate password (deterministic, based on user_id + secret)
|
||||
matrix_password = hashlib.sha256(
|
||||
f"{request.user_id}:{settings.synapse_registration_secret}".encode()
|
||||
).hexdigest()[:32]
|
||||
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
try:
|
||||
# Try to login first
|
||||
login_resp = await client.post(
|
||||
f"{settings.synapse_url}/_matrix/client/v3/login",
|
||||
json={
|
||||
"type": "m.login.password",
|
||||
"identifier": {
|
||||
"type": "m.id.user",
|
||||
"user": matrix_username
|
||||
},
|
||||
"password": matrix_password,
|
||||
"device_id": f"DAARION_{user_id_short}",
|
||||
"initial_device_display_name": "DAARION Web"
|
||||
}
|
||||
)
|
||||
|
||||
if login_resp.status_code == 200:
|
||||
result = login_resp.json()
|
||||
logger.info(f"Matrix user logged in: {matrix_user_id}")
|
||||
return UserTokenResponse(
|
||||
matrix_user_id=result["user_id"],
|
||||
access_token=result["access_token"],
|
||||
device_id=result.get("device_id", f"DAARION_{user_id_short}"),
|
||||
home_server=settings.matrix_server_name
|
||||
)
|
||||
|
||||
# User doesn't exist, create via admin API
|
||||
logger.info(f"Creating Matrix user: {matrix_username}")
|
||||
|
||||
# Get nonce
|
||||
nonce_resp = await client.get(
|
||||
f"{settings.synapse_url}/_synapse/admin/v1/register"
|
||||
)
|
||||
nonce_resp.raise_for_status()
|
||||
nonce = nonce_resp.json()["nonce"]
|
||||
|
||||
# Generate MAC
|
||||
mac = hmac.new(
|
||||
key=settings.synapse_registration_secret.encode('utf-8'),
|
||||
digestmod=hashlib.sha1
|
||||
)
|
||||
mac.update(nonce.encode('utf-8'))
|
||||
mac.update(b"\x00")
|
||||
mac.update(matrix_username.encode('utf-8'))
|
||||
mac.update(b"\x00")
|
||||
mac.update(matrix_password.encode('utf-8'))
|
||||
mac.update(b"\x00")
|
||||
mac.update(b"notadmin")
|
||||
|
||||
# Register user
|
||||
register_resp = await client.post(
|
||||
f"{settings.synapse_url}/_synapse/admin/v1/register",
|
||||
json={
|
||||
"nonce": nonce,
|
||||
"username": matrix_username,
|
||||
"password": matrix_password,
|
||||
"admin": False,
|
||||
"mac": mac.hexdigest()
|
||||
}
|
||||
)
|
||||
|
||||
if register_resp.status_code == 200:
|
||||
result = register_resp.json()
|
||||
logger.info(f"Matrix user created: {result['user_id']}")
|
||||
return UserTokenResponse(
|
||||
matrix_user_id=result["user_id"],
|
||||
access_token=result["access_token"],
|
||||
device_id=result.get("device_id", f"DAARION_{user_id_short}"),
|
||||
home_server=settings.matrix_server_name
|
||||
)
|
||||
else:
|
||||
error = register_resp.json()
|
||||
logger.error(f"Failed to create Matrix user: {error}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create Matrix user: {error.get('error', 'Unknown')}")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.error(f"Matrix request error: {e}")
|
||||
raise HTTPException(status_code=503, detail="Matrix unavailable")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=settings.port)
|
||||
|
||||
Reference in New Issue
Block a user