Spaces:
Sleeping
Sleeping
# 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() |