mirror of
https://github.com/dograh-hq/dograh.git
synced 2026-07-04 10:52:17 +02:00
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:
parent
d39a8111a6
commit
8e2e5c9327
21 changed files with 891 additions and 191 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue