mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-16 08:25:18 +02:00
feat: added vobiz telephony (#65)
* feat: added vobiz telephony * chore: run formatter * chore: add migration * Add tsclient --------- Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
This commit is contained in:
parent
749a0c557f
commit
09897cb5d8
16 changed files with 994 additions and 19 deletions
|
|
@ -9,6 +9,8 @@ from api.schemas.telephony_config import (
|
|||
TelephonyConfigurationResponse,
|
||||
TwilioConfigurationRequest,
|
||||
TwilioConfigurationResponse,
|
||||
VobizConfigurationRequest,
|
||||
VobizConfigurationResponse,
|
||||
VonageConfigurationRequest,
|
||||
VonageConfigurationResponse,
|
||||
)
|
||||
|
|
@ -21,6 +23,7 @@ router = APIRouter(prefix="/organizations", tags=["organizations"])
|
|||
PROVIDER_MASKED_FIELDS = {
|
||||
"twilio": ["account_sid", "auth_token"],
|
||||
"vonage": ["private_key", "api_key", "api_secret"],
|
||||
"vobiz": ["auth_id", "auth_token"],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -56,6 +59,7 @@ async def get_telephony_configuration(user: UserModel = Depends(get_user)):
|
|||
from_numbers=from_numbers,
|
||||
),
|
||||
vonage=None,
|
||||
vobiz=None,
|
||||
)
|
||||
elif stored_provider == "vonage":
|
||||
application_id = config.value.get("application_id", "")
|
||||
|
|
@ -78,6 +82,24 @@ async def get_telephony_configuration(user: UserModel = Depends(get_user)):
|
|||
api_secret=mask_key(api_secret) if api_secret else None,
|
||||
from_numbers=from_numbers,
|
||||
),
|
||||
vobiz=None,
|
||||
)
|
||||
elif stored_provider == "vobiz":
|
||||
auth_id = config.value.get("auth_id", "")
|
||||
auth_token = config.value.get("auth_token", "")
|
||||
from_numbers = (
|
||||
config.value.get("from_numbers", []) if auth_id and auth_token else []
|
||||
)
|
||||
|
||||
return TelephonyConfigurationResponse(
|
||||
twilio=None,
|
||||
vonage=None,
|
||||
vobiz=VobizConfigurationResponse(
|
||||
provider="vobiz",
|
||||
auth_id=mask_key(auth_id) if auth_id else "",
|
||||
auth_token=mask_key(auth_token) if auth_token else "",
|
||||
from_numbers=from_numbers,
|
||||
),
|
||||
)
|
||||
else:
|
||||
return TelephonyConfigurationResponse()
|
||||
|
|
@ -85,7 +107,11 @@ async def get_telephony_configuration(user: UserModel = Depends(get_user)):
|
|||
|
||||
@router.post("/telephony-config")
|
||||
async def save_telephony_configuration(
|
||||
request: Union[TwilioConfigurationRequest, VonageConfigurationRequest],
|
||||
request: Union[
|
||||
TwilioConfigurationRequest,
|
||||
VonageConfigurationRequest,
|
||||
VobizConfigurationRequest,
|
||||
],
|
||||
user: UserModel = Depends(get_user),
|
||||
):
|
||||
"""Save telephony configuration for the user's organization."""
|
||||
|
|
@ -115,6 +141,13 @@ async def save_telephony_configuration(
|
|||
"api_secret": getattr(request, "api_secret", None),
|
||||
"from_numbers": request.from_numbers,
|
||||
}
|
||||
elif request.provider == "vobiz":
|
||||
config_value = {
|
||||
"provider": "vobiz",
|
||||
"auth_id": request.auth_id,
|
||||
"auth_token": request.auth_token,
|
||||
"from_numbers": request.from_numbers,
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"Unsupported provider: {request.provider}"
|
||||
|
|
|
|||
|
|
@ -234,7 +234,10 @@ async def websocket_endpoint(
|
|||
provider_type = workflow_run.gathered_context.get("provider")
|
||||
|
||||
if not provider_type:
|
||||
logger.error(f"No provider type found in workflow run {workflow_run_id}")
|
||||
logger.error(
|
||||
f"No provider type found in workflow run {workflow_run_id}. "
|
||||
f"gathered_context: {workflow_run.gathered_context}, mode: {workflow_run.mode}"
|
||||
)
|
||||
await websocket.close(code=4400, reason="Provider type not found")
|
||||
return
|
||||
|
||||
|
|
@ -483,3 +486,160 @@ async def handle_vonage_events(
|
|||
|
||||
# Return 204 No Content as expected by Vonage
|
||||
return {"status": "ok"}
|
||||
|
||||
|
||||
@router.post("/vobiz-xml", include_in_schema=False)
|
||||
async def handle_vobiz_xml_webhook(
|
||||
workflow_id: int, user_id: int, workflow_run_id: int, organization_id: int
|
||||
):
|
||||
"""
|
||||
Handle initial webhook from Vobiz when call is answered.
|
||||
Returns Vobiz XML response with Stream element.
|
||||
|
||||
Vobiz uses Plivo-compatible XML format similar to Twilio's TwiML.
|
||||
"""
|
||||
logger.info(
|
||||
f"[run {workflow_run_id}] Vobiz XML webhook called - "
|
||||
f"workflow_id={workflow_id}, user_id={user_id}, org_id={organization_id}"
|
||||
)
|
||||
|
||||
provider = await get_telephony_provider(organization_id)
|
||||
|
||||
logger.debug(f"[run {workflow_run_id}] Using provider: {provider.PROVIDER_NAME}")
|
||||
|
||||
response_content = await provider.get_webhook_response(
|
||||
workflow_id, user_id, workflow_run_id
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
f"[run {workflow_run_id}] Vobiz XML response generated:\n{response_content}"
|
||||
)
|
||||
|
||||
return HTMLResponse(content=response_content, media_type="application/xml")
|
||||
|
||||
|
||||
@router.post("/vobiz/hangup-callback/{workflow_run_id}")
|
||||
async def handle_vobiz_hangup_callback(
|
||||
workflow_run_id: int,
|
||||
request: Request,
|
||||
):
|
||||
"""Handle Vobiz hangup callback (sent when call ends).
|
||||
|
||||
Vobiz sends callbacks to hangup_url when the call terminates.
|
||||
This includes call duration, status, and billing information.
|
||||
"""
|
||||
# Parse the callback data (Vobiz sends form data or JSON)
|
||||
try:
|
||||
callback_data = await request.json()
|
||||
logger.info(
|
||||
f"[run {workflow_run_id}] Received Vobiz hangup callback (JSON): "
|
||||
f"{json.dumps(callback_data)}"
|
||||
)
|
||||
except Exception:
|
||||
# Fallback to form data if JSON fails
|
||||
form_data = await request.form()
|
||||
callback_data = dict(form_data)
|
||||
logger.info(
|
||||
f"[run {workflow_run_id}] Received Vobiz hangup callback (form): "
|
||||
f"{json.dumps(callback_data)}"
|
||||
)
|
||||
|
||||
# Get workflow run for processing
|
||||
workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id)
|
||||
if not workflow_run:
|
||||
logger.warning(
|
||||
f"[run {workflow_run_id}] Workflow run not found for Vobiz hangup callback"
|
||||
)
|
||||
return {"status": "ignored", "reason": "workflow_run_not_found"}
|
||||
|
||||
# Get workflow and provider
|
||||
workflow = await db_client.get_workflow_by_id(workflow_run.workflow_id)
|
||||
if not workflow:
|
||||
logger.warning(f"[run {workflow_run_id}] Workflow not found")
|
||||
return {"status": "ignored", "reason": "workflow_not_found"}
|
||||
|
||||
provider = await get_telephony_provider(workflow.organization_id)
|
||||
|
||||
logger.debug(
|
||||
f"[run {workflow_run_id}] Processing Vobiz hangup with provider: {provider.PROVIDER_NAME}"
|
||||
)
|
||||
|
||||
# Parse the callback data into generic format
|
||||
parsed_data = provider.parse_status_callback(callback_data)
|
||||
|
||||
logger.debug(
|
||||
f"[run {workflow_run_id}] Parsed Vobiz callback data: {json.dumps(parsed_data)}"
|
||||
)
|
||||
|
||||
# Create StatusCallbackRequest from parsed data
|
||||
status_update = StatusCallbackRequest(
|
||||
call_id=parsed_data["call_id"],
|
||||
status=parsed_data["status"],
|
||||
from_number=parsed_data.get("from_number"),
|
||||
to_number=parsed_data.get("to_number"),
|
||||
direction=parsed_data.get("direction"),
|
||||
duration=parsed_data.get("duration"),
|
||||
extra=parsed_data.get("extra", {}),
|
||||
)
|
||||
|
||||
# Process the status update
|
||||
await _process_status_update(workflow_run_id, status_update, workflow_run)
|
||||
|
||||
logger.info(f"[run {workflow_run_id}] Vobiz hangup callback processed successfully")
|
||||
|
||||
return {"status": "success"}
|
||||
|
||||
|
||||
@router.post("/vobiz/ring-callback/{workflow_run_id}")
|
||||
async def handle_vobiz_ring_callback(
|
||||
workflow_run_id: int,
|
||||
request: Request,
|
||||
):
|
||||
"""Handle Vobiz ring callback (sent when call starts ringing).
|
||||
|
||||
Vobiz can send callbacks to ring_url when the call starts ringing.
|
||||
This is optional and used for tracking ringing status.
|
||||
"""
|
||||
# Parse the callback data
|
||||
try:
|
||||
callback_data = await request.json()
|
||||
logger.info(
|
||||
f"[run {workflow_run_id}] Received Vobiz ring callback (JSON): "
|
||||
f"{json.dumps(callback_data)}"
|
||||
)
|
||||
except Exception:
|
||||
form_data = await request.form()
|
||||
callback_data = dict(form_data)
|
||||
logger.info(
|
||||
f"[run {workflow_run_id}] Received Vobiz ring callback (form): "
|
||||
f"{json.dumps(callback_data)}"
|
||||
)
|
||||
|
||||
# Get workflow run for processing
|
||||
workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id)
|
||||
if not workflow_run:
|
||||
logger.warning(
|
||||
f"[run {workflow_run_id}] Workflow run not found for Vobiz ring callback"
|
||||
)
|
||||
return {"status": "ignored", "reason": "workflow_run_not_found"}
|
||||
|
||||
# Log the ringing event
|
||||
telephony_callback_logs = workflow_run.logs.get("telephony_status_callbacks", [])
|
||||
ring_log = {
|
||||
"status": "ringing",
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
"call_id": callback_data.get("call_uuid", callback_data.get("CallUUID", "")),
|
||||
"event_type": "ring",
|
||||
"raw_data": callback_data,
|
||||
}
|
||||
telephony_callback_logs.append(ring_log)
|
||||
|
||||
# Update workflow run logs
|
||||
await db_client.update_workflow_run(
|
||||
run_id=workflow_run_id,
|
||||
logs={"telephony_status_callbacks": telephony_callback_logs},
|
||||
)
|
||||
|
||||
logger.info(f"[run {workflow_run_id}] Vobiz ring callback logged")
|
||||
|
||||
return {"status": "success"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue