"""社区域路由(WP-11)。""" from typing import Annotated, Any from fastapi import APIRouter, Depends, Request, status from app.auth.dependencies import get_current_user, require_state_change from app.community import service from app.conversations.routes import _domain_of from app.db.session import DbSession from app.models import User router = APIRouter(prefix="/v1/community", tags=["community"]) def _content_out(c: Any, counts: dict[str, int] | None = None) -> dict[str, Any]: out: dict[str, Any] = { "id": c.id, "kind": c.kind, "title": c.title, "body": c.body, "visibility": c.visibility, "status": c.status, "author_domain_id": c.author_domain_id, "created_at": c.created_at.isoformat(), } if counts is not None: out["reactions"] = counts return out def _comment_out(c: Any) -> dict[str, Any]: return { "id": c.id, "content_id": c.content_id, "parent_comment_id": c.parent_comment_id, "body": c.body, "author_domain_id": c.author_domain_id, "created_at": c.created_at.isoformat(), } @router.post("/contents", status_code=201) async def publish( db: DbSession, user: Annotated[User, Depends(require_state_change)], request: Request, body: dict[str, Any], ) -> dict[str, Any]: domain = await _domain_of(db, user) content = await service.create_content( db, domain.id, body, getattr(request.state, "trace_id", None) ) return _content_out(content) @router.get("/contents") async def list_contents( db: DbSession, user: Annotated[User, Depends(get_current_user)], limit: int = 20, cursor: str | None = None, ) -> dict[str, Any]: if not 1 <= limit <= 100: from fastapi import HTTPException raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "limit 取值范围 1-100") page, next_cursor, has_more = await service.list_public(db, limit, cursor) return { "data": [_content_out(c) for c in page], "next_cursor": next_cursor, "has_more": has_more, } @router.get("/contents/{content_id}") async def get_content( content_id: str, db: DbSession, user: Annotated[User, Depends(get_current_user)], ) -> dict[str, Any]: content = await service.require_content(db, content_id) counts = await service.reaction_counts(db, content_id) return _content_out(content, counts) @router.post("/contents/{content_id}/comments", status_code=201) async def comment( content_id: str, db: DbSession, user: Annotated[User, Depends(require_state_change)], request: Request, body: dict[str, str], ) -> dict[str, Any]: domain = await _domain_of(db, user) created = await service.add_comment( db, domain.id, content_id, body.get("body", ""), body.get("parent_comment_id"), getattr(request.state, "trace_id", None), ) return _comment_out(created) @router.get("/contents/{content_id}/comments") async def list_comments( content_id: str, db: DbSession, user: Annotated[User, Depends(get_current_user)], ) -> dict[str, Any]: rows = await service.list_comments(db, content_id) return {"data": [_comment_out(c) for c in rows]} @router.post("/contents/{content_id}/reactions", status_code=201) async def react( content_id: str, db: DbSession, user: Annotated[User, Depends(require_state_change)], body: dict[str, str], ) -> dict[str, str]: domain = await _domain_of(db, user) kind = body.get("kind", "") if kind not in {"like", "save"}: from fastapi import HTTPException raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "kind 取值不合法") await service.react(db, domain.id, content_id, kind, None) counts = await service.reaction_counts(db, content_id) return {k: str(v) for k, v in counts.items()} @router.delete("/contents/{content_id}/reactions", status_code=204) async def unreact( content_id: str, db: DbSession, user: Annotated[User, Depends(require_state_change)], body: dict[str, str], ) -> None: domain = await _domain_of(db, user) await service.unreact(db, domain.id, content_id, body.get("kind", "")) @router.post("/follows", status_code=201) async def follow( db: DbSession, user: Annotated[User, Depends(require_state_change)], body: dict[str, str], ) -> dict[str, Any]: domain = await _domain_of(db, user) from app.models import PersonalDomain target = await db.get(PersonalDomain, body.get("followee_domain_id", "")) if target is None: from fastapi import HTTPException raise HTTPException(status.HTTP_404_NOT_FOUND, "用户不存在") await service.follow(db, domain.id, target.id) return await service.follow_counts(db, domain.id) @router.get("/follows/counts") async def follow_counts( db: DbSession, user: Annotated[User, Depends(get_current_user)] ) -> dict[str, Any]: domain = await _domain_of(db, user) return await service.follow_counts(db, domain.id) @router.post("/reports", status_code=201) async def report( db: DbSession, user: Annotated[User, Depends(require_state_change)], request: Request, body: dict[str, str], ) -> dict[str, Any]: domain = await _domain_of(db, user) report_row, case = await service.file_report( db, domain.id, body.get("target_type", ""), body.get("target_id", ""), body.get("reason", ""), getattr(request.state, "trace_id", None), ) return {"report_id": report_row.id, "case_id": case.id, "status": case.status} @router.post("/contents/{content_id}/withdrawals", status_code=200) async def withdraw( content_id: str, db: DbSession, user: Annotated[User, Depends(require_state_change)], request: Request, ) -> dict[str, Any]: domain = await _domain_of(db, user) content = await service.withdraw_content( db, domain.id, content_id, getattr(request.state, "trace_id", None) ) return _content_out(content)