122 lines
3.5 KiB
Python
122 lines
3.5 KiB
Python
from typing import Annotated
|
|
from urllib.parse import urlsplit
|
|
|
|
import asyncpg
|
|
from dishka.integrations.fastapi import DishkaRoute, FromDishka
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
|
|
from api.webhooks import send_test
|
|
from utils.read import webhooks
|
|
from utils.read.models import DeliveryView, WebhookView
|
|
|
|
router = APIRouter(prefix="/api/webhooks", tags=["webhooks"], route_class=DishkaRoute)
|
|
|
|
MAX_URL = 2048
|
|
|
|
|
|
class WebhookUpdate(BaseModel):
|
|
url: str
|
|
watch_ids: list[int] | None = None
|
|
enabled: bool = True
|
|
|
|
|
|
class WebhookCreate(WebhookUpdate):
|
|
account_id: int
|
|
|
|
|
|
def _url(value: str) -> str:
|
|
url = value.strip()
|
|
parts = urlsplit(url)
|
|
if (
|
|
len(url) > MAX_URL
|
|
or parts.scheme not in {"http", "https"}
|
|
or not parts.hostname
|
|
):
|
|
raise HTTPException(
|
|
status_code=422, detail="url must be an http or https address"
|
|
)
|
|
return url
|
|
|
|
|
|
def _watch_ids(ids: list[int] | None) -> list[int] | None:
|
|
return sorted(set(ids)) if ids else None
|
|
|
|
|
|
async def _webhook(pool: asyncpg.Pool, webhook_id: int) -> WebhookView:
|
|
webhook = await webhooks.get_webhook(pool, webhook_id)
|
|
if webhook is None:
|
|
raise HTTPException(status_code=404, detail="webhook not found")
|
|
return webhook
|
|
|
|
|
|
@router.get("")
|
|
async def list_webhooks(
|
|
pool: FromDishka[asyncpg.Pool], account_id: Annotated[int, Query()]
|
|
) -> list[WebhookView]:
|
|
return await webhooks.list_webhooks(pool, account_id)
|
|
|
|
|
|
@router.post("", status_code=201)
|
|
async def create_webhook(
|
|
pool: FromDishka[asyncpg.Pool], body: WebhookCreate
|
|
) -> WebhookView:
|
|
webhook = await webhooks.create_webhook(
|
|
pool,
|
|
body.account_id,
|
|
_url(body.url),
|
|
_watch_ids(body.watch_ids),
|
|
enabled=body.enabled,
|
|
)
|
|
if webhook is None:
|
|
raise HTTPException(status_code=500, detail="webhook was not saved")
|
|
return webhook
|
|
|
|
|
|
@router.put("/{webhook_id}")
|
|
async def update_webhook(
|
|
pool: FromDishka[asyncpg.Pool], webhook_id: int, body: WebhookUpdate
|
|
) -> WebhookView:
|
|
webhook = await webhooks.update_webhook(
|
|
pool,
|
|
webhook_id,
|
|
_url(body.url),
|
|
_watch_ids(body.watch_ids),
|
|
enabled=body.enabled,
|
|
)
|
|
if webhook is None:
|
|
raise HTTPException(status_code=404, detail="webhook not found")
|
|
return webhook
|
|
|
|
|
|
@router.delete("/{webhook_id}", status_code=204)
|
|
async def delete_webhook(pool: FromDishka[asyncpg.Pool], webhook_id: int) -> None:
|
|
if not await webhooks.delete_webhook(pool, webhook_id):
|
|
raise HTTPException(status_code=404, detail="webhook not found")
|
|
|
|
|
|
@router.post("/{webhook_id}/secret")
|
|
async def rotate_secret(pool: FromDishka[asyncpg.Pool], webhook_id: int) -> WebhookView:
|
|
webhook = await webhooks.rotate_secret(pool, webhook_id)
|
|
if webhook is None:
|
|
raise HTTPException(status_code=404, detail="webhook not found")
|
|
return webhook
|
|
|
|
|
|
@router.post("/{webhook_id}/test")
|
|
async def test_webhook(pool: FromDishka[asyncpg.Pool], webhook_id: int) -> DeliveryView:
|
|
delivery = await send_test(pool, await _webhook(pool, webhook_id))
|
|
if delivery is None:
|
|
raise HTTPException(status_code=500, detail="test delivery was not recorded")
|
|
return delivery
|
|
|
|
|
|
@router.get("/{webhook_id}/deliveries")
|
|
async def list_deliveries(
|
|
pool: FromDishka[asyncpg.Pool],
|
|
webhook_id: int,
|
|
limit: Annotated[int, Query(ge=1, le=100)] = webhooks.DELIVERY_LOG,
|
|
) -> list[DeliveryView]:
|
|
await _webhook(pool, webhook_id)
|
|
return await webhooks.list_deliveries(pool, webhook_id, limit)
|