mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-06-22 08:38:13 +02:00
feat: add asterisk ARI websocket interface (#159)
* chore: remove old files * feat: ari outbound dialing * feat: add websocket configuration for ARI * feat: handling inbound calls * delete ext channel from redis on stasis end * fix: add lock in workflow run update, refactor _handle_stasis_start * chore: update submodule --------- Co-authored-by: Sabiha Khan <sabihak89@gmail.com>
This commit is contained in:
parent
ee4a874e54
commit
7552b6c819
37 changed files with 2076 additions and 4172 deletions
|
|
@ -8,6 +8,8 @@ from api.db import db_client
|
|||
from api.db.models import UserModel
|
||||
from api.enums import OrganizationConfigurationKey
|
||||
from api.schemas.telephony_config import (
|
||||
ARIConfigurationRequest,
|
||||
ARIConfigurationResponse,
|
||||
CloudonixConfigurationRequest,
|
||||
CloudonixConfigurationResponse,
|
||||
TelephonyConfigurationResponse,
|
||||
|
|
@ -29,6 +31,7 @@ PROVIDER_MASKED_FIELDS = {
|
|||
"vonage": ["private_key", "api_key", "api_secret"],
|
||||
"vobiz": ["auth_id", "auth_token"],
|
||||
"cloudonix": ["bearer_token"],
|
||||
"ari": ["app_password"],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -125,6 +128,26 @@ async def get_telephony_configuration(user: UserModel = Depends(get_user)):
|
|||
),
|
||||
vobiz=None,
|
||||
)
|
||||
elif stored_provider == "ari":
|
||||
ari_endpoint = config.value.get("ari_endpoint", "")
|
||||
app_name = config.value.get("app_name", "")
|
||||
app_password = config.value.get("app_password", "")
|
||||
ws_client_name = config.value.get("ws_client_name", "")
|
||||
from_numbers = config.value.get("from_numbers", [])
|
||||
|
||||
inbound_workflow_id = config.value.get("inbound_workflow_id")
|
||||
|
||||
return TelephonyConfigurationResponse(
|
||||
ari=ARIConfigurationResponse(
|
||||
provider="ari",
|
||||
ari_endpoint=ari_endpoint,
|
||||
app_name=app_name,
|
||||
app_password=mask_key(app_password) if app_password else "",
|
||||
ws_client_name=ws_client_name,
|
||||
inbound_workflow_id=inbound_workflow_id,
|
||||
from_numbers=from_numbers,
|
||||
),
|
||||
)
|
||||
else:
|
||||
return TelephonyConfigurationResponse()
|
||||
|
||||
|
|
@ -136,6 +159,7 @@ async def save_telephony_configuration(
|
|||
VonageConfigurationRequest,
|
||||
VobizConfigurationRequest,
|
||||
CloudonixConfigurationRequest,
|
||||
ARIConfigurationRequest,
|
||||
],
|
||||
user: UserModel = Depends(get_user),
|
||||
):
|
||||
|
|
@ -180,6 +204,16 @@ async def save_telephony_configuration(
|
|||
"domain_id": request.domain_id,
|
||||
"from_numbers": request.from_numbers,
|
||||
}
|
||||
elif request.provider == "ari":
|
||||
config_value = {
|
||||
"provider": "ari",
|
||||
"ari_endpoint": request.ari_endpoint,
|
||||
"app_name": request.app_name,
|
||||
"app_password": request.app_password,
|
||||
"ws_client_name": request.ws_client_name,
|
||||
"inbound_workflow_id": request.inbound_workflow_id,
|
||||
"from_numbers": request.from_numbers,
|
||||
}
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"Unsupported provider: {request.provider}"
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
import random
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from api.db import db_client
|
||||
from api.enums import WorkflowRunMode
|
||||
from api.services.pipecat.run_pipeline import run_pipeline_ari_stasis
|
||||
from api.services.telephony.stasis_rtp_connection import StasisRTPConnection
|
||||
from pipecat.utils.run_context import set_current_run_id
|
||||
|
||||
|
||||
async def on_stasis_call(call: StasisRTPConnection, call_context_vars: dict):
|
||||
workflow_id = call_context_vars.get("workflow_id") or call_context_vars.get(
|
||||
"campaign_id"
|
||||
)
|
||||
user_id = call_context_vars.get("user_id")
|
||||
|
||||
assert workflow_id is not None
|
||||
assert user_id is not None
|
||||
|
||||
try:
|
||||
workflow_id = int(workflow_id)
|
||||
user_id = int(user_id)
|
||||
except ValueError:
|
||||
logger.error(f"Invalid workflow ID or user ID: {workflow_id} or {user_id}")
|
||||
return
|
||||
|
||||
workflow_run_name = f"WR-ARI-{random.randint(1000, 9999)}"
|
||||
workflow_run = await db_client.create_workflow_run(
|
||||
workflow_run_name, workflow_id, WorkflowRunMode.STASIS.value, user_id
|
||||
)
|
||||
|
||||
set_current_run_id(workflow_run.id)
|
||||
|
||||
# Store the workflow_run_id in the connection for later use
|
||||
call.workflow_run_id = workflow_run.id
|
||||
|
||||
# Connect channelID with Workflow run ID in logs
|
||||
logger.info(
|
||||
f"channelID: {call.caller_channel_id} run_id: {workflow_run.id} "
|
||||
f"Received call for workflow ID {workflow_id}, user ID {user_id}"
|
||||
)
|
||||
await run_pipeline_ari_stasis(
|
||||
call, workflow_id, workflow_run.id, user_id, call_context_vars
|
||||
)
|
||||
|
|
@ -523,13 +523,47 @@ async def handle_ncco_webhook(
|
|||
return json.loads(response_content)
|
||||
|
||||
|
||||
@router.websocket("/ws/ari")
|
||||
async def websocket_ari_endpoint(websocket: WebSocket):
|
||||
"""WebSocket endpoint for ARI chan_websocket external media.
|
||||
|
||||
Asterisk connects here via chan_websocket. Routing params are passed as
|
||||
query params (appended by the v() dial string option in externalMedia).
|
||||
"""
|
||||
workflow_id = websocket.query_params.get("workflow_id")
|
||||
user_id = websocket.query_params.get("user_id")
|
||||
workflow_run_id = websocket.query_params.get("workflow_run_id")
|
||||
|
||||
if not workflow_id or not user_id or not workflow_run_id:
|
||||
logger.error(
|
||||
f"ARI WebSocket missing query params: "
|
||||
f"workflow_id={workflow_id}, user_id={user_id}, workflow_run_id={workflow_run_id}"
|
||||
)
|
||||
await websocket.close(code=4400, reason="Missing required query params")
|
||||
return
|
||||
|
||||
# Accept with "media" subprotocol — chan_websocket sends
|
||||
# Sec-WebSocket-Protocol: media and requires it echoed back.
|
||||
await websocket.accept(subprotocol="media")
|
||||
|
||||
await _handle_telephony_websocket(
|
||||
websocket, int(workflow_id), int(user_id), int(workflow_run_id)
|
||||
)
|
||||
|
||||
|
||||
@router.websocket("/ws/{workflow_id}/{user_id}/{workflow_run_id}")
|
||||
async def websocket_endpoint(
|
||||
websocket: WebSocket, workflow_id: int, user_id: int, workflow_run_id: int
|
||||
):
|
||||
"""WebSocket endpoint for real-time call handling - routes to provider-specific handlers."""
|
||||
await websocket.accept()
|
||||
await _handle_telephony_websocket(websocket, workflow_id, user_id, workflow_run_id)
|
||||
|
||||
|
||||
async def _handle_telephony_websocket(
|
||||
websocket: WebSocket, workflow_id: int, user_id: int, workflow_run_id: int
|
||||
):
|
||||
"""Shared WebSocket handler logic (connection already accepted)."""
|
||||
try:
|
||||
# Set the run context
|
||||
set_current_run_id(workflow_run_id)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue