Files
site11/services/notifications/backend/channel_handlers.py
2025-09-28 20:41:57 +09:00

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