Files
openclaw-mission-control/backend/app/api/souls_directory.py

89 lines
2.9 KiB
Python

"""API routes for searching and fetching souls-directory markdown entries."""
from __future__ import annotations
import re
from fastapi import APIRouter, Depends, HTTPException, Query, status
from app.api.deps import ActorContext, require_admin_or_agent
from app.schemas.souls_directory import (
SoulsDirectoryMarkdownResponse,
SoulsDirectorySearchResponse,
SoulsDirectorySoulRef,
)
from app.services import souls_directory
router = APIRouter(prefix="/souls-directory", tags=["souls-directory"])
ADMIN_OR_AGENT_DEP = Depends(require_admin_or_agent)
_SAFE_SEGMENT_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$")
_SAFE_SLUG_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$")
def _validate_segment(value: str, *, field: str) -> str:
cleaned = value.strip().strip("/")
if not cleaned:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=f"{field} is required",
)
if field == "handle":
ok = bool(_SAFE_SEGMENT_RE.match(cleaned))
else:
ok = bool(_SAFE_SLUG_RE.match(cleaned))
if not ok:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=f"{field} contains unsupported characters",
)
return cleaned
@router.get("/search", response_model=SoulsDirectorySearchResponse)
async def search(
q: str = Query(default="", min_length=0),
limit: int = Query(default=20, ge=1, le=100),
_actor: ActorContext = ADMIN_OR_AGENT_DEP,
) -> SoulsDirectorySearchResponse:
"""Search souls-directory entries by handle/slug query text."""
refs = await souls_directory.list_souls_directory_refs()
matches = souls_directory.search_souls(refs, query=q, limit=limit)
items = [
SoulsDirectorySoulRef(
handle=ref.handle,
slug=ref.slug,
page_url=ref.page_url,
raw_md_url=ref.raw_md_url,
)
for ref in matches
]
return SoulsDirectorySearchResponse(items=items)
@router.get("/{handle}/{slug}.md", response_model=SoulsDirectoryMarkdownResponse)
@router.get("/{handle}/{slug}", response_model=SoulsDirectoryMarkdownResponse)
async def get_markdown(
handle: str,
slug: str,
_actor: ActorContext = ADMIN_OR_AGENT_DEP,
) -> SoulsDirectoryMarkdownResponse:
"""Fetch markdown content for a validated souls-directory handle and slug."""
safe_handle = _validate_segment(handle, field="handle")
safe_slug = _validate_segment(slug.removesuffix(".md"), field="slug")
try:
content = await souls_directory.fetch_soul_markdown(
handle=safe_handle,
slug=safe_slug,
)
except Exception as exc:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail=str(exc),
) from exc
return SoulsDirectoryMarkdownResponse(
handle=safe_handle,
slug=safe_slug,
content=content,
)