deerflow-code/offline-backend-20260512/backend/app/gateway/routers/notifications.py
2026-09-07 18:24:55 +08:00

153 lines
4.6 KiB
Python

"""In-app notification inbox API."""
from __future__ import annotations
import logging
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from pydantic import BaseModel, Field
from app.gateway.deps import get_notification_store
from deerflow.persistence.notifications import NotificationStore
from deerflow.runtime.user_context import get_effective_user_id
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["notifications"])
def _current_user_id(request: Request) -> str:
user = getattr(request.state, "user", None)
if user is not None:
return str(user.id)
return get_effective_user_id()
class NotificationItem(BaseModel):
id: str
user_id: str
type: str
title: str
body: str
payload: dict[str, Any]
read: bool
created_at: str | None
class NotificationListResponse(BaseModel):
items: list[NotificationItem]
class NotificationUnreadCountResponse(BaseModel):
unread: int = Field(..., description="Number of unread notifications for the current user")
class MarkReadResponse(BaseModel):
success: bool
class MarkAllReadResponse(BaseModel):
updated: int
@router.get(
"/notifications",
response_model=NotificationListResponse,
summary="List Notifications",
)
async def list_notifications(
request: Request,
unread_only: bool = Query(default=False),
limit: int = Query(default=50, ge=1, le=200),
store: NotificationStore = Depends(get_notification_store),
) -> NotificationListResponse:
try:
items = await store.list_for_user(
_current_user_id(request),
unread_only=unread_only,
limit=limit,
)
return NotificationListResponse(items=[NotificationItem(**item) for item in items])
except Exception as e:
logger.error("Failed to list notifications: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to list notifications: {e}")
@router.get(
"/notifications/unread_count",
response_model=NotificationUnreadCountResponse,
summary="Unread Notification Count",
)
async def unread_count(
request: Request,
store: NotificationStore = Depends(get_notification_store),
) -> NotificationUnreadCountResponse:
try:
count = await store.unread_count(_current_user_id(request))
return NotificationUnreadCountResponse(unread=count)
except Exception as e:
logger.error("Failed to read unread notification count: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to read unread count: {e}")
@router.post(
"/notifications/{notification_id}/read",
response_model=MarkReadResponse,
summary="Mark Notification Read",
)
async def mark_read(
notification_id: str,
request: Request,
store: NotificationStore = Depends(get_notification_store),
) -> MarkReadResponse:
try:
ok = await store.mark_read(notification_id, _current_user_id(request))
if not ok:
raise HTTPException(status_code=404, detail="Notification not found")
return MarkReadResponse(success=True)
except HTTPException:
raise
except Exception as e:
logger.error("Failed to mark notification read: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to mark read: {e}")
@router.post(
"/notifications/read_all",
response_model=MarkAllReadResponse,
summary="Mark All Notifications Read",
)
async def mark_all_read(
request: Request,
store: NotificationStore = Depends(get_notification_store),
) -> MarkAllReadResponse:
try:
count = await store.mark_all_read(_current_user_id(request))
return MarkAllReadResponse(updated=count)
except Exception as e:
logger.error("Failed to mark all notifications read: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to mark all read: {e}")
@router.delete(
"/notifications/{notification_id}",
response_model=MarkReadResponse,
summary="Delete Notification",
)
async def delete_notification(
notification_id: str,
request: Request,
store: NotificationStore = Depends(get_notification_store),
) -> MarkReadResponse:
try:
ok = await store.delete(notification_id, _current_user_id(request))
if not ok:
raise HTTPException(status_code=404, detail="Notification not found")
return MarkReadResponse(success=True)
except HTTPException:
raise
except Exception as e:
logger.error("Failed to delete notification: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail=f"Failed to delete: {e}")