feat: Enable telephony for OSS (#21)

* fix: fix tooltip bug

* feat: add Twilio with CloudFlare configuration

* chore: update Tella Video
This commit is contained in:
Abhishek 2025-10-04 12:22:50 +05:30 committed by GitHub
parent d39a8111a6
commit 8e2e5c9327
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 891 additions and 191 deletions

View file

@ -168,10 +168,10 @@ async def start_campaign(
user: UserModel = Depends(get_user),
) -> CampaignResponse:
"""Start campaign execution"""
# Check if organization has TWILIO_PHONE_NUMBERS configured
# Check if organization has TWILIO_CONFIGURATION configured
twilio_config = await db_client.get_configuration(
user.selected_organization_id,
OrganizationConfigurationKey.TWILIO_PHONE_NUMBERS.value,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value,
)
if (
@ -280,10 +280,10 @@ async def resume_campaign(
user: UserModel = Depends(get_user),
) -> CampaignResponse:
"""Resume a paused campaign"""
# Check if organization has TWILIO_PHONE_NUMBERS configured
# Check if organization has TWILIO_CONFIGURATION configured
twilio_config = await db_client.get_configuration(
user.selected_organization_id,
OrganizationConfigurationKey.TWILIO_PHONE_NUMBERS.value,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value,
)
if (

View file

@ -4,6 +4,7 @@ from loguru import logger
from api.routes.campaign import router as campaign_router
from api.routes.integration import router as integration_router
from api.routes.looptalk import router as looptalk_router
from api.routes.organization import router as organization_router
from api.routes.organization_usage import router as organization_usage_router
from api.routes.reports import router as reports_router
from api.routes.rtc_offer import router as rtc_offer_router
@ -27,6 +28,7 @@ router.include_router(workflow_router)
router.include_router(user_router)
router.include_router(campaign_router)
router.include_router(integration_router)
router.include_router(organization_router)
router.include_router(s3_router)
router.include_router(service_keys_router)
router.include_router(looptalk_router)

View file

@ -0,0 +1,85 @@
from fastapi import APIRouter, Depends, HTTPException
from api.db import db_client
from api.db.models import UserModel
from api.enums import OrganizationConfigurationKey
from api.schemas.telephony_config import (
TelephonyConfigurationResponse,
TwilioConfigurationRequest,
TwilioConfigurationResponse,
)
from api.services.auth.depends import get_user
from api.services.configuration.masking import is_mask_of, mask_key
router = APIRouter(prefix="/organizations", tags=["organizations"])
@router.get("/telephony-config", response_model=TelephonyConfigurationResponse)
async def get_telephony_configuration(user: UserModel = Depends(get_user)):
"""Get telephony configuration for the user's organization with masked sensitive fields."""
if not user.selected_organization_id:
raise HTTPException(status_code=400, detail="No organization selected")
config = await db_client.get_configuration(
user.selected_organization_id,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value,
)
if not config or not config.value:
return TelephonyConfigurationResponse(twilio=None)
# Mask sensitive fields (account_sid and auth_token) before returning
account_sid = config.value.get("account_sid", "")
auth_token = config.value.get("auth_token", "")
return TelephonyConfigurationResponse(
twilio=TwilioConfigurationResponse(
provider="twilio",
account_sid=mask_key(account_sid) if account_sid else "",
auth_token=mask_key(auth_token) if auth_token else "",
from_numbers=config.value.get("from_numbers", []),
)
)
@router.post("/telephony-config")
async def save_telephony_configuration(
request: TwilioConfigurationRequest, user: UserModel = Depends(get_user)
):
"""Save telephony configuration for the user's organization."""
if not user.selected_organization_id:
raise HTTPException(status_code=400, detail="No organization selected")
# Fetch existing configuration to handle masked values
existing_config = await db_client.get_configuration(
user.selected_organization_id,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value,
)
# Build new configuration
config_value = {
"provider": request.provider,
"account_sid": request.account_sid,
"auth_token": request.auth_token,
"from_numbers": request.from_numbers,
}
# If incoming values are masked (same as stored masked value), keep the original
if existing_config and existing_config.value:
# Check if account_sid is unchanged (masked value matches)
stored_account_sid = existing_config.value.get("account_sid", "")
if stored_account_sid and is_mask_of(request.account_sid, stored_account_sid):
config_value["account_sid"] = stored_account_sid # Keep original
# Check if auth_token is unchanged (masked value matches)
stored_auth_token = existing_config.value.get("auth_token", "")
if stored_auth_token and is_mask_of(request.auth_token, stored_auth_token):
config_value["auth_token"] = stored_auth_token # Keep original
await db_client.upsert_configuration(
user.selected_organization_id,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value,
config_value,
)
return {"message": "Telephony configuration saved successfully"}

View file

@ -5,7 +5,6 @@ from typing import Annotated, Optional
from fastapi import APIRouter, Depends, Form, Header, HTTPException, Request, WebSocket
from loguru import logger
from pipecat.utils.context import set_current_run_id
from pydantic import BaseModel
from starlette.responses import HTMLResponse
@ -19,6 +18,7 @@ from api.services.campaign.campaign_event_publisher import (
)
from api.services.pipecat.run_pipeline import run_pipeline_twilio
from api.services.telephony.twilio import TwilioService
from pipecat.utils.context import set_current_run_id
router = APIRouter(prefix="/twilio")
@ -45,20 +45,16 @@ class TwilioStatusCallbackRequest(BaseModel):
async def initiate_call(
request: InitiateCallRequest, user: UserModel = Depends(get_user)
):
# Check if organization has TWILIO_PHONE_NUMBERS configured
# Check if organization has TWILIO_CONFIGURATION configured
twilio_config = await db_client.get_configuration(
user.selected_organization_id,
OrganizationConfigurationKey.TWILIO_PHONE_NUMBERS.value,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value,
)
if (
not twilio_config
or not twilio_config.value
or not twilio_config.value.get("value")
):
if not twilio_config or not twilio_config.value:
raise HTTPException(
status_code=401,
detail="Your organisation is not allowed to make phone call. Contact founders@dograh.com for further support.",
status_code=400,
detail="telephony_not_configured", # Special error code
)
user_configuration = await db_client.get_user_configurations(user.id)
@ -84,15 +80,16 @@ async def initiate_call(
workflow_run_name = workflow_run.name
if user_configuration.test_phone_number:
await TwilioService().initiate_call(
twilio_service = TwilioService(user.selected_organization_id)
await twilio_service.initiate_call(
to_number=user_configuration.test_phone_number,
url_args={
"workflow_id": request.workflow_id,
"user_id": user.id,
"workflow_run_id": workflow_run_id,
"organization_id": user.selected_organization_id,
},
workflow_run_id=workflow_run_id,
organization_id=user.selected_organization_id,
)
return {
"message": f"Call initiated successfully with run name {workflow_run_name}"
@ -102,8 +99,10 @@ async def initiate_call(
@router.post("/twiml", include_in_schema=False)
async def start_call(workflow_id: int, user_id: int, workflow_run_id: int):
twiml_content = await TwilioService().get_start_call_twiml(
async def start_call(
workflow_id: int, user_id: int, workflow_run_id: int, organization_id: int
):
twiml_content = await TwilioService(organization_id).get_start_call_twiml(
workflow_id, user_id, workflow_run_id
)
return HTMLResponse(content=twiml_content, media_type="application/xml")