mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-25 08:48:13 +02:00
feat: add ultravox realtime and fix signature issue in telephony
- Add UltraVox realtime - Fix signature issue on telephony
This commit is contained in:
parent
9135c2da13
commit
ea0cac63cd
24 changed files with 2082 additions and 133 deletions
|
|
@ -5,9 +5,8 @@ provider registry — see ProviderSpec.router.
|
|||
"""
|
||||
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Header, Request
|
||||
from fastapi import APIRouter, Request
|
||||
from loguru import logger
|
||||
from pipecat.utils.run_context import set_current_run_id
|
||||
from starlette.responses import HTMLResponse
|
||||
|
|
@ -18,7 +17,6 @@ from api.services.telephony.status_processor import (
|
|||
StatusCallbackRequest,
|
||||
_process_status_update,
|
||||
)
|
||||
from api.utils.common import get_backend_endpoints
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
|
@ -26,9 +24,6 @@ router = APIRouter()
|
|||
async def _handle_plivo_status_callback(
|
||||
workflow_run_id: int,
|
||||
request: Request,
|
||||
x_plivo_signature_v3: Optional[str],
|
||||
x_plivo_signature_ma_v3: Optional[str],
|
||||
x_plivo_signature_v3_nonce: Optional[str],
|
||||
):
|
||||
set_current_run_id(workflow_run_id)
|
||||
|
||||
|
|
@ -52,19 +47,14 @@ async def _handle_plivo_status_callback(
|
|||
workflow_run, workflow.organization_id
|
||||
)
|
||||
|
||||
signature = x_plivo_signature_v3 or x_plivo_signature_ma_v3
|
||||
if signature:
|
||||
backend_endpoint, _ = await get_backend_endpoints()
|
||||
callback_kind = request.url.path.split("/")[-2]
|
||||
full_url = f"{backend_endpoint}/api/v1/telephony/plivo/{callback_kind}/{workflow_run_id}"
|
||||
is_valid = await provider.verify_inbound_signature(
|
||||
full_url,
|
||||
callback_data,
|
||||
dict(request.headers),
|
||||
)
|
||||
if not is_valid:
|
||||
logger.warning(f"[run {workflow_run_id}] Invalid Plivo webhook signature")
|
||||
return {"status": "error", "reason": "invalid_signature"}
|
||||
is_valid = await provider.verify_inbound_signature(
|
||||
str(request.url),
|
||||
callback_data,
|
||||
dict(request.headers),
|
||||
)
|
||||
if not is_valid:
|
||||
logger.warning(f"[run {workflow_run_id}] Invalid Plivo webhook signature")
|
||||
return {"status": "error", "reason": "invalid_signature"}
|
||||
|
||||
parsed_data = provider.parse_status_callback(callback_data)
|
||||
status_update = StatusCallbackRequest(
|
||||
|
|
@ -88,9 +78,6 @@ async def handle_plivo_xml_webhook(
|
|||
workflow_run_id: int,
|
||||
organization_id: int,
|
||||
request: Request,
|
||||
x_plivo_signature_v3: Optional[str] = Header(None),
|
||||
x_plivo_signature_ma_v3: Optional[str] = Header(None),
|
||||
x_plivo_signature_v3_nonce: Optional[str] = Header(None),
|
||||
):
|
||||
"""
|
||||
Handle initial webhook from Plivo when an outbound call is answered.
|
||||
|
|
@ -103,26 +90,16 @@ async def handle_plivo_xml_webhook(
|
|||
form_data = await request.form()
|
||||
callback_data = dict(form_data)
|
||||
|
||||
signature = x_plivo_signature_v3 or x_plivo_signature_ma_v3
|
||||
if signature:
|
||||
backend_endpoint, _ = await get_backend_endpoints()
|
||||
full_url = (
|
||||
f"{backend_endpoint}/api/v1/telephony/plivo-xml"
|
||||
f"?workflow_id={workflow_id}"
|
||||
f"&user_id={user_id}"
|
||||
f"&workflow_run_id={workflow_run_id}"
|
||||
f"&organization_id={organization_id}"
|
||||
is_valid = await provider.verify_inbound_signature(
|
||||
str(request.url), callback_data, dict(request.headers)
|
||||
)
|
||||
if not is_valid:
|
||||
logger.warning(
|
||||
f"[run {workflow_run_id}] Invalid Plivo signature on answer webhook"
|
||||
)
|
||||
is_valid = await provider.verify_inbound_signature(
|
||||
full_url, callback_data, dict(request.headers)
|
||||
return provider.generate_error_response(
|
||||
"invalid_signature", "Invalid webhook signature."
|
||||
)
|
||||
if not is_valid:
|
||||
logger.warning(
|
||||
f"[run {workflow_run_id}] Invalid Plivo signature on answer webhook"
|
||||
)
|
||||
return provider.generate_error_response(
|
||||
"invalid_signature", "Invalid webhook signature."
|
||||
)
|
||||
|
||||
call_id = callback_data.get("CallUUID") or callback_data.get("RequestUUID")
|
||||
if call_id:
|
||||
|
|
@ -142,33 +119,15 @@ async def handle_plivo_xml_webhook(
|
|||
async def handle_plivo_hangup_callback(
|
||||
workflow_run_id: int,
|
||||
request: Request,
|
||||
x_plivo_signature_v3: Optional[str] = Header(None),
|
||||
x_plivo_signature_ma_v3: Optional[str] = Header(None),
|
||||
x_plivo_signature_v3_nonce: Optional[str] = Header(None),
|
||||
):
|
||||
"""Handle Plivo hangup callbacks."""
|
||||
return await _handle_plivo_status_callback(
|
||||
workflow_run_id,
|
||||
request,
|
||||
x_plivo_signature_v3,
|
||||
x_plivo_signature_ma_v3,
|
||||
x_plivo_signature_v3_nonce,
|
||||
)
|
||||
return await _handle_plivo_status_callback(workflow_run_id, request)
|
||||
|
||||
|
||||
@router.post("/plivo/ring-callback/{workflow_run_id}")
|
||||
async def handle_plivo_ring_callback(
|
||||
workflow_run_id: int,
|
||||
request: Request,
|
||||
x_plivo_signature_v3: Optional[str] = Header(None),
|
||||
x_plivo_signature_ma_v3: Optional[str] = Header(None),
|
||||
x_plivo_signature_v3_nonce: Optional[str] = Header(None),
|
||||
):
|
||||
"""Handle Plivo ring callbacks."""
|
||||
return await _handle_plivo_status_callback(
|
||||
workflow_run_id,
|
||||
request,
|
||||
x_plivo_signature_v3,
|
||||
x_plivo_signature_ma_v3,
|
||||
x_plivo_signature_v3_nonce,
|
||||
)
|
||||
return await _handle_plivo_status_callback(workflow_run_id, request)
|
||||
|
|
|
|||
|
|
@ -5,9 +5,8 @@ provider registry — see ProviderSpec.router.
|
|||
"""
|
||||
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Header, Request
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from loguru import logger
|
||||
from pipecat.utils.run_context import set_current_run_id
|
||||
from starlette.responses import HTMLResponse
|
||||
|
|
@ -18,14 +17,17 @@ from api.services.telephony.status_processor import (
|
|||
StatusCallbackRequest,
|
||||
_process_status_update,
|
||||
)
|
||||
from api.utils.common import get_backend_endpoints
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/twiml", include_in_schema=False)
|
||||
async def handle_twiml_webhook(
|
||||
workflow_id: int, user_id: int, workflow_run_id: int, organization_id: int
|
||||
workflow_id: int,
|
||||
user_id: int,
|
||||
workflow_run_id: int,
|
||||
organization_id: int,
|
||||
request: Request,
|
||||
):
|
||||
"""
|
||||
Handle initial webhook from telephony provider.
|
||||
|
|
@ -34,6 +36,18 @@ async def handle_twiml_webhook(
|
|||
|
||||
workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id)
|
||||
provider = await get_telephony_provider_for_run(workflow_run, organization_id)
|
||||
callback_data = dict(await request.form())
|
||||
|
||||
is_valid = await provider.verify_inbound_signature(
|
||||
str(request.url),
|
||||
callback_data,
|
||||
dict(request.headers),
|
||||
)
|
||||
if not is_valid:
|
||||
logger.warning(
|
||||
f"[run {workflow_run_id}] Invalid Twilio signature on answer webhook"
|
||||
)
|
||||
raise HTTPException(status_code=401, detail="Invalid webhook signature")
|
||||
|
||||
response_content = await provider.get_webhook_response(
|
||||
workflow_id, user_id, workflow_run_id
|
||||
|
|
@ -46,7 +60,6 @@ async def handle_twiml_webhook(
|
|||
async def handle_twilio_status_callback(
|
||||
workflow_run_id: int,
|
||||
request: Request,
|
||||
x_webhook_signature: Optional[str] = Header(None),
|
||||
):
|
||||
"""Handle Twilio-specific status callbacks."""
|
||||
set_current_run_id(workflow_run_id)
|
||||
|
|
@ -75,19 +88,14 @@ async def handle_twilio_status_callback(
|
|||
workflow_run, workflow.organization_id
|
||||
)
|
||||
|
||||
if x_webhook_signature:
|
||||
backend_endpoint, _ = await get_backend_endpoints()
|
||||
full_url = f"{backend_endpoint}/api/v1/telephony/twilio/status-callback/{workflow_run_id}"
|
||||
|
||||
is_valid = await provider.verify_webhook_signature(
|
||||
full_url, callback_data, x_webhook_signature
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
logger.warning(
|
||||
f"Invalid webhook signature for workflow run {workflow_run_id}"
|
||||
)
|
||||
return {"status": "error", "reason": "invalid_signature"}
|
||||
is_valid = await provider.verify_inbound_signature(
|
||||
str(request.url),
|
||||
callback_data,
|
||||
dict(request.headers),
|
||||
)
|
||||
if not is_valid:
|
||||
logger.warning(f"Invalid webhook signature for workflow run {workflow_run_id}")
|
||||
raise HTTPException(status_code=401, detail="Invalid webhook signature")
|
||||
|
||||
# Parse the callback data into generic format
|
||||
parsed_data = provider.parse_status_callback(callback_data)
|
||||
|
|
|
|||
|
|
@ -81,9 +81,9 @@ async def handle_vobiz_hangup_callback(
|
|||
f"[run {workflow_run_id}] Vobiz hangup callback - Headers: {json.dumps(all_headers)}"
|
||||
)
|
||||
|
||||
# Parse the callback data (Vobiz sends form data or JSON)
|
||||
form_data = await request.form()
|
||||
callback_data = dict(form_data)
|
||||
# Parse the callback data from the raw body so signed webhooks can verify
|
||||
# the exact bytes Vobiz sent without draining the request stream first.
|
||||
callback_data, raw_body = await parse_webhook_request(request)
|
||||
|
||||
# TODO: Remove this debug logging after Vobiz team clarifies webhook authentication
|
||||
logger.info(
|
||||
|
|
@ -114,10 +114,6 @@ async def handle_vobiz_hangup_callback(
|
|||
workflow_run, workflow.organization_id
|
||||
)
|
||||
|
||||
# Get raw body for signature verification
|
||||
raw_body = await request.body()
|
||||
webhook_body = raw_body.decode("utf-8")
|
||||
|
||||
# Verify signature
|
||||
backend_endpoint, _ = await get_backend_endpoints()
|
||||
webhook_url = f"{backend_endpoint}/api/v1/telephony/vobiz/hangup-callback/{workflow_run_id}"
|
||||
|
|
@ -127,7 +123,7 @@ async def handle_vobiz_hangup_callback(
|
|||
callback_data,
|
||||
x_vobiz_signature,
|
||||
x_vobiz_timestamp,
|
||||
webhook_body,
|
||||
raw_body,
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
|
|
@ -206,9 +202,9 @@ async def handle_vobiz_ring_callback(
|
|||
f"[run {workflow_run_id}] Vobiz ring callback - Headers: {json.dumps(all_headers)}"
|
||||
)
|
||||
|
||||
# Parse the callback data
|
||||
form_data = await request.form()
|
||||
callback_data = dict(form_data)
|
||||
# Parse the callback data from the raw body so signed webhooks can verify
|
||||
# the exact bytes Vobiz sent without draining the request stream first.
|
||||
callback_data, raw_body = await parse_webhook_request(request)
|
||||
|
||||
# TODO: Remove this debug logging after Vobiz team clarifies webhook authentication
|
||||
logger.info(
|
||||
|
|
@ -240,10 +236,6 @@ async def handle_vobiz_ring_callback(
|
|||
workflow_run, workflow.organization_id
|
||||
)
|
||||
|
||||
# Get raw body for signature verification
|
||||
raw_body = await request.body()
|
||||
webhook_body = raw_body.decode("utf-8")
|
||||
|
||||
# Verify signature
|
||||
backend_endpoint, _ = await get_backend_endpoints()
|
||||
webhook_url = (
|
||||
|
|
@ -255,7 +247,7 @@ async def handle_vobiz_ring_callback(
|
|||
callback_data,
|
||||
x_vobiz_signature,
|
||||
x_vobiz_timestamp,
|
||||
webhook_body,
|
||||
raw_body,
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
|
|
@ -311,9 +303,10 @@ async def handle_vobiz_hangup_callback_by_workflow(
|
|||
)
|
||||
|
||||
try:
|
||||
callback_data, _ = await parse_webhook_request(request)
|
||||
callback_data, raw_body = await parse_webhook_request(request)
|
||||
except ValueError:
|
||||
callback_data = {}
|
||||
raw_body = ""
|
||||
|
||||
call_uuid = callback_data.get("CallUUID") or callback_data.get("call_uuid")
|
||||
logger.info(
|
||||
|
|
@ -356,8 +349,6 @@ async def handle_vobiz_hangup_callback_by_workflow(
|
|||
)
|
||||
|
||||
if x_vobiz_signature:
|
||||
raw_body = await request.body()
|
||||
webhook_body = raw_body.decode("utf-8")
|
||||
backend_endpoint, _ = await get_backend_endpoints()
|
||||
webhook_url = f"{backend_endpoint}/api/v1/telephony/vobiz/hangup-callback/workflow/{workflow_id}"
|
||||
|
||||
|
|
@ -366,7 +357,7 @@ async def handle_vobiz_hangup_callback_by_workflow(
|
|||
callback_data,
|
||||
x_vobiz_signature,
|
||||
x_vobiz_timestamp,
|
||||
webhook_body,
|
||||
raw_body,
|
||||
)
|
||||
|
||||
if not is_valid:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue