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

@ -7,72 +7,65 @@ from loguru import logger
from pydantic import ValidationError
from twilio.request_validator import RequestValidator
from api.constants import (
BACKEND_API_ENDPOINT,
TWILIO_ACCOUNT_SID,
TWILIO_AUTH_TOKEN,
TWILIO_DEFAULT_FROM_NUMBER,
)
from api.db import db_client
from api.enums import OrganizationConfigurationKey
from api.utils.tunnel import TunnelURLProvider
class TwilioService:
"""Service for interacting with Twilio API."""
def __init__(self):
if (
not TWILIO_DEFAULT_FROM_NUMBER
or not TWILIO_ACCOUNT_SID
or not TWILIO_AUTH_TOKEN
):
def __init__(self, organization_id: int):
"""Initialize TwilioService with organization_id."""
self.organization_id = organization_id
self.account_sid = None
self.auth_token = None
self.from_numbers = []
self.base_url = None
async def _ensure_credentials(self):
"""Load credentials from organization configuration."""
if self.account_sid and self.auth_token:
return
# Fetch from organization config only - no env var fallback
config = await db_client.get_configuration(
self.organization_id,
OrganizationConfigurationKey.TWILIO_CONFIGURATION.value,
)
if not config or not config.value:
raise ValidationError(
"Please set TWILIO_DEFAULT_FROM_NUMBER, TWILIO_ACCOUNT_SID, and TWILIO_AUTH_TOKEN environment"
"variables to use TwilioService"
"Twilio credentials not configured for this organization. "
"Please configure telephony settings."
)
self.account_sid = TWILIO_ACCOUNT_SID
self.auth_token = TWILIO_AUTH_TOKEN
self.default_from_number = TWILIO_DEFAULT_FROM_NUMBER
self.account_sid = config.value.get("account_sid")
self.auth_token = config.value.get("auth_token")
self.from_numbers = config.value.get("from_numbers", [])
if not self.account_sid or not self.auth_token or not self.from_numbers:
raise ValidationError(
"Incomplete Twilio configuration. Please update telephony settings."
)
self.base_url = f"https://api.twilio.com/2010-04-01/Accounts/{self.account_sid}"
async def get_organization_phone_numbers(self, organization_id: int) -> List[str]:
async def get_organization_phone_numbers(self) -> List[str]:
"""
Get the list of Twilio phone numbers configured for an organization.
Args:
organization_id: The organization ID
Get the list of Twilio phone numbers configured for the organization.
Returns:
List of phone numbers, or default if none configured
List of phone numbers
"""
try:
from api.enums import OrganizationConfigurationKey
config = await db_client.get_configuration(
organization_id,
OrganizationConfigurationKey.TWILIO_PHONE_NUMBERS.value,
)
if config and config.value:
# Expect the value to be a list of phone numbers
phone_numbers = config.value.get("value", [])
if isinstance(phone_numbers, list) and phone_numbers:
return phone_numbers
except Exception as e:
logger.warning(
f"Error getting phone numbers for org {organization_id}: {e}"
)
# Fall back to default from environment
return [self.default_from_number]
await self._ensure_credentials()
return self.from_numbers
async def initiate_call(
self,
to_number: str,
url_args: Dict[str, Any] = {},
workflow_run_id: Optional[int] = None,
organization_id: Optional[int] = None,
**kwargs: Any,
) -> Dict[str, Any]:
"""
@ -82,21 +75,20 @@ class TwilioService:
to_number: The destination phone number
url_args: Dictionary of URL parameters to append to the base URL
workflow_run_id: The workflow run ID for tracking callbacks
organization_id: The organization ID for selecting phone numbers
**kwargs: Additional parameters to pass to the Twilio API
Returns:
Dict containing the Twilio API response
"""
await self._ensure_credentials()
endpoint = f"{self.base_url}/Calls.json"
if not BACKEND_API_ENDPOINT:
raise ValidationError(
"Please set BACKEND_API_ENDPOINT environment variable to a tunnel or persistant URL"
)
# Get tunnel URL at runtime
backend_endpoint = await TunnelURLProvider.get_tunnel_url()
# Construct the URL with parameters if any
url: str = f"https://{BACKEND_API_ENDPOINT}/api/v1/twilio/twiml"
url: str = f"https://{backend_endpoint}/api/v1/twilio/twiml"
if url_args:
query_string = urlencode(url_args)
url = f"{url}?{query_string}"
@ -104,27 +96,19 @@ class TwilioService:
logger.debug(f"Initiating call with URL: {url}")
# Get phone numbers for organization and select one randomly
if organization_id:
phone_numbers = await self.get_organization_phone_numbers(organization_id)
from_number = random.choice(phone_numbers)
logger.info(
f"Selected phone number {from_number} from {len(phone_numbers)} "
f"available numbers for org {organization_id}"
)
else:
from_number = self.default_from_number
phone_numbers = await self.get_organization_phone_numbers()
from_number = random.choice(phone_numbers)
logger.info(
f"Selected phone number {from_number} from {len(phone_numbers)} "
f"available numbers for org {self.organization_id}"
)
# Prepare call data
data = {"To": to_number, "From": from_number, "Url": url}
if not BACKEND_API_ENDPOINT:
raise ValidationError(
"Please set BACKEND_API_ENDPOINT environment variable to a tunnel or persistant URL"
)
# Add status callback configuration if workflow_run_id is provided
if workflow_run_id:
callback_url = f"https://{BACKEND_API_ENDPOINT}/api/v1/twilio/status-callback/{workflow_run_id}"
callback_url = f"https://{backend_endpoint}/api/v1/twilio/status-callback/{workflow_run_id}"
data.update(
{
"StatusCallback": callback_url,
@ -154,15 +138,13 @@ class TwilioService:
async def get_start_call_twiml(
self, workflow_id: int, user_id: int, workflow_run_id: int
) -> str:
if not BACKEND_API_ENDPOINT:
raise ValidationError(
"Please set BACKEND_API_ENDPOINT environment variable to a tunnel or persistant URL"
)
# Get tunnel URL at runtime
backend_endpoint = await TunnelURLProvider.get_tunnel_url()
twiml_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://{BACKEND_API_ENDPOINT}/api/v1/twilio/ws/{workflow_id}/{user_id}/{workflow_run_id}"></Stream>
<Stream url="wss://{backend_endpoint}/api/v1/twilio/ws/{workflow_id}/{user_id}/{workflow_run_id}"></Stream>
</Connect>
<Pause length="40"/>
</Response>"""
@ -178,6 +160,8 @@ class TwilioService:
Returns:
Dict containing the call information
"""
await self._ensure_credentials()
endpoint = f"{self.base_url}/Calls/{call_sid}.json"
async with aiohttp.ClientSession() as session:
@ -189,7 +173,7 @@ class TwilioService:
return await response.json()
def verify_signature(
async def verify_signature(
self, url: str, params: Dict[str, Any], signature: str
) -> bool:
"""
@ -203,5 +187,7 @@ class TwilioService:
Returns:
bool: True if signature is valid, False otherwise
"""
await self._ensure_credentials()
validator = RequestValidator(self.auth_token)
return validator.validate(url, params, signature)