""" 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