Initial commit - cleaned repository
This commit is contained in:
335
services/notifications/backend/channel_handlers.py
Normal file
335
services/notifications/backend/channel_handlers.py
Normal file
@ -0,0 +1,335 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user