Jayscallcenter / modules /twilio_integration.py
jjmandog's picture
Update modules/twilio_integration.py
8a5fa76 verified
# 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()