feat: add cloudonix outbound telephony (#101)

Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
This commit is contained in:
Nir Simionovich 2026-01-03 08:32:21 +02:00 committed by GitHub
parent a33fa6cffe
commit 90b690efff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1080 additions and 47 deletions

View file

@ -6,6 +6,8 @@ from api.db import db_client
from api.db.models import UserModel
from api.enums import OrganizationConfigurationKey
from api.schemas.telephony_config import (
CloudonixConfigurationRequest,
CloudonixConfigurationResponse,
TelephonyConfigurationResponse,
TwilioConfigurationRequest,
TwilioConfigurationResponse,
@ -24,6 +26,7 @@ PROVIDER_MASKED_FIELDS = {
"twilio": ["account_sid", "auth_token"],
"vonage": ["private_key", "api_key", "api_secret"],
"vobiz": ["auth_id", "auth_token"],
"cloudonix": ["bearer_token"],
}
@ -60,6 +63,7 @@ async def get_telephony_configuration(user: UserModel = Depends(get_user)):
),
vonage=None,
vobiz=None,
cloudonix=None,
)
elif stored_provider == "vonage":
application_id = config.value.get("application_id", "")
@ -83,6 +87,7 @@ async def get_telephony_configuration(user: UserModel = Depends(get_user)):
from_numbers=from_numbers,
),
vobiz=None,
cloudonix=None,
)
elif stored_provider == "vobiz":
auth_id = config.value.get("auth_id", "")
@ -100,6 +105,23 @@ async def get_telephony_configuration(user: UserModel = Depends(get_user)):
auth_token=mask_key(auth_token) if auth_token else "",
from_numbers=from_numbers,
),
cloudonix=None,
)
elif stored_provider == "cloudonix":
bearer_token = config.value.get("bearer_token", "")
domain_id = config.value.get("domain_id", "")
from_numbers = config.value.get("from_numbers", [])
return TelephonyConfigurationResponse(
twilio=None,
vonage=None,
cloudonix=CloudonixConfigurationResponse(
provider="cloudonix",
bearer_token=mask_key(bearer_token) if bearer_token else "",
domain_id=domain_id,
from_numbers=from_numbers,
),
vobiz=None,
)
else:
return TelephonyConfigurationResponse()
@ -111,6 +133,7 @@ async def save_telephony_configuration(
TwilioConfigurationRequest,
VonageConfigurationRequest,
VobizConfigurationRequest,
CloudonixConfigurationRequest,
],
user: UserModel = Depends(get_user),
):
@ -148,6 +171,13 @@ async def save_telephony_configuration(
"auth_token": request.auth_token,
"from_numbers": request.from_numbers,
}
elif request.provider == "cloudonix":
config_value = {
"provider": "cloudonix",
"bearer_token": request.bearer_token,
"domain_id": request.domain_id,
"from_numbers": request.from_numbers,
}
else:
raise HTTPException(
status_code=400, detail=f"Unsupported provider: {request.provider}"

View file

@ -152,11 +152,14 @@ async def initiate_call(
f"&organization_id={user.selected_organization_id}"
)
keywords = {"workflow_id": request.workflow_id, "user_id": user.id}
# Initiate call via provider
result = await provider.initiate_call(
to_number=phone_number,
webhook_url=webhook_url,
workflow_run_id=workflow_run_id,
**keywords,
)
# Store provider type and any provider-specific metadata in workflow run context
@ -303,6 +306,7 @@ async def handle_twilio_status_callback(
# Parse form data
form_data = await request.form()
callback_data = dict(form_data)
logger.info(
f"[run {workflow_run_id}] Received status callback: {json.dumps(callback_data)}"
)
@ -646,3 +650,60 @@ async def handle_vobiz_ring_callback(
logger.info(f"[run {workflow_run_id}] Vobiz ring callback logged")
return {"status": "success"}
@router.post("/cloudonix/status-callback/{workflow_run_id}")
async def handle_cloudonix_status_callback(
workflow_run_id: int,
request: Request,
):
"""Handle Cloudonix-specific status callbacks.
Cloudonix sends call status updates to the callback URL specified during call initiation.
"""
# Parse callback data - determine if JSON or form data
content_type = request.headers.get("content-type", "")
if "application/json" in content_type:
callback_data = await request.json()
else:
# Assume form data (like Twilio)
form_data = await request.form()
callback_data = dict(form_data)
logger.info(
f"[run {workflow_run_id}] Received Cloudonix status callback: {json.dumps(callback_data)}"
)
# Get workflow run to find organization
workflow_run = await db_client.get_workflow_run_by_id(workflow_run_id)
if not workflow_run:
logger.warning(f"Workflow run {workflow_run_id} not found for status 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"Workflow {workflow_run.workflow_id} not found")
return {"status": "ignored", "reason": "workflow_not_found"}
provider = await get_telephony_provider(workflow.organization_id)
# Parse the callback data into generic format
parsed_data = provider.parse_status_callback(callback_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)
return {"status": "success"}