335 lines
13 KiB
Python
335 lines
13 KiB
Python
"""
|
|
Channel Handlers for different notification delivery methods
|
|
"""
|
|
import logging
|
|
import asyncio
|
|
from typing import Optional, Dict, Any
|
|
from models import Notification, NotificationStatus
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
import httpx
|
|
import json
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class BaseChannelHandler:
|
|
"""Base class for channel handlers"""
|
|
|
|
async def send(self, notification: Notification) -> bool:
|
|
"""Send notification through the channel"""
|
|
raise NotImplementedError
|
|
|
|
async def verify_delivery(self, notification: Notification) -> bool:
|
|
"""Verify if notification was delivered"""
|
|
return True
|
|
|
|
class EmailHandler(BaseChannelHandler):
|
|
"""Email notification handler"""
|
|
|
|
def __init__(self, smtp_host: str, smtp_port: int, smtp_user: str, smtp_password: str):
|
|
self.smtp_host = smtp_host
|
|
self.smtp_port = smtp_port
|
|
self.smtp_user = smtp_user
|
|
self.smtp_password = smtp_password
|
|
|
|
async def send(self, notification: Notification) -> bool:
|
|
"""Send email notification"""
|
|
try:
|
|
# In production, would use async SMTP library
|
|
# For demo, we'll simulate email sending
|
|
logger.info(f"Sending email to user {notification.user_id}")
|
|
|
|
if not self.smtp_user or not self.smtp_password:
|
|
# Simulate sending without actual SMTP config
|
|
await asyncio.sleep(0.1) # Simulate network delay
|
|
logger.info(f"Email sent (simulated) to user {notification.user_id}")
|
|
return True
|
|
|
|
# Create message
|
|
msg = MIMEMultipart()
|
|
msg['From'] = self.smtp_user
|
|
msg['To'] = f"user_{notification.user_id}@example.com" # Would fetch actual email
|
|
msg['Subject'] = notification.title
|
|
|
|
# Add body
|
|
body = notification.message
|
|
if notification.data and "html_content" in notification.data:
|
|
msg.attach(MIMEText(notification.data["html_content"], 'html'))
|
|
else:
|
|
msg.attach(MIMEText(body, 'plain'))
|
|
|
|
# Send email (would be async in production)
|
|
# server = smtplib.SMTP(self.smtp_host, self.smtp_port)
|
|
# server.starttls()
|
|
# server.login(self.smtp_user, self.smtp_password)
|
|
# server.send_message(msg)
|
|
# server.quit()
|
|
|
|
logger.info(f"Email sent successfully to user {notification.user_id}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send email: {e}")
|
|
return False
|
|
|
|
class SMSHandler(BaseChannelHandler):
|
|
"""SMS notification handler"""
|
|
|
|
def __init__(self, api_key: str, api_url: str):
|
|
self.api_key = api_key
|
|
self.api_url = api_url
|
|
self.client = httpx.AsyncClient()
|
|
|
|
async def send(self, notification: Notification) -> bool:
|
|
"""Send SMS notification"""
|
|
try:
|
|
# In production, would integrate with SMS provider (Twilio, etc.)
|
|
logger.info(f"Sending SMS to user {notification.user_id}")
|
|
|
|
if not self.api_key or not self.api_url:
|
|
# Simulate sending without actual API config
|
|
await asyncio.sleep(0.1) # Simulate network delay
|
|
logger.info(f"SMS sent (simulated) to user {notification.user_id}")
|
|
return True
|
|
|
|
# Would fetch user's phone number from database
|
|
phone_number = notification.data.get("phone") if notification.data else None
|
|
if not phone_number:
|
|
phone_number = "+1234567890" # Demo number
|
|
|
|
# Send SMS via API (example structure)
|
|
payload = {
|
|
"to": phone_number,
|
|
"message": f"{notification.title}\n{notification.message}",
|
|
"api_key": self.api_key
|
|
}
|
|
|
|
# response = await self.client.post(self.api_url, json=payload)
|
|
# return response.status_code == 200
|
|
|
|
# Simulate success
|
|
await asyncio.sleep(0.1)
|
|
logger.info(f"SMS sent successfully to user {notification.user_id}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send SMS: {e}")
|
|
return False
|
|
|
|
class PushHandler(BaseChannelHandler):
|
|
"""Push notification handler (FCM/APNS)"""
|
|
|
|
def __init__(self, fcm_server_key: str):
|
|
self.fcm_server_key = fcm_server_key
|
|
self.fcm_url = "https://fcm.googleapis.com/fcm/send"
|
|
self.client = httpx.AsyncClient()
|
|
|
|
async def send(self, notification: Notification) -> bool:
|
|
"""Send push notification"""
|
|
try:
|
|
logger.info(f"Sending push notification to user {notification.user_id}")
|
|
|
|
if not self.fcm_server_key:
|
|
# Simulate sending without actual FCM config
|
|
await asyncio.sleep(0.1)
|
|
logger.info(f"Push notification sent (simulated) to user {notification.user_id}")
|
|
return True
|
|
|
|
# Would fetch user's device tokens from database
|
|
device_tokens = notification.data.get("device_tokens", []) if notification.data else []
|
|
|
|
if not device_tokens:
|
|
# Simulate with dummy token
|
|
device_tokens = ["dummy_token"]
|
|
|
|
# Send to each device token
|
|
for token in device_tokens:
|
|
payload = {
|
|
"to": token,
|
|
"notification": {
|
|
"title": notification.title,
|
|
"body": notification.message,
|
|
"icon": notification.data.get("icon") if notification.data else None,
|
|
"click_action": notification.data.get("click_action") if notification.data else None
|
|
},
|
|
"data": notification.data or {}
|
|
}
|
|
|
|
headers = {
|
|
"Authorization": f"key={self.fcm_server_key}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
# response = await self.client.post(
|
|
# self.fcm_url,
|
|
# json=payload,
|
|
# headers=headers
|
|
# )
|
|
|
|
# Simulate success
|
|
await asyncio.sleep(0.05)
|
|
|
|
logger.info(f"Push notification sent successfully to user {notification.user_id}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send push notification: {e}")
|
|
return False
|
|
|
|
class InAppHandler(BaseChannelHandler):
|
|
"""In-app notification handler"""
|
|
|
|
def __init__(self):
|
|
self.ws_server = None
|
|
|
|
def set_ws_server(self, ws_server):
|
|
"""Set WebSocket server for real-time delivery"""
|
|
self.ws_server = ws_server
|
|
|
|
async def send(self, notification: Notification) -> bool:
|
|
"""Send in-app notification"""
|
|
try:
|
|
logger.info(f"Sending in-app notification to user {notification.user_id}")
|
|
|
|
# Store notification in database (already done in manager)
|
|
# This would be retrieved when user logs in or requests notifications
|
|
|
|
# If WebSocket connection exists, send real-time
|
|
if self.ws_server:
|
|
await self.ws_server.send_to_user(
|
|
notification.user_id,
|
|
{
|
|
"type": "notification",
|
|
"notification": {
|
|
"id": notification.id,
|
|
"title": notification.title,
|
|
"message": notification.message,
|
|
"priority": notification.priority.value,
|
|
"category": notification.category.value if hasattr(notification, 'category') else "system",
|
|
"timestamp": notification.created_at.isoformat(),
|
|
"data": notification.data
|
|
}
|
|
}
|
|
)
|
|
|
|
logger.info(f"In-app notification sent successfully to user {notification.user_id}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send in-app notification: {e}")
|
|
return False
|
|
|
|
class SlackHandler(BaseChannelHandler):
|
|
"""Slack notification handler"""
|
|
|
|
def __init__(self, webhook_url: Optional[str] = None):
|
|
self.webhook_url = webhook_url
|
|
self.client = httpx.AsyncClient()
|
|
|
|
async def send(self, notification: Notification) -> bool:
|
|
"""Send Slack notification"""
|
|
try:
|
|
logger.info(f"Sending Slack notification for user {notification.user_id}")
|
|
|
|
if not self.webhook_url:
|
|
# Simulate sending
|
|
await asyncio.sleep(0.1)
|
|
logger.info(f"Slack notification sent (simulated) for user {notification.user_id}")
|
|
return True
|
|
|
|
# Format message for Slack
|
|
slack_message = {
|
|
"text": notification.title,
|
|
"blocks": [
|
|
{
|
|
"type": "header",
|
|
"text": {
|
|
"type": "plain_text",
|
|
"text": notification.title
|
|
}
|
|
},
|
|
{
|
|
"type": "section",
|
|
"text": {
|
|
"type": "mrkdwn",
|
|
"text": notification.message
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
# Add additional fields if present
|
|
if notification.data:
|
|
fields = []
|
|
for key, value in notification.data.items():
|
|
if key not in ["html_content", "device_tokens"]:
|
|
fields.append({
|
|
"type": "mrkdwn",
|
|
"text": f"*{key}:* {value}"
|
|
})
|
|
|
|
if fields:
|
|
slack_message["blocks"].append({
|
|
"type": "section",
|
|
"fields": fields[:10] # Slack limits to 10 fields
|
|
})
|
|
|
|
# Send to Slack
|
|
# response = await self.client.post(self.webhook_url, json=slack_message)
|
|
# return response.status_code == 200
|
|
|
|
await asyncio.sleep(0.1)
|
|
logger.info(f"Slack notification sent successfully")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send Slack notification: {e}")
|
|
return False
|
|
|
|
class WebhookHandler(BaseChannelHandler):
|
|
"""Generic webhook notification handler"""
|
|
|
|
def __init__(self, default_webhook_url: Optional[str] = None):
|
|
self.default_webhook_url = default_webhook_url
|
|
self.client = httpx.AsyncClient()
|
|
|
|
async def send(self, notification: Notification) -> bool:
|
|
"""Send webhook notification"""
|
|
try:
|
|
# Get webhook URL from notification data or use default
|
|
webhook_url = None
|
|
if notification.data and "webhook_url" in notification.data:
|
|
webhook_url = notification.data["webhook_url"]
|
|
else:
|
|
webhook_url = self.default_webhook_url
|
|
|
|
if not webhook_url:
|
|
logger.warning("No webhook URL configured")
|
|
return False
|
|
|
|
logger.info(f"Sending webhook notification for user {notification.user_id}")
|
|
|
|
# Prepare payload
|
|
payload = {
|
|
"notification_id": notification.id,
|
|
"user_id": notification.user_id,
|
|
"title": notification.title,
|
|
"message": notification.message,
|
|
"priority": notification.priority.value,
|
|
"timestamp": notification.created_at.isoformat(),
|
|
"data": notification.data
|
|
}
|
|
|
|
# Send webhook
|
|
# response = await self.client.post(webhook_url, json=payload)
|
|
# return response.status_code in [200, 201, 202, 204]
|
|
|
|
# Simulate success
|
|
await asyncio.sleep(0.1)
|
|
logger.info(f"Webhook notification sent successfully")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send webhook notification: {e}")
|
|
return False |