Delete src/whatsapp_service.py
Browse files- src/whatsapp_service.py +0 -190
src/whatsapp_service.py
DELETED
@@ -1,190 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
import logging
|
3 |
-
import base64
|
4 |
-
import requests
|
5 |
-
from twilio.rest import Client
|
6 |
-
from twilio.base.exceptions import TwilioRestException
|
7 |
-
|
8 |
-
# Configure logging
|
9 |
-
logging.basicConfig(level=logging.INFO)
|
10 |
-
logger = logging.getLogger(__name__)
|
11 |
-
|
12 |
-
class WhatsAppService:
|
13 |
-
def __init__(self):
|
14 |
-
"""Initialize WhatsApp service with Twilio credentials"""
|
15 |
-
self.account_sid = os.getenv('TWILIO_ACCOUNT_SID')
|
16 |
-
self.auth_token = os.getenv('TWILIO_AUTH_TOKEN')
|
17 |
-
self.from_number = os.getenv('TWILIO_WHATSAPP_NUMBER') # Format: 'whatsapp:+1234567890'
|
18 |
-
|
19 |
-
# Validate WhatsApp number format
|
20 |
-
if self.from_number and not self.from_number.startswith('whatsapp:'):
|
21 |
-
logger.warning(f"TWILIO_WHATSAPP_NUMBER should start with 'whatsapp:' - Current value: {self.from_number}")
|
22 |
-
# Try to fix it automatically
|
23 |
-
if self.from_number.startswith('+'):
|
24 |
-
self.from_number = f"whatsapp:{self.from_number}"
|
25 |
-
logger.info(f"Fixed WhatsApp number format: {self.from_number}")
|
26 |
-
|
27 |
-
if not all([self.account_sid, self.auth_token, self.from_number]):
|
28 |
-
missing = []
|
29 |
-
if not self.account_sid: missing.append("TWILIO_ACCOUNT_SID")
|
30 |
-
if not self.auth_token: missing.append("TWILIO_AUTH_TOKEN")
|
31 |
-
if not self.from_number: missing.append("TWILIO_WHATSAPP_NUMBER")
|
32 |
-
logger.warning(f"Twilio credentials not found in environment variables: {', '.join(missing)}")
|
33 |
-
|
34 |
-
# Initialize Twilio client if credentials are available
|
35 |
-
if all([self.account_sid, self.auth_token]):
|
36 |
-
self.client = Client(self.account_sid, self.auth_token)
|
37 |
-
else:
|
38 |
-
self.client = None
|
39 |
-
|
40 |
-
def validate_phone(self, phone_number):
|
41 |
-
"""
|
42 |
-
Basic validation for phone numbers
|
43 |
-
|
44 |
-
Args:
|
45 |
-
phone_number (str): The phone number to validate
|
46 |
-
|
47 |
-
Returns:
|
48 |
-
bool: True if phone number format is valid
|
49 |
-
"""
|
50 |
-
import re
|
51 |
-
# Remove any non-digit characters except for leading +
|
52 |
-
cleaned = re.sub(r'[^\d+]', '', phone_number)
|
53 |
-
|
54 |
-
# Check if it starts with + followed by 7-15 digits
|
55 |
-
if re.match(r'^\+\d{7,15}$', cleaned):
|
56 |
-
return True, cleaned
|
57 |
-
# Check if it's 7-15 digits without +
|
58 |
-
elif re.match(r'^\d{7,15}$', cleaned):
|
59 |
-
return True, f"+{cleaned}"
|
60 |
-
|
61 |
-
return False, None
|
62 |
-
|
63 |
-
def format_whatsapp_number(self, phone_number):
|
64 |
-
"""
|
65 |
-
Format phone number for WhatsApp API
|
66 |
-
|
67 |
-
Args:
|
68 |
-
phone_number (str): Raw phone number
|
69 |
-
|
70 |
-
Returns:
|
71 |
-
str: Formatted WhatsApp number or None if invalid
|
72 |
-
"""
|
73 |
-
is_valid, cleaned = self.validate_phone(phone_number)
|
74 |
-
if is_valid:
|
75 |
-
# WhatsApp numbers must be in format 'whatsapp:+1234567890'
|
76 |
-
if not cleaned.startswith('+'):
|
77 |
-
cleaned = f"+{cleaned}"
|
78 |
-
return f"whatsapp:{cleaned}"
|
79 |
-
return None
|
80 |
-
|
81 |
-
def check_twilio_setup(self):
|
82 |
-
"""
|
83 |
-
Diagnose Twilio setup issues
|
84 |
-
|
85 |
-
Returns:
|
86 |
-
tuple: (is_valid, error_message)
|
87 |
-
"""
|
88 |
-
if not self.client:
|
89 |
-
return False, "Twilio client not initialized. Check your TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN."
|
90 |
-
|
91 |
-
if not self.from_number:
|
92 |
-
return False, "WhatsApp number not specified. Set TWILIO_WHATSAPP_NUMBER environment variable."
|
93 |
-
|
94 |
-
if not self.from_number.startswith('whatsapp:'):
|
95 |
-
return False, f"TWILIO_WHATSAPP_NUMBER should be in format 'whatsapp:+1234567890'. Current: {self.from_number}"
|
96 |
-
|
97 |
-
# Basic connectivity test
|
98 |
-
try:
|
99 |
-
# Try to fetch account info to verify credentials
|
100 |
-
self.client.api.accounts(self.account_sid).fetch()
|
101 |
-
return True, "Twilio setup looks correct"
|
102 |
-
except TwilioRestException as e:
|
103 |
-
return False, f"Twilio authentication failed: {str(e)}"
|
104 |
-
except Exception as e:
|
105 |
-
return False, f"Twilio connectivity error: {str(e)}"
|
106 |
-
|
107 |
-
def send_report(self, to_phone, pdf_data, patient_name):
|
108 |
-
"""
|
109 |
-
Send medical report via WhatsApp
|
110 |
-
|
111 |
-
Args:
|
112 |
-
to_phone (str): Recipient's phone number
|
113 |
-
pdf_data (bytes): The PDF report as bytes
|
114 |
-
patient_name (str): Patient name for personalization
|
115 |
-
|
116 |
-
Returns:
|
117 |
-
tuple[bool, str]: Success status and message
|
118 |
-
"""
|
119 |
-
try:
|
120 |
-
# Run diagnostic checks first
|
121 |
-
is_setup_valid, setup_error = self.check_twilio_setup()
|
122 |
-
if not is_setup_valid:
|
123 |
-
logger.error(f"WhatsApp service setup error: {setup_error}")
|
124 |
-
return False, f"WhatsApp service not properly configured: {setup_error}"
|
125 |
-
|
126 |
-
# Format the recipient's phone number
|
127 |
-
to_whatsapp = self.format_whatsapp_number(to_phone)
|
128 |
-
if not to_whatsapp:
|
129 |
-
return False, "Invalid phone number format. Please provide a valid number with country code."
|
130 |
-
|
131 |
-
# Log the 'from' and 'to' numbers for debugging
|
132 |
-
logger.info(f"Attempting to send WhatsApp message from {self.from_number} to {to_whatsapp}")
|
133 |
-
|
134 |
-
# For Twilio sandbox, first check if the recipient is opted-in
|
135 |
-
# Note: This is just a warning, it doesn't prevent sending
|
136 |
-
logger.warning("For Twilio sandbox, recipients must first send the join code to your WhatsApp number")
|
137 |
-
|
138 |
-
# For test purposes, use a publicly accessible PDF instead of trying to upload
|
139 |
-
# In production, you would need to host the file somewhere publicly accessible
|
140 |
-
try:
|
141 |
-
# Send the WhatsApp message with test PDF
|
142 |
-
message = self.client.messages.create(
|
143 |
-
from_=self.from_number,
|
144 |
-
body=f"Medical Report for {patient_name}\n\nPlease find attached your medical report from your recent consultation.\n\nNote: This report is for informational purposes only.",
|
145 |
-
to=to_whatsapp,
|
146 |
-
media_url=['https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf'] # Test PDF URL
|
147 |
-
)
|
148 |
-
|
149 |
-
logger.info(f"Successfully sent WhatsApp to {to_phone} (SID: {message.sid})")
|
150 |
-
return True, "Report sent successfully via WhatsApp!"
|
151 |
-
|
152 |
-
except TwilioRestException as e:
|
153 |
-
# Specific error handling for Twilio API errors
|
154 |
-
error_code = getattr(e, 'code', None)
|
155 |
-
error_message = str(e)
|
156 |
-
|
157 |
-
if error_code == 63018:
|
158 |
-
# "To number is not currently opted in" error
|
159 |
-
logger.error(f"Recipient not opted in: {error_message}")
|
160 |
-
return False, "The recipient needs to opt in to your WhatsApp sandbox first. Please ask them to message your WhatsApp number with the join code."
|
161 |
-
elif 'Channel' in error_message and 'From address' in error_message:
|
162 |
-
# Channel not found error
|
163 |
-
logger.error(f"WhatsApp channel not found: {error_message}")
|
164 |
-
return False, "Your WhatsApp channel is not properly set up. Make sure you've completed the Twilio sandbox setup and your TWILIO_WHATSAPP_NUMBER is correct."
|
165 |
-
else:
|
166 |
-
# Other Twilio API errors
|
167 |
-
logger.error(f"Twilio API error ({error_code}): {error_message}")
|
168 |
-
|
169 |
-
# Try a fallback text-only message
|
170 |
-
try:
|
171 |
-
fallback_message = self.client.messages.create(
|
172 |
-
from_=self.from_number,
|
173 |
-
body=f"Medical Report for {patient_name}\n\nYour medical report is ready. Due to technical limitations, please download it from the app or request it via email.\n\nThis is a fallback message as we couldn't attach the PDF.",
|
174 |
-
to=to_whatsapp
|
175 |
-
)
|
176 |
-
logger.info(f"Sent fallback WhatsApp message to {to_phone} (SID: {fallback_message.sid})")
|
177 |
-
return True, "WhatsApp notification sent, but the PDF couldn't be attached. Please use email or download options instead."
|
178 |
-
except Exception as e2:
|
179 |
-
logger.error(f"Fallback message also failed: {str(e2)}")
|
180 |
-
return False, f"WhatsApp sending failed. Error: {error_message}"
|
181 |
-
|
182 |
-
except TwilioRestException as e:
|
183 |
-
# General Twilio API errors
|
184 |
-
logger.error(f"Twilio API error: {str(e)}")
|
185 |
-
return False, f"WhatsApp sending failed: {str(e)}"
|
186 |
-
|
187 |
-
except Exception as e:
|
188 |
-
# Unexpected errors
|
189 |
-
logger.error(f"Unexpected error sending WhatsApp: {str(e)}")
|
190 |
-
return False, "An unexpected error occurred. Please try again later."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|