from typing import Annotated import asyncpg from dishka.integrations.fastapi import DishkaRoute, FromDishka from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel from utils.policy import overview, repository from utils.policy.models import ( CaptureToggles, ChatKind, ChatMeta, PolicyCreate, PolicyRecord, PolicyRuleView, PolicyScopeView, ) from utils.policy.resolver import resolve router = APIRouter(prefix="/api/policy", tags=["policy"], route_class=DishkaRoute) POLICY_CHANGED_CHANNEL = "policy_changed" class EffectiveQuery(BaseModel): account_id: int chat_id: int kind: ChatKind is_bot: bool = False is_contact: bool | None = None @router.get("") async def list_policies( pool: FromDishka[asyncpg.Pool], account_id: Annotated[int, Query()] ) -> list[PolicyRecord]: return await repository.list_policies(pool, account_id) @router.get("/effective") async def effective_policy( pool: FromDishka[asyncpg.Pool], query: Annotated[EffectiveQuery, Query()] ) -> CaptureToggles: folders = await repository.list_folders(pool, query.account_id) policies = await repository.load_policy_set(pool, query.account_id) chat = ChatMeta( chat_id=query.chat_id, kind=query.kind, is_bot=query.is_bot, is_contact=query.is_contact, ) return resolve(chat, folders, policies) @router.get("/overview") async def policy_overview( pool: FromDishka[asyncpg.Pool], account_id: Annotated[int, Query()] ) -> list[PolicyRuleView]: return await overview.list_rules(pool, account_id) @router.get("/scope") async def policy_scope( pool: FromDishka[asyncpg.Pool], account_id: Annotated[int, Query()], chat_id: Annotated[int | None, Query()] = None, folder_id: Annotated[int | None, Query()] = None, ) -> PolicyScopeView: if chat_id is not None: return await overview.chat_scope(pool, account_id, chat_id) scope = ( await overview.folder_scope(pool, account_id, folder_id) if folder_id is not None else None ) if scope is None: raise HTTPException(status_code=404, detail="scope not found") return scope @router.post("", status_code=201) async def create_policy( pool: FromDishka[asyncpg.Pool], body: PolicyCreate ) -> PolicyRecord: record = await repository.create_policy( pool, body.account_id, body.scope_type, body.scope_id, body ) await pool.execute(f"NOTIFY {POLICY_CHANGED_CHANNEL}") return record @router.get("/{policy_id}") async def get_policy(pool: FromDishka[asyncpg.Pool], policy_id: int) -> PolicyRecord: record = await repository.get_policy(pool, policy_id) if record is None: raise HTTPException(status_code=404, detail="policy not found") return record @router.put("/{policy_id}") async def update_policy( pool: FromDishka[asyncpg.Pool], policy_id: int, body: CaptureToggles, account_id: Annotated[int | None, Query()] = None, ) -> PolicyRecord: if account_id is None: record = await repository.update_policy(pool, policy_id, body) else: record = await repository.override_policy(pool, policy_id, account_id, body) if record is None: raise HTTPException(status_code=404, detail="policy not found") await pool.execute(f"NOTIFY {POLICY_CHANGED_CHANNEL}") return record @router.delete("/{policy_id}", status_code=204) async def delete_policy(pool: FromDishka[asyncpg.Pool], policy_id: int) -> None: if not await repository.delete_policy(pool, policy_id): raise HTTPException(status_code=404, detail="policy not found") await pool.execute(f"NOTIFY {POLICY_CHANGED_CHANNEL}")