147 lines
7.1 KiB
Python
147 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
import os
|
|
import json
|
|
from flask import Flask, request, jsonify, Response
|
|
import logging
|
|
import sys
|
|
from dotenv import load_dotenv # Import load_dotenv
|
|
|
|
# --- Configuration Loading ---
|
|
# Load environment variables from from $HOME/.env and ./.env files
|
|
# $HOME/.env allows for user-specific settings, ./.env for project-specific settings.
|
|
# Values in ./.env might override $HOME/.env if load_dotenv is called again without override=False
|
|
# or if the variable exists in both and override=True is used on the second call.
|
|
# Current approach: Load $HOME/.env first, then load ./.env allowing ./.env to add or override.
|
|
logging.info("Loading environment variables...")
|
|
home_dotenv_path = os.path.join(os.environ.get("HOME", "."), '.env')
|
|
project_dotenv_path = os.path.join(os.getcwd(), '.env')
|
|
|
|
if os.path.exists(home_dotenv_path):
|
|
logging.info(f"Loading environment variables from: {home_dotenv_path}")
|
|
load_dotenv(dotenv_path=home_dotenv_path)
|
|
else:
|
|
logging.info(f"Optional environment file not found: {home_dotenv_path}")
|
|
|
|
if os.path.exists(project_dotenv_path):
|
|
logging.info(f"Loading environment variables from: {project_dotenv_path}")
|
|
# load_dotenv will not override existing variables by default.
|
|
# Use override=True if you want ./.env to forcefully replace variables from $HOME/.env
|
|
load_dotenv(dotenv_path=project_dotenv_path, override=True)
|
|
else:
|
|
logging.info(f"Project environment file not found: {project_dotenv_path}")
|
|
|
|
|
|
# --- Configuration ---
|
|
# Now loaded from environment variables defined in the .env files
|
|
TELNYX_API_KEY = os.environ.get("TELNYX_API_KEY") # Used for making API calls (e.g., sending messages)
|
|
TELNYX_SENDER_ID = os.environ.get("TELNYX_SENDER_ID") # Default 'from' number if sending messages via API
|
|
TELNYX_CONNECTION_ID = os.environ.get("TELNYX_CONNECTION_ID") # Used for Voice/TTS Calls via API
|
|
TELNYX_MESSAGING_PROFILE_ID = os.environ.get("TELNYX_MESSAGING_PROFILE_ID") # Associated messaging profile
|
|
TELNYX_PUBLIC_KEY = os.environ.get("TELNYX_PUBLIC_KEY") # REQUIRED if enabling signature validation
|
|
|
|
# --- Other Configurations ---
|
|
# Keyword can also be moved to .env if preferred
|
|
OPT_IN_KEYWORD = os.environ.get("OPT_IN_KEYWORD", "WELLNUOJOIN") # Load from env or default
|
|
|
|
# Configure logging (after potentially loading log level from env)
|
|
LOG_LEVEL = os.environ.get("LOG_LEVEL", "INFO").upper()
|
|
logging.basicConfig(level=LOG_LEVEL, format='%(asctime)s - %(levelname)s - %(message)s', stream=sys.stdout) # Log to stdout for containers/systemd
|
|
|
|
|
|
# --- Verification ---
|
|
if not TELNYX_PUBLIC_KEY:
|
|
logging.warning("TELNYX_PUBLIC_KEY is not set in environment variables. Signature validation will be skipped.")
|
|
# Add checks for other essential variables if needed for specific functions
|
|
|
|
app = Flask(__name__)
|
|
|
|
# --- Database Interaction Placeholder ---
|
|
# (Keep your existing database functions here)
|
|
def add_opted_in_number(phone_number):
|
|
logging.info(f"Adding phone number to opt-in list: {phone_number}")
|
|
print(f"DATABASE STUB: Adding {phone_number} to opt-ins.")
|
|
# Replace with your actual DB logic
|
|
return True
|
|
|
|
def is_opted_in(phone_number):
|
|
logging.info(f"Checking opt-in status for: {phone_number}")
|
|
print(f"DATABASE STUB: Checking if {phone_number} is opted in.")
|
|
# Replace with your actual DB logic
|
|
return False
|
|
# --------------------------------------
|
|
|
|
@app.route('/webhook', methods=['POST'])
|
|
def telnyx_webhook():
|
|
"""Handles incoming webhooks from Telnyx."""
|
|
logging.debug("Webhook received") # Use debug level for noisy logs
|
|
|
|
# --- Telnyx Signature Validation (Using loaded TELNYX_PUBLIC_KEY) ---
|
|
# UNCOMMENT this section if you have set TELNYX_PUBLIC_KEY in your .env file
|
|
# and installed the 'telnyx' library.
|
|
# ---
|
|
# if not TELNYX_PUBLIC_KEY:
|
|
# logging.warning("Skipping signature validation: TELNYX_PUBLIC_KEY not configured.")
|
|
# else:
|
|
# signature = request.headers.get("Telnyx-Signature-Ed25519")
|
|
# timestamp = request.headers.get("Telnyx-Timestamp")
|
|
# raw_payload = request.data # Get raw bytes
|
|
# if not signature or not timestamp:
|
|
# logging.warning("Missing Telnyx signature headers")
|
|
# return jsonify({"error": "Missing signature headers"}), 400
|
|
# try:
|
|
# # Requires 'telnyx' library: pip install telnyx
|
|
# from telnyx.webhook import Webhook
|
|
# Webhook.construct_event(raw_payload, signature, timestamp, TELNYX_PUBLIC_KEY, tolerance=600) # 10 min tolerance
|
|
# logging.info("Telnyx signature validated successfully.")
|
|
# except Exception as e:
|
|
# logging.error(f"Invalid Telnyx signature: {e}")
|
|
# return jsonify({"error": "Invalid signature"}), 401
|
|
# --- End Signature Validation Section ---
|
|
|
|
|
|
try:
|
|
payload = request.json
|
|
# Log the full payload only at DEBUG level to avoid excessive logging
|
|
logging.debug(f"Payload received: {json.dumps(payload, indent=2)}")
|
|
|
|
event_type = payload.get('data', {}).get('event_type')
|
|
record_type = payload.get('data', {}).get('record_type')
|
|
|
|
if event_type == 'message.received' and record_type == 'message':
|
|
message_payload = payload.get('data', {}).get('payload', {})
|
|
direction = message_payload.get('direction')
|
|
|
|
if direction == 'inbound':
|
|
from_number = message_payload.get('from', {}).get('phone_number')
|
|
# Use the OPT_IN_KEYWORD loaded from env or default
|
|
message_body = message_payload.get('text', '').strip().upper()
|
|
|
|
logging.info(f"Received inbound message from {from_number}: '{message_body[:50]}'...") # Log truncated message
|
|
|
|
if message_body == OPT_IN_KEYWORD:
|
|
logging.info(f"Opt-in keyword '{OPT_IN_KEYWORD}' detected from {from_number}")
|
|
if not is_opted_in(from_number):
|
|
if add_opted_in_number(from_number):
|
|
logging.info(f"Successfully processed opt-in for {from_number}")
|
|
else:
|
|
logging.error(f"Failed to add {from_number} to opt-in database.")
|
|
else:
|
|
logging.info(f"{from_number} is already opted in.")
|
|
else:
|
|
logging.info(f"Message from {from_number} did not match opt-in keyword.")
|
|
else:
|
|
logging.debug(f"Ignoring non-inbound message: direction={direction}") # Use debug for less important ignores
|
|
else:
|
|
logging.debug(f"Ignoring non-message.received event: type={event_type}, record={record_type}") # Use debug
|
|
|
|
# Acknowledge receipt to Telnyx
|
|
return Response(status=204)
|
|
|
|
except Exception as e:
|
|
logging.exception("Error processing webhook") # Logs the full exception traceback
|
|
return jsonify({"error": "Internal server error"}), 500
|
|
|
|
if __name__ == '__main__':
|
|
logging.info("Starting Flask development server for local testing...")
|
|
# Bind to 0.0.0.0 to be accessible externally if needed for local testing with ngrok/similar
|
|
app.run(host='0.0.0.0', port=8002, debug=True) # Debug=True is NOT for production |