Spaces:
Paused
Paused
File size: 3,569 Bytes
fa8d021 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | import asyncio
import hashlib
import hmac
import json
import logging
import httpx
from sqlalchemy.ext.asyncio import AsyncSession
from src.config import settings
from src.crud.webhook import list_webhook_endpoints
from src.dependencies import tracked_db
from src.utils.formatting import utc_now_iso
from src.utils.queue_payload import WebhookPayload
logger = logging.getLogger(__name__)
async def deliver_webhook(payload: WebhookPayload, workspace_name: str) -> None:
"""
Deliver a single webhook event to its configured endpoints.
"""
try:
async with tracked_db("webhook.deliver") as db:
webhook_urls = await _get_webhook_urls(db, workspace_name)
if not webhook_urls:
logger.debug(
f"No webhook endpoints for workspace {workspace_name}, skipping."
)
return
event_payload = {
"type": payload.event_type,
"data": payload.data,
"timestamp": utc_now_iso(),
}
event_json = json.dumps(event_payload, separators=(",", ":"), sort_keys=True)
try:
signature = _generate_webhook_signature(event_json)
except ValueError:
logger.exception("Failed to generate webhook signature")
return
async with httpx.AsyncClient(timeout=30.0) as client:
tasks = [
client.post(
url=url,
content=event_json,
headers={
"Content-Type": "application/json",
"X-Honcho-Signature": signature,
},
)
for url in webhook_urls
]
results = await asyncio.gather(*tasks, return_exceptions=True)
for url, result in zip(webhook_urls, results, strict=False):
if isinstance(result, httpx.Response):
if 200 <= result.status_code < 300:
logger.debug(
f"Successfully delivered webhook {payload.event_type} to {url}"
)
else:
logger.error(
f"Failed delivery for {payload.event_type} to {url}. Status: {result.status_code}"
)
else:
logger.error(
f"Failed delivery for {payload.event_type} to {url}. Exception: {result}"
)
except httpx.RequestError:
logger.exception(f"Error sending webhook for {workspace_name}.")
except Exception:
logger.exception("Unexpected error delivering webhook.")
async def _get_webhook_urls(db: AsyncSession, workspace_name: str) -> list[str]:
"""
Get all webhook endpoint URLs for a workspace.
"""
try:
endpoints = await list_webhook_endpoints(workspace_name)
result = await db.execute(endpoints)
return [endpoint.url for endpoint in result.scalars().all()]
except Exception:
logger.exception(f"Error fetching endpoints for {workspace_name}")
return []
def _generate_webhook_signature(payload: str) -> str:
"""
Generate HMAC-SHA256 signature for webhook payload using WEBHOOK_SECRET.
"""
webhook_secret = settings.WEBHOOK.SECRET
if not webhook_secret:
raise ValueError("WEBHOOK_SECRET not found - cannot sign webhook")
return hmac.new(
webhook_secret.encode("utf-8"), payload.encode("utf-8"), hashlib.sha256
).hexdigest()
|