#!/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