Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5481e60f83 |
@@ -55,7 +55,6 @@ simple-websocket==1.1.0
|
||||
sniffio==1.3.1
|
||||
sqlalchemy==2.0.23
|
||||
starlette==0.27.0
|
||||
twilio==8.10.0
|
||||
typing-extensions==4.14.1
|
||||
uritemplate==4.2.0
|
||||
urllib3==2.5.0
|
||||
|
||||
+10
-8
@@ -11,7 +11,7 @@ from database import (
|
||||
get_last_incoming_outgoing,
|
||||
get_thread_messages,
|
||||
)
|
||||
from whatsapp_sender import WhatsAppSender
|
||||
from slack_sender import SlackSender
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -68,7 +68,7 @@ def _format_alert(
|
||||
|
||||
|
||||
def process_alerts(db: Session, cfg: dict) -> List[int]:
|
||||
"""Analyze threads and send WhatsApp alerts when thresholds are met."""
|
||||
"""Analyze threads and send Slack alerts when thresholds are met."""
|
||||
import logging
|
||||
|
||||
account_email = (cfg.get("email_address") or cfg.get("zoho_email") or "").lower()
|
||||
@@ -97,9 +97,11 @@ def process_alerts(db: Session, cfg: dict) -> List[int]:
|
||||
|
||||
logging.info(f"Found {len(threads)} threads requiring reply")
|
||||
|
||||
to_number = cfg.get("whatsapp_to") or None
|
||||
logging.info(f"WhatsApp target: {to_number}")
|
||||
sender = WhatsAppSender(to_number=to_number)
|
||||
webhook_url = cfg.get("slack_webhook_url") or None
|
||||
logging.info(
|
||||
f"Slack webhook URL: {'✓ Configured' if webhook_url else '✗ Not configured'}"
|
||||
)
|
||||
sender = SlackSender(webhook_url=webhook_url)
|
||||
alerted = []
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
@@ -179,11 +181,11 @@ def process_alerts(db: Session, cfg: dict) -> List[int]:
|
||||
logging.info(f" Thread {t.id}: Not actionable, skipping alert")
|
||||
continue
|
||||
|
||||
# Compose and send WhatsApp
|
||||
# Compose and send Slack
|
||||
msg = _format_alert(target_level, target_hours, t, ai, last_in)
|
||||
logging.info(f" Thread {t.id}: Sending alert via WhatsApp")
|
||||
logging.info(f" Thread {t.id}: Sending alert via Slack")
|
||||
res = sender.send_alert(msg, thread_id=str(t.id))
|
||||
logging.info(f" Thread {t.id}: WhatsApp response: {res}")
|
||||
logging.info(f" Thread {t.id}: Slack response: {res}")
|
||||
|
||||
if res.get("status") == "success":
|
||||
t.last_alert_level_sent = target_level
|
||||
|
||||
+5
-3
@@ -226,7 +226,7 @@ def load_config() -> dict:
|
||||
"agency_domains": [],
|
||||
"zoho_email": "",
|
||||
"zoho_app_password": "",
|
||||
"whatsapp_to": "",
|
||||
"slack_webhook_url": "",
|
||||
"auto_process": False,
|
||||
"auto_process_interval": 30,
|
||||
# Sync status
|
||||
@@ -309,8 +309,10 @@ async def config_save(request: Request):
|
||||
cfg["zoho_app_password"] = (
|
||||
form.get("zoho_app_password") or cfg.get("zoho_app_password", "")
|
||||
).strip()
|
||||
# WhatsApp destination
|
||||
cfg["whatsapp_to"] = (form.get("whatsapp_to") or cfg.get("whatsapp_to", "")).strip()
|
||||
# Slack webhook URL
|
||||
cfg["slack_webhook_url"] = (
|
||||
form.get("slack_webhook_url") or cfg.get("slack_webhook_url", "")
|
||||
).strip()
|
||||
|
||||
# Time frames: collect indexed rows
|
||||
frames: list[dict] = []
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import requests
|
||||
|
||||
class SlackSender:
|
||||
def __init__(self, webhook_url: str | None = None):
|
||||
env_webhook = (os.getenv("SLACK_WEBHOOK_URL") or "").strip()
|
||||
self.webhook_url = webhook_url or env_webhook
|
||||
|
||||
# Log credential status (without exposing full webhook URL)
|
||||
webhook_preview = (
|
||||
self.webhook_url[:40] + "..."
|
||||
if len(self.webhook_url) > 40
|
||||
else self.webhook_url
|
||||
)
|
||||
logging.info(
|
||||
f"Slack config: Webhook={'✓' if self.webhook_url else '✗'} "
|
||||
f"({webhook_preview if self.webhook_url else 'missing'})"
|
||||
)
|
||||
|
||||
if self.webhook_url:
|
||||
self.use_mock = False
|
||||
logging.info("Slack sender initialized successfully")
|
||||
else:
|
||||
self.use_mock = True
|
||||
logging.warning(
|
||||
"Using mock Slack sender. Missing: SLACK_WEBHOOK_URL or webhook_url parameter"
|
||||
)
|
||||
|
||||
def send_alert(self, alert_message: str, thread_id: str = None) -> Dict[str, Any]:
|
||||
"""Send alert message to Slack"""
|
||||
if self.use_mock:
|
||||
return self._mock_send(alert_message, thread_id)
|
||||
|
||||
try:
|
||||
# Format message for Slack
|
||||
formatted_message = self._format_message(alert_message, thread_id)
|
||||
|
||||
# Send to Slack via webhook
|
||||
response = requests.post(
|
||||
self.webhook_url,
|
||||
json={"text": formatted_message},
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=10,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
# Slack webhooks return plain text "ok" on success, not JSON
|
||||
try:
|
||||
response_data = response.json()
|
||||
logging.info(f"Slack response: {response_data}")
|
||||
except ValueError:
|
||||
# Response is plain text (e.g., "ok")
|
||||
response_text = response.text.strip()
|
||||
logging.info(f"Slack response: {response_text}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message_sid": response.headers.get(
|
||||
"X-Slack-Request-Timestamp", "unknown"
|
||||
),
|
||||
"thread_id": thread_id,
|
||||
"sent_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
error_msg = str(e)
|
||||
logging.error(f"Slack send error: {error_msg}")
|
||||
|
||||
# Provide specific guidance based on error
|
||||
if "401" in error_msg or "403" in error_msg:
|
||||
logging.error(
|
||||
"Authentication failed! Check your Slack webhook URL:\n"
|
||||
" 1. Verify SLACK_WEBHOOK_URL in environment or config\n"
|
||||
" 2. Make sure the webhook URL is valid and not revoked\n"
|
||||
" 3. Check that the webhook is enabled in your Slack app settings\n"
|
||||
" 4. Create a new webhook at https://api.slack.com/apps if needed"
|
||||
)
|
||||
elif "404" in error_msg:
|
||||
logging.error(
|
||||
"Invalid webhook URL: The endpoint was not found.\n"
|
||||
" Make sure your webhook URL is correct and hasn't been revoked."
|
||||
)
|
||||
elif "timeout" in error_msg.lower():
|
||||
logging.error("Request timeout: Slack may be temporarily unavailable.")
|
||||
|
||||
return {"status": "error", "error": error_msg, "thread_id": thread_id}
|
||||
|
||||
def _mock_send(self, alert_message: str, thread_id: str = None) -> Dict[str, Any]:
|
||||
"""Mock Slack sending for testing"""
|
||||
print("💬 [MOCK] Slack Alert Sent:")
|
||||
print(f" Webhook: {self.webhook_url or 'not_configured'}")
|
||||
print(f" Thread ID: {thread_id}")
|
||||
print(f" Message: {alert_message[:100]}...")
|
||||
return {
|
||||
"status": "success",
|
||||
"message_sid": "mock_sid_123",
|
||||
"thread_id": thread_id,
|
||||
"sent_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
|
||||
def _format_message(self, alert_message: str, thread_id: str = None) -> str:
|
||||
"""Format alert message for Slack"""
|
||||
# Slack messages can be longer, but we'll keep it reasonable
|
||||
# Thread ID is already included in the alert_message format
|
||||
formatted = alert_message
|
||||
|
||||
# Slack has a 4000 character limit for messages
|
||||
max_length = 4000
|
||||
if len(formatted) > max_length:
|
||||
formatted = formatted[: max_length - 3] + "..."
|
||||
|
||||
return formatted
|
||||
|
||||
def send_bulk_alerts(self, alerts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Send multiple alerts to Slack"""
|
||||
results = []
|
||||
|
||||
for alert in alerts:
|
||||
message = alert.get("message", "")
|
||||
thread_id = alert.get("thread_id", "unknown")
|
||||
|
||||
result = self.send_alert(message, thread_id)
|
||||
results.append(result)
|
||||
|
||||
# Add small delay between messages to avoid rate limits
|
||||
time.sleep(0.5)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test Slack sender
|
||||
sender = SlackSender()
|
||||
|
||||
test_message = """
|
||||
🚨 LEVEL 1 ALERT (24 Hours)
|
||||
|
||||
🟢 Urgency: LOW
|
||||
📧 Thread ID: test_thread_123
|
||||
|
||||
📝 Summary:
|
||||
Client inquiry about project status. Requires follow-up.
|
||||
|
||||
🎯 Action Required:
|
||||
Respond to client question
|
||||
|
||||
⏰ Confidence: 70.0%
|
||||
""".strip()
|
||||
|
||||
result = sender.send_alert(test_message, "test_thread_123")
|
||||
print(f"Send result: {result}")
|
||||
@@ -1,156 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from twilio.base.exceptions import TwilioException
|
||||
from twilio.rest import Client
|
||||
|
||||
|
||||
class WhatsAppSender:
|
||||
def __init__(self, to_number: str | None = None):
|
||||
self.account_sid = (os.getenv("TWILIO_ACCOUNT_SID") or "").strip()
|
||||
self.auth_token = (os.getenv("TWILIO_AUTH_TOKEN") or "").strip()
|
||||
self.from_number = (os.getenv("TWILIO_WHATSAPP_NUMBER") or "").strip()
|
||||
env_to = (os.getenv("WHATSAPP_TO_NUMBER") or "").strip()
|
||||
self.to_number = to_number or env_to # Individual phone number
|
||||
|
||||
# Log credential status (without exposing full credentials)
|
||||
logging.info(
|
||||
f"Twilio config: SID={'✓' if self.account_sid else '✗'} "
|
||||
f"({self.account_sid[:8] + '...' if self.account_sid else 'missing'}), "
|
||||
f"Token={'✓' if self.auth_token else '✗'}, "
|
||||
f"From={'✓' if self.from_number else '✗'} ({self.from_number}), "
|
||||
f"To={'✓' if self.to_number else '✗'} ({self.to_number})"
|
||||
)
|
||||
|
||||
if self.account_sid and self.auth_token and self.from_number and self.to_number:
|
||||
try:
|
||||
self.client = Client(self.account_sid, self.auth_token)
|
||||
self.use_mock = False
|
||||
logging.info("Twilio client initialized successfully")
|
||||
except Exception as e:
|
||||
logging.error(f"Twilio client failed to initialize: {e}")
|
||||
self.use_mock = True
|
||||
else:
|
||||
self.use_mock = True
|
||||
missing = []
|
||||
if not self.account_sid:
|
||||
missing.append("TWILIO_ACCOUNT_SID")
|
||||
if not self.auth_token:
|
||||
missing.append("TWILIO_AUTH_TOKEN")
|
||||
if not self.from_number:
|
||||
missing.append("TWILIO_WHATSAPP_NUMBER")
|
||||
if not self.to_number:
|
||||
missing.append("WHATSAPP_TO_NUMBER or to_number parameter")
|
||||
logging.warning(
|
||||
f"Using mock WhatsApp sender. Missing: {', '.join(missing)}"
|
||||
)
|
||||
|
||||
def send_alert(self, alert_message: str, thread_id: str = None) -> Dict[str, Any]:
|
||||
"""Send alert message to WhatsApp"""
|
||||
if self.use_mock:
|
||||
return self._mock_send(alert_message, thread_id)
|
||||
|
||||
try:
|
||||
# Format message for WhatsApp
|
||||
formatted_message = self._format_message(alert_message)
|
||||
|
||||
# Send to WhatsApp
|
||||
message = self.client.messages.create(
|
||||
from_=f"whatsapp:{self.from_number}",
|
||||
body=formatted_message,
|
||||
to=f"whatsapp:{self.to_number}",
|
||||
)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message_sid": message.sid,
|
||||
"thread_id": thread_id,
|
||||
"sent_at": message.date_created,
|
||||
}
|
||||
|
||||
except TwilioException as e:
|
||||
error_msg = str(e)
|
||||
logging.error(f"WhatsApp send error: {error_msg}")
|
||||
|
||||
# Provide specific guidance based on error
|
||||
if "20003" in error_msg or "Authenticate" in error_msg:
|
||||
logging.error(
|
||||
"Authentication failed! Check your Twilio credentials:\n"
|
||||
" 1. Verify TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN in .env file\n"
|
||||
" 2. Make sure there are no extra spaces or quotes\n"
|
||||
" 3. Confirm you're using the correct credentials (Live vs Test)\n"
|
||||
" 4. Check https://console.twilio.com for correct values"
|
||||
)
|
||||
elif "21211" in error_msg:
|
||||
logging.error(f"Invalid 'To' phone number: {self.to_number}")
|
||||
elif "21408" in error_msg:
|
||||
logging.error(
|
||||
f"WhatsApp not enabled for number: {self.from_number}\n"
|
||||
" Enable WhatsApp at https://console.twilio.com/us1/develop/sms/senders/whatsapp-senders"
|
||||
)
|
||||
|
||||
return {"status": "error", "error": error_msg, "thread_id": thread_id}
|
||||
|
||||
def _mock_send(self, alert_message: str, thread_id: str = None) -> Dict[str, Any]:
|
||||
"""Mock WhatsApp sending for testing"""
|
||||
print("📱 [MOCK] WhatsApp Alert Sent:")
|
||||
print(f" To: {self.to_number or 'your_number'}")
|
||||
print(f" Thread ID: {thread_id}")
|
||||
print(f" Message: {alert_message[:100]}...")
|
||||
return {
|
||||
"status": "success",
|
||||
"message_sid": "mock_sid_123",
|
||||
"thread_id": thread_id,
|
||||
"sent_at": "2024-01-15T10:00:00Z",
|
||||
}
|
||||
|
||||
def _format_message(self, alert_message: str) -> str:
|
||||
"""Format alert message for WhatsApp"""
|
||||
# WhatsApp has character limits, so we might need to truncate
|
||||
max_length = 1000
|
||||
if len(alert_message) > max_length:
|
||||
alert_message = alert_message[: max_length - 3] + "..."
|
||||
|
||||
return alert_message
|
||||
|
||||
def send_bulk_alerts(self, alerts: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""Send multiple alerts to WhatsApp"""
|
||||
results = []
|
||||
|
||||
for alert in alerts:
|
||||
message = alert.get("message", "")
|
||||
thread_id = alert.get("thread_id", "unknown")
|
||||
|
||||
result = self.send_alert(message, thread_id)
|
||||
results.append(result)
|
||||
|
||||
# Add small delay between messages to avoid rate limits
|
||||
import time
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Test WhatsApp sender
|
||||
sender = WhatsAppSender()
|
||||
|
||||
test_message = """
|
||||
🚨 LEVEL 1 ALERT (24 Hours)
|
||||
|
||||
🟢 Urgency: LOW
|
||||
📧 Thread ID: test_thread_123
|
||||
|
||||
📝 Summary:
|
||||
Client inquiry about project status. Requires follow-up.
|
||||
|
||||
🎯 Action Required:
|
||||
Respond to client question
|
||||
|
||||
⏰ Confidence: 70.0%
|
||||
""".strip()
|
||||
|
||||
result = sender.send_alert(test_message, "test_thread_123")
|
||||
print(f"Send result: {result}")
|
||||
@@ -47,12 +47,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="mt-2">WhatsApp</h3>
|
||||
<h3 class="mt-2">Slack</h3>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<label>Send To (WhatsApp E.164, no spaces)<br>
|
||||
<input type="text" name="whatsapp_to" placeholder="+15551234567" value="{{ cfg.whatsapp_to or '' }}" />
|
||||
<label>Webhook URL<br>
|
||||
<input type="url" name="slack_webhook_url" placeholder="https://hooks.slack.com/services/..." value="{{ cfg.slack_webhook_url or '' }}" />
|
||||
</label>
|
||||
<p class="muted" style="font-size: 0.9em; margin-top: 0.25em;">
|
||||
Create a webhook at <a href="https://api.slack.com/apps" target="_blank">api.slack.com/apps</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
-114
@@ -1,114 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test Twilio credentials and WhatsApp configuration
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from twilio.rest import Client
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def test_credentials():
|
||||
print("=" * 60)
|
||||
print("TWILIO CREDENTIALS TEST")
|
||||
print("=" * 60)
|
||||
|
||||
# Get credentials
|
||||
account_sid = (os.getenv("TWILIO_ACCOUNT_SID") or "").strip()
|
||||
auth_token = (os.getenv("TWILIO_AUTH_TOKEN") or "").strip()
|
||||
from_number = (os.getenv("TWILIO_WHATSAPP_NUMBER") or "").strip()
|
||||
to_number = (os.getenv("WHATSAPP_TO_NUMBER") or "").strip()
|
||||
|
||||
# Check if credentials exist
|
||||
print("\n1. Checking environment variables:")
|
||||
print(f" TWILIO_ACCOUNT_SID: {'✓ Found' if account_sid else '✗ Missing'}")
|
||||
if account_sid:
|
||||
print(f" Value: {account_sid[:8]}...{account_sid[-4:]}")
|
||||
|
||||
print(f" TWILIO_AUTH_TOKEN: {'✓ Found' if auth_token else '✗ Missing'}")
|
||||
if auth_token:
|
||||
print(f" Value: {'*' * len(auth_token[:4])}...{auth_token[-4:]}")
|
||||
|
||||
print(f" TWILIO_WHATSAPP_NUMBER: {'✓ Found' if from_number else '✗ Missing'}")
|
||||
if from_number:
|
||||
print(f" Value: {from_number}")
|
||||
|
||||
print(f" WHATSAPP_TO_NUMBER: {'✓ Found' if to_number else '✗ Missing'}")
|
||||
if to_number:
|
||||
print(f" Value: {to_number}")
|
||||
|
||||
# Test authentication
|
||||
if not (account_sid and auth_token):
|
||||
print("\n❌ Missing required credentials!")
|
||||
print("\nPlease check your .env file and ensure:")
|
||||
print(" - TWILIO_ACCOUNT_SID is set")
|
||||
print(" - TWILIO_AUTH_TOKEN is set")
|
||||
print(" - No extra spaces or quotes around values")
|
||||
return False
|
||||
|
||||
print("\n2. Testing Twilio authentication...")
|
||||
try:
|
||||
client = Client(account_sid, auth_token)
|
||||
|
||||
# Try to fetch account info to verify auth
|
||||
account = client.api.accounts(account_sid).fetch()
|
||||
print(" ✓ Authentication successful!")
|
||||
print(f" Account: {account.friendly_name}")
|
||||
print(f" Status: {account.status}")
|
||||
|
||||
except Exception as e:
|
||||
print(" ✗ Authentication failed!")
|
||||
print(f" Error: {e}")
|
||||
print("\n Troubleshooting:")
|
||||
print(" 1. Go to https://console.twilio.com")
|
||||
print(" 2. Navigate to Account > API keys & tokens")
|
||||
print(" 3. Copy the Account SID and Auth Token")
|
||||
print(" 4. Update your .env file with correct values")
|
||||
print(" 5. Make sure there are NO quotes or spaces")
|
||||
return False
|
||||
|
||||
# Test WhatsApp sender configuration
|
||||
print("\n3. Testing WhatsApp sender configuration...")
|
||||
if not from_number:
|
||||
print(" ✗ TWILIO_WHATSAPP_NUMBER not set")
|
||||
print(" Please set up WhatsApp sender at:")
|
||||
print(" https://console.twilio.com/us1/develop/sms/senders/whatsapp-senders")
|
||||
return False
|
||||
|
||||
if not from_number.startswith("+"):
|
||||
print(f" ⚠ Warning: Phone number should start with '+': {from_number}")
|
||||
|
||||
print(f" ✓ WhatsApp sender configured: {from_number}")
|
||||
|
||||
# Test recipient
|
||||
print("\n4. Testing recipient configuration...")
|
||||
if not to_number:
|
||||
print(" ✗ WHATSAPP_TO_NUMBER not set")
|
||||
return False
|
||||
|
||||
if not to_number.startswith("+"):
|
||||
print(f" ⚠ Warning: Phone number should start with '+': {to_number}")
|
||||
|
||||
print(f" ✓ Recipient configured: {to_number}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("✓ ALL CHECKS PASSED - Ready to send WhatsApp messages!")
|
||||
print("=" * 60)
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = test_credentials()
|
||||
|
||||
if success:
|
||||
print("\nYou can now send test messages with:")
|
||||
print(
|
||||
" python -c 'from src.whatsapp_sender import WhatsAppSender; "
|
||||
'w = WhatsAppSender(); print(w.send_alert("Test message"))\''
|
||||
)
|
||||
else:
|
||||
print("\n❌ Please fix the issues above before sending WhatsApp messages")
|
||||
Reference in New Issue
Block a user