- Create app factory with routers and templates - Auth, assets, activities, anchors, storage, users, renderers routers - Federation router for WebFinger and nodeinfo - Jinja2 templates for L2 pages - Config and dependency injection 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
162 lines
4.6 KiB
Python
162 lines
4.6 KiB
Python
"""
|
|
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,
|
|
)
|