Webhook changes (Branko?)
This commit is contained in:
parent
66d789436b
commit
cb57ae8d7b
415
webhook-http1998.py
Normal file
415
webhook-http1998.py
Normal file
@ -0,0 +1,415 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
import re
|
||||
import base64
|
||||
from flask import Flask, request, Response, jsonify
|
||||
import logging
|
||||
import logging.handlers
|
||||
import sys
|
||||
import argparse
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# --- Configuration Loading ---
|
||||
def load_env_file(filepath):
|
||||
"""Load environment variables from file with shell-style export syntax support"""
|
||||
if not os.path.exists(filepath):
|
||||
return False
|
||||
|
||||
print(f"INFO: Found environment file: {filepath}")
|
||||
|
||||
# Check if it's a standard .env file first (let dotenv handle it)
|
||||
if filepath.endswith('.env'):
|
||||
print(f"INFO: Loading environment variables from .env file")
|
||||
load_dotenv(dotenv_path=filepath)
|
||||
return True
|
||||
|
||||
# Handle shell script style env file with 'export KEY=VALUE' format
|
||||
try:
|
||||
with open(filepath, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
# Skip comments and empty lines
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
# Parse export KEY=VALUE format
|
||||
export_match = re.match(r'^export\s+([A-Za-z0-9_]+)=(?:"(.+?)"|\'(.+?)\'|(.*))$', line)
|
||||
if export_match:
|
||||
key = export_match.group(1)
|
||||
# Get the first non-None value from the captured groups
|
||||
value = next((g for g in export_match.groups()[1:] if g is not None), '')
|
||||
|
||||
# Only set if not already set in environment
|
||||
if key not in os.environ:
|
||||
os.environ[key] = value
|
||||
print(f"INFO: Loaded {key}={value[:3]}{'*'*(len(value)-6)}{value[-3:] if len(value) > 6 else ''}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"ERROR: Failed to load environment file {filepath}: {e}")
|
||||
return False
|
||||
|
||||
# Try to load from .env or env file in the current directory
|
||||
env_loaded = False
|
||||
dotenv_path = os.path.join(os.getcwd(), '.env')
|
||||
alt_env_path = os.path.join(os.getcwd(), 'env')
|
||||
|
||||
if os.path.exists(dotenv_path):
|
||||
env_loaded = load_env_file(dotenv_path)
|
||||
if not env_loaded and os.path.exists(alt_env_path):
|
||||
env_loaded = load_env_file(alt_env_path)
|
||||
|
||||
if not env_loaded:
|
||||
print("INFO: No .env or env file found, using defaults or provided arguments.")
|
||||
|
||||
# --- Global Configuration ---
|
||||
LOG_LEVEL_DEFAULT = os.environ.get("LOG_LEVEL", "INFO").upper()
|
||||
LOG_FILE_NAME_DEFAULT = os.environ.get("LOG_FILE_PATH", "webhook.log")
|
||||
OPT_IN_KEYWORD_DEFAULT = os.environ.get("OPT_IN_KEYWORD", "WELLNUOJOIN").upper()
|
||||
DEFAULT_TTS_VOICE_DEFAULT = os.environ.get("TELNYX_DEFAULT_TTS_VOICE", "female")
|
||||
DEFAULT_TTS_LANGUAGE_DEFAULT = os.environ.get("TELNYX_DEFAULT_TTS_LANGUAGE", "en-US")
|
||||
CLIENT_STATE_PREFIX_DEFAULT = os.environ.get("TELNYX_CLIENT_STATE_PREFIX", "app_state")
|
||||
PLAY_AUDIO_ON_ANSWER_DEFAULT = os.environ.get("PLAY_AUDIO_ON_ANSWER", "false").lower() == "true"
|
||||
TELNYX_API_KEY_DEFAULT = os.environ.get("TELNYX_API_KEY", "")
|
||||
TELNYX_API_BASE_URL = os.environ.get("TELNYX_API_BASE_URL", "https://api.telnyx.com/v2")
|
||||
|
||||
# Print key configuration for debugging at startup
|
||||
print(f"INFO: TELNYX_API_KEY is {'set' if TELNYX_API_KEY_DEFAULT else 'NOT SET'}")
|
||||
if TELNYX_API_KEY_DEFAULT:
|
||||
masked_key = TELNYX_API_KEY_DEFAULT[:8] + '*'*(len(TELNYX_API_KEY_DEFAULT)-16) + TELNYX_API_KEY_DEFAULT[-8:] if len(TELNYX_API_KEY_DEFAULT) > 16 else "***"
|
||||
print(f"INFO: TELNYX_API_KEY value: {masked_key}")
|
||||
|
||||
# --- Application Setup ---
|
||||
app = Flask(__name__)
|
||||
# Initialize app_logger; it will be configured in setup_logging
|
||||
app_logger = logging.getLogger("TelnyxWebhookApp")
|
||||
|
||||
|
||||
# --- Helper Functions ---
|
||||
def setup_logging(log_level_str, log_file_path, app_name="TelnyxWebhookApp"):
|
||||
global app_logger # Ensure we are configuring the global app_logger
|
||||
app_logger = logging.getLogger(app_name)
|
||||
|
||||
numeric_level = getattr(logging, log_level_str.upper(), logging.INFO)
|
||||
if not isinstance(numeric_level, int): # Should not happen with getattr default
|
||||
print(f"ERROR: Invalid log level string: {log_level_str}. Defaulting to INFO.")
|
||||
numeric_level = logging.INFO
|
||||
|
||||
app_logger.setLevel(numeric_level)
|
||||
app_logger.propagate = False # Important to prevent duplicate logs if root is also configured
|
||||
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - [%(funcName)s:%(lineno)d] - %(message)s')
|
||||
|
||||
# Clear existing handlers from app_logger to prevent duplication on script re-run/re-import
|
||||
if app_logger.hasHandlers():
|
||||
app_logger.handlers.clear()
|
||||
|
||||
# Console Handler
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setFormatter(formatter)
|
||||
app_logger.addHandler(console_handler)
|
||||
|
||||
# File Handler
|
||||
try:
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
log_file_path, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8'
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
app_logger.addHandler(file_handler)
|
||||
# Use app_logger for this initial message AFTER handlers are attached
|
||||
app_logger.info(f"Application logging configured. Level: {log_level_str}. File: {os.path.abspath(log_file_path)}")
|
||||
except Exception as e:
|
||||
# Fallback to print if logger itself fails during setup for file handler
|
||||
print(f"ERROR: Failed to configure file logger at {log_file_path}: {e}")
|
||||
if app_logger.handlers: # If console handler was added, use it
|
||||
app_logger.error(f"Failed to configure file logger at {log_file_path}: {e}")
|
||||
|
||||
|
||||
def find_custom_header(headers, name):
|
||||
if not headers: return None
|
||||
for header in headers:
|
||||
if header.get('name', '').lower() == name.lower():
|
||||
return header.get('value')
|
||||
return None
|
||||
|
||||
def create_client_state(base_event, call_control_id, prefix):
|
||||
"""Create a base64 encoded client state string as required by Telnyx API"""
|
||||
# Create the plain text client state string
|
||||
plain_state = f"{prefix}_{base_event}_{call_control_id[:8]}" if call_control_id else f"{prefix}_{base_event}_unknownccid"
|
||||
|
||||
# Encode to base64 as required by Telnyx API
|
||||
# First encode to bytes, then encode to base64, then decode to ASCII string
|
||||
base64_state = base64.b64encode(plain_state.encode('utf-8')).decode('ascii')
|
||||
|
||||
app_logger.debug(f"Client state created: '{plain_state}' -> base64: '{base64_state}'")
|
||||
return base64_state
|
||||
|
||||
def send_telnyx_command(command_type, params, api_key):
|
||||
"""
|
||||
Send command to Telnyx Call Control API
|
||||
|
||||
Args:
|
||||
command_type (str): Type of command (speak, play, hangup, etc)
|
||||
params (dict): Command parameters
|
||||
api_key (str): Telnyx API key
|
||||
|
||||
Returns:
|
||||
dict: API response as JSON or None on error
|
||||
"""
|
||||
if not api_key:
|
||||
app_logger.error(f"Cannot send {command_type} command: TELNYX_API_KEY not set")
|
||||
return None
|
||||
|
||||
endpoint = f"{TELNYX_API_BASE_URL}/calls/{params.get('call_control_id')}/{command_type}"
|
||||
|
||||
# Remove call_control_id from params as it's in the URL
|
||||
api_params = params.copy()
|
||||
if 'call_control_id' in api_params:
|
||||
del api_params['call_control_id']
|
||||
|
||||
# Ensure client_state is present and base64 encoded (handled by create_client_state function)
|
||||
if 'client_state' in api_params and not api_params['client_state']:
|
||||
del api_params['client_state'] # Remove if empty
|
||||
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
app_logger.debug(f"Sending {command_type} command to Telnyx API: {json.dumps(api_params)}")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
endpoint,
|
||||
json=api_params,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code >= 200 and response.status_code < 300:
|
||||
result = response.json()
|
||||
app_logger.info(f"Telnyx accepted {command_type} command. Response: {response.status_code}")
|
||||
app_logger.debug(f"Full API response: {json.dumps(result)}")
|
||||
return result
|
||||
else:
|
||||
app_logger.error(f"Telnyx rejected {command_type} command. Status: {response.status_code}")
|
||||
app_logger.error(f"Response body: {response.text}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
app_logger.exception(f"Error sending {command_type} command to Telnyx API")
|
||||
return None
|
||||
|
||||
# --- Webhook Route Handler ---
|
||||
@app.route('/<path:webhook_path_received>', methods=['POST'])
|
||||
def handle_telnyx_webhook(webhook_path_received):
|
||||
# Access app_args via app.config for a cleaner way if set during app init,
|
||||
# or pass it via other means. For simplicity with __main__, global is used.
|
||||
# Make sure app_args is accessible here if not using global.
|
||||
# For now, assuming app_args is available as configured in __main__
|
||||
if f"/{webhook_path_received}" != app_args.webhook_path: # app_args is from main
|
||||
app_logger.warning(f"Request to unknown path '/{webhook_path_received}'. Configured for '{app_args.webhook_path}'")
|
||||
return "Path not found", 404
|
||||
|
||||
remote_addr = request.remote_addr
|
||||
request_id = request.headers.get("X-Request-Id") or request.headers.get("Telnyx-Request-Id") or "N/A"
|
||||
# EXPLICITLY use app_logger for all subsequent logging
|
||||
app_logger.info(f"Webhook request received. Path: '/{webhook_path_received}', From: {remote_addr}, X-Telnyx-Request-ID: {request_id}")
|
||||
|
||||
try:
|
||||
if not request.is_json:
|
||||
raw_data_preview = request.get_data(as_text=True)[:250]
|
||||
app_logger.warning(f"Non-JSON request. Content-Type: {request.content_type}. Data Preview: '{raw_data_preview}...'")
|
||||
return "Invalid Content-Type: Expected application/json", 415
|
||||
|
||||
webhook_data = request.get_json()
|
||||
app_logger.debug(f"Full incoming webhook payload: {json.dumps(webhook_data, indent=2)}")
|
||||
|
||||
data = webhook_data.get('data', {})
|
||||
event_type = data.get('event_type')
|
||||
record_type = data.get('record_type')
|
||||
payload = data.get('payload', {})
|
||||
call_control_id = payload.get('call_control_id')
|
||||
call_session_id = payload.get('call_session_id')
|
||||
message_id = payload.get('id') if record_type == 'message' else None
|
||||
client_state_on_event = payload.get("client_state")
|
||||
|
||||
log_prefix = f"Event: '{event_type}' ({record_type})"
|
||||
if call_control_id: log_prefix += f", CCID: {call_control_id}"
|
||||
if call_session_id: log_prefix += f", CSID: {call_session_id}"
|
||||
if message_id: log_prefix += f", MsgID: {message_id}"
|
||||
if client_state_on_event: log_prefix += f", ClientStateRcvd: {client_state_on_event}"
|
||||
app_logger.info(log_prefix) # Main event identifier log
|
||||
|
||||
command_response = None
|
||||
|
||||
# --- SMS Event Handling ---
|
||||
if record_type == 'message':
|
||||
direction = payload.get('direction')
|
||||
messaging_profile_id = payload.get("messaging_profile_id")
|
||||
from_details = payload.get('from', {})
|
||||
from_number = from_details.get('phone_number')
|
||||
to_list = payload.get('to', [])
|
||||
to_number = to_list[0].get('phone_number') if to_list else 'N/A'
|
||||
text = payload.get('text', '')
|
||||
num_media = len(payload.get('media', []))
|
||||
tags = payload.get('tags', [])
|
||||
cost = payload.get('cost') # Might be None
|
||||
|
||||
# Log detailed SMS payload information
|
||||
sms_details = (
|
||||
f" SMS Details: Direction='{direction}', From='{from_number}', To='{to_number}', "
|
||||
f"Profile='{messaging_profile_id}', MediaCount={num_media}, Tags={tags}"
|
||||
)
|
||||
if cost: sms_details += f", Cost={cost.get('amount')} {cost.get('currency')}"
|
||||
app_logger.info(sms_details)
|
||||
if text: app_logger.info(f" Text: '{text.strip()}'")
|
||||
|
||||
|
||||
if event_type == 'message.received' and direction == 'inbound':
|
||||
app_logger.info(f" -> Inbound SMS received.") # More specific than just "SMS RECEIVED"
|
||||
if text.strip().upper() == app_args.opt_in_keyword:
|
||||
app_logger.info(f" -> Opt-in keyword '{app_args.opt_in_keyword}' detected from {from_number}.")
|
||||
elif event_type == 'message.sent':
|
||||
status = payload.get('status')
|
||||
errors = payload.get('errors', [])
|
||||
app_logger.info(f" -> SMS Sent (DLR): Status='{status}', ErrorsCount={len(errors)}")
|
||||
if errors: app_logger.warning(f" -> DLR Errors: {json.dumps(errors)}")
|
||||
else:
|
||||
app_logger.info(f" -> Other SMS Event: Type='{event_type}'")
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
# --- Voice Event Handling ---
|
||||
elif record_type == 'event':
|
||||
client_s_prefix = app_args.client_state_prefix
|
||||
from_number_call = payload.get('from') # 'from' in call events is usually the caller
|
||||
to_number_call = payload.get('to') # 'to' in call events is usually the callee
|
||||
|
||||
if event_type == 'call.initiated':
|
||||
state = payload.get('state')
|
||||
direction_call = payload.get('direction') # 'incoming' or 'outgoing'
|
||||
app_logger.info(f" CALL INITIATED: State='{state}', Direction='{direction_call}', From='{from_number_call}', To='{to_number_call}'")
|
||||
elif event_type == 'call.answered':
|
||||
custom_headers = payload.get('custom_headers', [])
|
||||
app_logger.info(f" CALL ANSWERED: From='{from_number_call}', To='{to_number_call}'")
|
||||
audio_url = find_custom_header(custom_headers, 'X-Audio-Url')
|
||||
tts_payload = find_custom_header(custom_headers, 'X-TTS-Payload')
|
||||
client_s = create_client_state("answered", call_control_id, client_s_prefix)
|
||||
|
||||
if app_args.enable_audio_playback and audio_url:
|
||||
app_logger.info(f" -> Audio URL '{audio_url}' found. Sending playback_start command.")
|
||||
play_params = {
|
||||
"call_control_id": call_control_id,
|
||||
"client_state": client_s,
|
||||
"audio_url": audio_url
|
||||
}
|
||||
send_telnyx_command("actions/playback_start", play_params, app_args.api_key)
|
||||
elif tts_payload:
|
||||
app_logger.info(f" -> TTS payload '{tts_payload}' found. Sending speak command.")
|
||||
speak_params = {
|
||||
"payload": tts_payload,
|
||||
"voice": app_args.default_tts_voice,
|
||||
"language": app_args.default_tts_language,
|
||||
"call_control_id": call_control_id,
|
||||
"client_state": client_s
|
||||
}
|
||||
send_telnyx_command("actions/speak", speak_params, app_args.api_key)
|
||||
else:
|
||||
app_logger.warning(" -> Call answered, but no 'X-Audio-Url' or 'X-TTS-Payload' found/enabled. Issuing hangup.")
|
||||
hangup_params = {
|
||||
"call_control_id": call_control_id,
|
||||
"client_state": create_client_state("nohdr_hup", call_control_id, client_s_prefix)
|
||||
}
|
||||
send_telnyx_command("actions/hangup", hangup_params, app_args.api_key)
|
||||
elif event_type == 'call.speak.started':
|
||||
app_logger.info(f" CALL SPEAK STARTED") # CCID already logged in prefix
|
||||
elif event_type == 'call.playback.started':
|
||||
media_url = payload.get('media_url', 'N/A')
|
||||
app_logger.info(f" CALL PLAYBACK STARTED: MediaURL='{media_url}'")
|
||||
elif event_type in ['call.speak.ended', 'call.playback.ended']:
|
||||
status = payload.get('status')
|
||||
ended_event_type = event_type.split('.')[-2]
|
||||
app_logger.info(f" CALL {ended_event_type.upper()} ENDED: Status='{status}'. Issuing hangup.")
|
||||
hangup_params = {
|
||||
"call_control_id": call_control_id,
|
||||
"client_state": create_client_state(f"{ended_event_type}_hup", call_control_id, client_s_prefix)
|
||||
}
|
||||
send_telnyx_command("actions/hangup", hangup_params, app_args.api_key)
|
||||
elif event_type == 'call.dtmf.received':
|
||||
digit = payload.get('digit')
|
||||
dtmf_from = payload.get('from')
|
||||
dtmf_to = payload.get('to')
|
||||
app_logger.info(f" DTMF RECEIVED: Digit='{digit}', FromLeg='{dtmf_from}', ToLeg='{dtmf_to}'")
|
||||
elif event_type == 'call.hangup':
|
||||
cause = payload.get('cause')
|
||||
sip_code = payload.get('sip_hangup_cause')
|
||||
hangup_source = payload.get('hangup_source') # 'local' or 'remote'
|
||||
app_logger.info(f" CALL HANGUP: Cause='{cause}', SIPCause='{sip_code}', Source='{hangup_source}'")
|
||||
else:
|
||||
app_logger.warning(f" UNHANDLED VOICE EVENT: Type='{event_type}'. Specific payload details:")
|
||||
app_logger.info(f" {json.dumps(payload, indent=4)}")
|
||||
else:
|
||||
app_logger.warning(f"UNKNOWN RECORD TYPE: '{record_type}'. Full webhook data:")
|
||||
app_logger.info(f" {json.dumps(webhook_data, indent=4)}")
|
||||
|
||||
# Always return 204 No Content to acknowledge webhook receipt
|
||||
return Response(status=204)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
app_logger.error(f"Failed to decode JSON from request body: {e}")
|
||||
return "Invalid JSON payload", 400
|
||||
except Exception as e:
|
||||
app_logger.exception("Unhandled error processing webhook")
|
||||
return "Internal Server Error", 500
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def handle_root():
|
||||
app_logger.info(f"Request to root path '/' from {request.remote_addr}. Method: {request.method}")
|
||||
return "Telnyx Webhook Listener (v2) is active.", 200
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description="Telnyx Webhook Listener v2 with detailed logging.", formatter_class=argparse.RawTextHelpFormatter)
|
||||
parser.add_argument('--host', type=str, default='0.0.0.0', help='Host address (default: 0.0.0.0).')
|
||||
parser.add_argument('--port', type=int, default=1998, help='Port (default: 1998).')
|
||||
parser.add_argument('--webhook-path', type=str, default='/telnyx-webhook', help="URL path for webhooks (default: /telnyx-webhook). Must start with '/'.")
|
||||
parser.add_argument('--log-level', type=str, default=LOG_LEVEL_DEFAULT, choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'], help=f'Logging level (default: {LOG_LEVEL_DEFAULT}).')
|
||||
parser.add_argument('--log-file', type=str, default=LOG_FILE_NAME_DEFAULT, help=f'Path to log file (default: {LOG_FILE_NAME_DEFAULT}).')
|
||||
parser.add_argument('--debug', action='store_true', help='Run Flask in debug mode (sets log to DEBUG, reloader). NOT FOR PRODUCTION.')
|
||||
parser.add_argument('--opt-in-keyword', type=str, default=OPT_IN_KEYWORD_DEFAULT, help=f'SMS opt-in keyword (default: {OPT_IN_KEYWORD_DEFAULT}).')
|
||||
parser.add_argument('--default-tts-voice', type=str, default=DEFAULT_TTS_VOICE_DEFAULT, help=f'Default TTS voice (default: {DEFAULT_TTS_VOICE_DEFAULT}).')
|
||||
parser.add_argument('--default-tts-language', type=str, default=DEFAULT_TTS_LANGUAGE_DEFAULT, help=f'Default TTS language (default: {DEFAULT_TTS_LANGUAGE_DEFAULT}).')
|
||||
parser.add_argument('--client-state-prefix', type=str, default=CLIENT_STATE_PREFIX_DEFAULT, help=f'Prefix for client_state (default: {CLIENT_STATE_PREFIX_DEFAULT}).')
|
||||
parser.add_argument('--enable-audio-playback', action='store_true', default=PLAY_AUDIO_ON_ANSWER_DEFAULT, help=f'Prioritize X-Audio-Url (default: {PLAY_AUDIO_ON_ANSWER_DEFAULT}).')
|
||||
parser.add_argument('--api-key', type=str, default=TELNYX_API_KEY_DEFAULT, help='Telnyx API Key (default: from environment)')
|
||||
|
||||
app_args = parser.parse_args() # Define app_args globally for access in route handler
|
||||
|
||||
if not app_args.webhook_path.startswith('/'):
|
||||
print(f"ERROR: --webhook-path ('{app_args.webhook_path}') must start with a '/'. Prepending.")
|
||||
app_args.webhook_path = '/' + app_args.webhook_path
|
||||
app_args.opt_in_keyword = app_args.opt_in_keyword.upper()
|
||||
|
||||
log_level_to_use = "DEBUG" if app_args.debug else app_args.log_level.upper()
|
||||
setup_logging(log_level_to_use, app_args.log_file, app_name="TelnyxWebhookApp")
|
||||
# app_logger is now configured and can be used directly
|
||||
|
||||
# Disable Werkzeug's default access logger to reduce console noise if desired,
|
||||
# as we have our own request received log.
|
||||
# Or set its level higher if you still want it but less verbose.
|
||||
if not app_args.debug: # Keep Werkzeug logs for Flask debug mode
|
||||
werkzeug_logger = logging.getLogger('werkzeug')
|
||||
werkzeug_logger.setLevel(logging.WARNING) # Or ERROR to only see issues
|
||||
|
||||
app_logger.info("--- Telnyx Webhook Listener Starting (v2) ---")
|
||||
app_logger.info(f" Version: 1.2.0")
|
||||
app_logger.info(f" Configuration:")
|
||||
for arg, value in sorted(vars(app_args).items()):
|
||||
# Don't log API key value
|
||||
if arg == 'api_key':
|
||||
value = "***" if value else "Not set"
|
||||
app_logger.info(f" {arg}: {value}")
|
||||
app_logger.info(f" Log File (Absolute): {os.path.abspath(app_args.log_file)}")
|
||||
app_logger.info("-------------------------------------------")
|
||||
|
||||
app.run(host=app_args.host, port=app_args.port, debug=app_args.debug, use_reloader=app_args.debug)
|
||||
75
well-voicettscall.py
Normal file
75
well-voicettscall.py
Normal file
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import requests
|
||||
import json
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
|
||||
# --- Configuration ---
|
||||
TELNYX_API_KEY = os.environ.get("TELNYX_API_KEY") # Make sure this matches your KEY019...
|
||||
TELNYX_FROM_NUMBER = os.environ.get("TELNYX_FROM_NUMBER", "+16505820706")
|
||||
# This Connection ID MUST be associated with a Call Control Application
|
||||
# configured to send webhooks to your webhook server/path.
|
||||
TELNYX_CONNECTION_ID = os.environ.get("TELNYX_CONNECTION_ID", "2671409623596009055")
|
||||
TELNYX_API_URL = "https://api.telnyx.com/v2/calls"
|
||||
TELNYX_WEBHOOK_URL = "http://eluxnetworks.net:1998/telnyx-webhook" # Define your webhook URL
|
||||
|
||||
if not TELNYX_API_KEY:
|
||||
print("Error: TELNYX_API_KEY not found in environment variables or .env file.")
|
||||
exit(1)
|
||||
if not TELNYX_CONNECTION_ID:
|
||||
print("Error: TELNYX_CONNECTION_ID not found in environment variables or .env file.")
|
||||
exit(1)
|
||||
|
||||
# --- Get Call Details ---
|
||||
to_number = input("Enter the phone number to call (e.g., +14082397258): ")
|
||||
tts_message = input("Enter the message to speak after answer: ")
|
||||
|
||||
# --- Prepare API Request ---
|
||||
headers = {
|
||||
"Authorization": f"Bearer {TELNYX_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"to": to_number,
|
||||
"from": TELNYX_FROM_NUMBER,
|
||||
"connection_id": TELNYX_CONNECTION_ID,
|
||||
"custom_headers": [
|
||||
{
|
||||
"name": "X-TTS-Payload", # Custom header to pass the message
|
||||
"value": tts_message
|
||||
}
|
||||
# Add other custom headers if needed
|
||||
],
|
||||
"webhook_url": TELNYX_WEBHOOK_URL,
|
||||
"webhook_url_method": "POST"
|
||||
# You might need to specify the webhook URL here if the connection_id doesn't have one pre-configured
|
||||
# "webhook_url": "http://eluxnetworks.net:1998/", # Example - USE HTTPS!
|
||||
}
|
||||
|
||||
print("\nInitiating call...")
|
||||
print(f"Payload: {json.dumps(payload, indent=2)}")
|
||||
|
||||
# --- Make API Call ---
|
||||
try:
|
||||
response = requests.post(TELNYX_API_URL, headers=headers, json=payload)
|
||||
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
|
||||
|
||||
print("\nCall initiated successfully!")
|
||||
print("Response:")
|
||||
print(json.dumps(response.json(), indent=2))
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"\nError initiating call: {e}")
|
||||
if e.response is not None:
|
||||
print(f"Status Code: {e.response.status_code}")
|
||||
try:
|
||||
print(f"Response Body: {json.dumps(e.response.json(), indent=2)}")
|
||||
except json.JSONDecodeError:
|
||||
print(f"Response Body: {e.response.text}")
|
||||
except Exception as e:
|
||||
print(f"\nAn unexpected error occurred: {e}")
|
||||
Loading…
x
Reference in New Issue
Block a user