153 lines
4.6 KiB
Python
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}")
|