""" User profile routes for L2 server. Handles ActivityPub actor profiles. """ import logging from fastapi import APIRouter, Request, HTTPException from fastapi.responses import JSONResponse from artdag_common import render from artdag_common.middleware import wants_html from ..config import settings from ..dependencies import get_templates, get_user_from_cookie router = APIRouter() logger = logging.getLogger(__name__) @router.get("/users/{username}") async def get_user_profile( username: str, request: Request, ): """Get user profile (ActivityPub actor).""" import db user = await db.get_user(username) if not user: raise HTTPException(404, "User not found") # ActivityPub response accept = request.headers.get("accept", "") if "application/activity+json" in accept or "application/ld+json" in accept: actor = { "@context": [ "https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", ], "type": "Person", "id": f"https://{settings.domain}/users/{username}", "name": user.get("display_name", username), "preferredUsername": username, "inbox": f"https://{settings.domain}/users/{username}/inbox", "outbox": f"https://{settings.domain}/users/{username}/outbox", "publicKey": { "id": f"https://{settings.domain}/users/{username}#main-key", "owner": f"https://{settings.domain}/users/{username}", "publicKeyPem": user.get("public_key", ""), }, } return JSONResponse(content=actor, media_type="application/activity+json") # HTML profile page current_user = get_user_from_cookie(request) assets = await db.get_user_assets(username, limit=12) templates = get_templates(request) return render(templates, "users/profile.html", request, profile=user, assets=assets, user={"username": current_user} if current_user else None, ) @router.get("/users/{username}/outbox") async def get_outbox( username: str, request: Request, page: bool = False, ): """Get user's outbox (ActivityPub).""" import db user = await db.get_user(username) if not user: raise HTTPException(404, "User not found") actor_id = f"https://{settings.domain}/users/{username}" if not page: # Return collection summary total = await db.count_user_activities(username) return JSONResponse( content={ "@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollection", "id": f"{actor_id}/outbox", "totalItems": total, "first": f"{actor_id}/outbox?page=true", }, media_type="application/activity+json", ) # Return paginated activities activities = await db.get_user_activities(username, limit=20) items = [a.get("activity_json", a) for a in activities] return JSONResponse( content={ "@context": "https://www.w3.org/ns/activitystreams", "type": "OrderedCollectionPage", "id": f"{actor_id}/outbox?page=true", "partOf": f"{actor_id}/outbox", "orderedItems": items, }, media_type="application/activity+json", ) @router.post("/users/{username}/inbox") async def receive_inbox( username: str, request: Request, ): """Receive ActivityPub inbox message.""" import db user = await db.get_user(username) if not user: raise HTTPException(404, "User not found") # TODO: Verify HTTP signature # TODO: Process activity (Follow, Like, Announce, etc.) body = await request.json() logger.info(f"Received inbox activity for {username}: {body.get('type')}") # For now, just acknowledge return {"status": "accepted"} @router.get("/") async def home(request: Request): """Home page.""" import db import markdown username = get_user_from_cookie(request) # Get recent activities activities = await db.get_activities(limit=10) # Get README if exists readme_html = "" try: from pathlib import Path readme_path = Path(__file__).parent.parent.parent / "README.md" if readme_path.exists(): readme_html = markdown.markdown(readme_path.read_text()) except Exception: pass templates = get_templates(request) return render(templates, "home.html", request, user={"username": username} if username else None, activities=activities, readme_html=readme_html, )