# TWILIO INTEGRATION MODULE FOR JAY'S MOBILE WASH # Handles Twilio API integration for calls and messages import os import json import logging import traceback from datetime import datetime from pathlib import Path # Try to import dotenv (it's in our requirements) try: from dotenv import load_dotenv load_dotenv() except ImportError: logging.warning("dotenv not available - environment variables must be set directly") # Configure logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger('twilio_integration') # Base paths BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATA_DIR = os.path.join(BASE_DIR, "data") CONFIG_DIR = os.path.join(BASE_DIR, "config") LOGS_DIR = os.path.join(BASE_DIR, "logs") # Create necessary directories for directory in [DATA_DIR, CONFIG_DIR, LOGS_DIR]: try: Path(directory).mkdir(parents=True, exist_ok=True) except Exception as e: logger.error(f"Error creating directory {directory}: {str(e)}") # Utility function to format phone numbers def format_phone(number): """Format a phone number to E.164 format""" # Remove all non-digit characters digits = ''.join(filter(str.isdigit, str(number))) # Handle US numbers if len(digits) == 10: return f"+1{digits}" elif len(digits) == 11 and digits[0] == '1': return f"+{digits}" else: # Just add + if not recognized format return f"+{digits}" # Generate ID utility def generate_id(prefix='id_'): """Generate a unique ID""" import random, time timestamp = int(time.time() * 1000) random_part = random.randint(1000, 9999) return f"{prefix}{timestamp}_{random_part}" # Try importing Twilio try: from twilio.rest import Client from twilio.twiml.voice_response import VoiceResponse, Say from twilio.twiml.messaging_response import MessagingResponse TWILIO_AVAILABLE = True logger.info("✅ Twilio library available") except ImportError: TWILIO_AVAILABLE = False logger.warning("⚠️ Twilio library not available! SMS/Call functionality will be limited to demo mode.") # Twilio credentials TWILIO_ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID') TWILIO_AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN') TWILIO_PHONE_NUMBER = os.getenv('TWILIO_PHONE_NUMBER') # Initialize client variable client = None # Check if Twilio is configured if TWILIO_AVAILABLE and TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN and TWILIO_PHONE_NUMBER: TWILIO_CONFIGURED = True try: client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) logger.info(f"✅ Connected to Twilio - Phone number: {TWILIO_PHONE_NUMBER}") except Exception as e: TWILIO_CONFIGURED = False logger.error(f"❌ Failed to connect to Twilio: {str(e)}") else: TWILIO_CONFIGURED = False if TWILIO_AVAILABLE: logger.warning("⚠️ Twilio credentials not set or incomplete. Using demo mode.") class TwilioManager: """Manages Twilio integration for calls and messages""" def __init__(self): """Initialize Twilio manager""" self.ready = TWILIO_CONFIGURED self.demo_mode = not self.ready # Default to demo mode if Twilio not configured self.client = client self.phone_number = TWILIO_PHONE_NUMBER # Create messages directory self.messages_dir = os.path.join(DATA_DIR, "messages") Path(self.messages_dir).mkdir(parents=True, exist_ok=True) logger.info(f"TwilioManager initialized. Demo mode: {self.demo_mode}") def set_demo_mode(self, value): """Set demo mode""" self.demo_mode = bool(value) logger.info(f"Twilio demo mode set to: {self.demo_mode}") def set_credentials(self, account_sid, auth_token, phone_number): """Set Twilio credentials""" global client, TWILIO_CONFIGURED if not TWILIO_AVAILABLE: logger.error("Cannot set credentials: Twilio library not available") return False, "Twilio library not available" try: # Save the credentials settings_file = os.path.join(CONFIG_DIR, "twilio_settings.json") with open(settings_file, 'w') as f: json.dump({ "account_sid": account_sid, "auth_token": "********", # Don't save actual token "phone_number": phone_number, "timestamp": datetime.now().isoformat() }, f, indent=2) # Try to create a client with new credentials test_client = Client(account_sid, auth_token) # If successful, update the main client self.client = test_client self.phone_number = phone_number self.ready = True TWILIO_CONFIGURED = True client = test_client logger.info(f"Twilio credentials updated successfully") return True, "Credentials saved and validated successfully" except Exception as e: logger.error(f"Failed to set Twilio credentials: {str(e)}") return False, f"Failed to set credentials: {str(e)}" def send_sms(self, to_number, message): """Send SMS message""" # Format the phone number to_number = format_phone(to_number) if self.demo_mode: # In demo mode, just log the message logger.info(f"[DEMO] SMS to {to_number}: {message}") message_id = generate_id('sms_') # Save message in history self._save_message({ "id": message_id, "to": to_number, "from": self.phone_number or "+15551234567", "body": message, "status": "delivered", "direction": "outbound", "timestamp": datetime.now().isoformat(), "demo": True }) return { "success": True, "id": message_id, "status": "delivered (demo)", "message": "Message sent in demo mode" } if not self.ready: error_msg = "Twilio not configured - cannot send real SMS" logger.error(error_msg) return { "success": False, "error": error_msg, "message": "Please configure Twilio settings first" } # Send the message using Twilio try: twilio_message = self.client.messages.create( body=message, from_=self.phone_number, to=to_number ) logger.info(f"SMS sent to {to_number}: {twilio_message.sid}") # Save message in history message_data = { "id": twilio_message.sid, "to": to_number, "from": self.phone_number, "body": message, "status": twilio_message.status, "direction": "outbound", "timestamp": datetime.now().isoformat(), "demo": False } self._save_message(message_data) return { "success": True, "id": twilio_message.sid, "status": twilio_message.status, "message": "Message sent successfully" } except Exception as e: error_msg = f"Failed to send SMS to {to_number}: {str(e)}" logger.error(error_msg) return { "success": False, "error": str(e), "message": "Failed to send message" } def _save_message(self, message_data): """Save message to history file""" try: # Ensure messages directory exists Path(self.messages_dir).mkdir(parents=True, exist_ok=True) # Save individual message file message_id = message_data["id"] message_file = os.path.join(self.messages_dir, f"{message_id}.json") with open(message_file, 'w') as f: json.dump(message_data, f, indent=2) # Update message list self._update_message_list(message_data) logger.info(f"Saved message to history: {message_id}") return True except Exception as e: logger.error(f"Failed to save message history: {str(e)}") return False def _update_message_list(self, message_data): """Update the master message list""" message_list_file = os.path.join(DATA_DIR, "message_list.json") try: # Load existing list or create new if os.path.exists(message_list_file): with open(message_list_file, 'r') as f: message_list = json.load(f) else: message_list = [] # Add new message to list (limited data) message_list.append({ "id": message_data["id"], "to": message_data["to"], "from": message_data["from"], "timestamp": message_data["timestamp"], "direction": message_data.get("direction", "outbound"), "preview": message_data["body"][:50] + "..." if len(message_data["body"]) > 50 else message_data["body"] }) # Sort by timestamp (newest first) message_list.sort(key=lambda x: x.get("timestamp", ""), reverse=True) # Limit list to most recent 100 messages if len(message_list) > 100: message_list = message_list[:100] # Save updated list with open(message_list_file, 'w') as f: json.dump(message_list, f, indent=2) return True except Exception as e: logger.error(f"Failed to update message list: {str(e)}") return False def get_message_history(self, limit=50, offset=0): """Get message history""" message_list_file = os.path.join(DATA_DIR, "message_list.json") try: if os.path.exists(message_list_file): with open(message_list_file, 'r') as f: message_list = json.load(f) # Slice the list based on offset and limit return message_list[offset:offset+limit] else: return [] except Exception as e: logger.error(f"Failed to get message history: {str(e)}") return [] def get_message(self, message_id): """Get a specific message by ID""" message_file = os.path.join(self.messages_dir, f"{message_id}.json") try: if os.path.exists(message_file): with open(message_file, 'r') as f: return json.load(f) else: return None except Exception as e: logger.error(f"Failed to get message {message_id}: {str(e)}") return None def is_ready(self): """Check if Twilio is configured and ready""" return self.ready and not self.demo_mode # Create the global instance twilio_manager = TwilioManager()