377 lines
15 KiB
Python
377 lines
15 KiB
Python
import logging
|
|
from datetime import datetime, timezone, timedelta
|
|
from django.core.mail import EmailMultiAlternatives
|
|
from django.template.loader import render_to_string
|
|
from django.utils.html import strip_tags
|
|
from django.conf import settings
|
|
from django.urls import reverse
|
|
|
|
from monitor.models import NotificationPreference, EmailNotification
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EmailService:
|
|
"""Service for sending Bitcoin monitor notifications."""
|
|
|
|
def __init__(self):
|
|
self.site_domain = getattr(settings, 'SITE_DOMAIN', 'localhost:8000')
|
|
self.from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@bitcoin-monitor.com')
|
|
|
|
def get_notification_preferences(self):
|
|
"""Get all active notification preferences."""
|
|
return NotificationPreference.objects.filter(is_active=True)
|
|
|
|
def create_email_notification_record(self, recipient, subject, notification_type):
|
|
"""Create a record of the email to be sent."""
|
|
return EmailNotification.objects.create(
|
|
recipient=recipient,
|
|
subject=subject,
|
|
notification_type=notification_type,
|
|
status='pending'
|
|
)
|
|
|
|
def update_notification_status(self, email_notification, status, error_message=''):
|
|
"""Update the status of an email notification."""
|
|
email_notification.status = status
|
|
email_notification.error_message = error_message
|
|
if status == 'sent':
|
|
email_notification.sent_at = datetime.now(timezone.utc)
|
|
email_notification.save()
|
|
|
|
def render_email_template(self, template_name, context):
|
|
"""Render email template with context."""
|
|
# Add common context variables
|
|
context.update({
|
|
'dashboard_url': f'http://{self.site_domain}',
|
|
'admin_url': f'http://{self.site_domain}/admin/',
|
|
'unsubscribe_url': f'http://{self.site_domain}/api/notifications/unsubscribe/',
|
|
})
|
|
|
|
# Render HTML and plain text versions
|
|
html_content = render_to_string(f'emails/{template_name}', context)
|
|
text_content = strip_tags(html_content)
|
|
|
|
return html_content, text_content
|
|
|
|
def send_event_alert(self, analysis):
|
|
"""Send event alert email."""
|
|
try:
|
|
# Get recipients who want event alerts
|
|
preferences = self.get_notification_preferences().filter(
|
|
receive_event_alerts=True
|
|
)
|
|
|
|
if not preferences.exists():
|
|
logger.info("No recipients for event alerts")
|
|
return
|
|
|
|
# Calculate percent change
|
|
percent_change = abs(
|
|
(float(analysis.current_price) - float(analysis.average_price)) /
|
|
float(analysis.average_price) * 100
|
|
)
|
|
|
|
# Prepare context
|
|
context = {
|
|
'alert_type': analysis.status,
|
|
'current_price': float(analysis.current_price),
|
|
'yearly_average': float(analysis.average_price),
|
|
'threshold_percent': float(analysis.threshold_percent),
|
|
'lower_threshold': float(analysis.lower_threshold),
|
|
'upper_threshold': float(analysis.upper_threshold),
|
|
'percent_change': percent_change,
|
|
'detected_at': analysis.timestamp,
|
|
'previous_status': 'neutral', # Could be tracked
|
|
'recommendation': self._get_event_recommendation(analysis),
|
|
}
|
|
|
|
# Send to each recipient
|
|
for pref in preferences:
|
|
try:
|
|
email_notification = self.create_email_notification_record(
|
|
recipient=pref.email_address,
|
|
subject=f"🚨 Bitcoin {analysis.status.upper()} Alert: "
|
|
f"${float(analysis.current_price):.2f}",
|
|
notification_type='event'
|
|
)
|
|
|
|
self._send_single_email(
|
|
email_notification,
|
|
'event_alert.html',
|
|
context
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send event alert to {pref.email_address}: {e}")
|
|
|
|
logger.info(f"Event alert sent to {preferences.count()} recipients")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error sending event alert: {e}")
|
|
|
|
def send_system_alert(self, alert_title, alert_message, severity='warning',
|
|
affected_component='system', error_code=None):
|
|
"""Send system alert email."""
|
|
try:
|
|
# Get recipients who want system alerts
|
|
preferences = self.get_notification_preferences().filter(
|
|
receive_system_alerts=True
|
|
)
|
|
|
|
if not preferences.exists():
|
|
logger.info("No recipients for system alerts")
|
|
return
|
|
|
|
# Prepare context
|
|
context = {
|
|
'alert_title': alert_title,
|
|
'alert_message': alert_message,
|
|
'severity': severity,
|
|
'affected_component': affected_component,
|
|
'error_code': error_code,
|
|
'occurred_at': datetime.now(timezone.utc),
|
|
'troubleshooting_steps': self._get_troubleshooting_steps(severity),
|
|
}
|
|
|
|
# Send to each recipient
|
|
for pref in preferences:
|
|
try:
|
|
email_notification = self.create_email_notification_record(
|
|
recipient=pref.email_address,
|
|
subject=f"⚠️ Bitcoin Monitor System Alert: {alert_title}",
|
|
notification_type='system'
|
|
)
|
|
|
|
self._send_single_email(
|
|
email_notification,
|
|
'system_alert.html',
|
|
context
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send system alert to {pref.email_address}: {e}")
|
|
|
|
logger.info(f"System alert sent to {preferences.count()} recipients")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error sending system alert: {e}")
|
|
|
|
def send_daily_digest(self):
|
|
"""Send daily digest email."""
|
|
try:
|
|
# Get recipients who want daily digest
|
|
preferences = self.get_notification_preferences().filter(
|
|
receive_daily_digest=True
|
|
)
|
|
|
|
if not preferences.exists():
|
|
logger.info("No recipients for daily digest")
|
|
return
|
|
|
|
# Get data for the past 24 hours
|
|
yesterday = datetime.now(timezone.utc) - timedelta(hours=24)
|
|
|
|
from monitor.models import BitcoinPrice, MarketAnalysis, SystemStatus
|
|
from django.db.models import Max, Min
|
|
|
|
# Get price stats
|
|
prices = BitcoinPrice.objects.filter(
|
|
timestamp__gte=yesterday
|
|
)
|
|
|
|
if prices.exists():
|
|
current_price = prices.latest('timestamp').price_usd
|
|
daily_high = prices.aggregate(Max('price_usd'))['price_usd__max']
|
|
daily_low = prices.aggregate(Min('price_usd'))['price_usd__min']
|
|
daily_change = ((float(daily_high) - float(daily_low)) / float(daily_low) * 100)
|
|
else:
|
|
current_price = daily_high = daily_low = 0
|
|
daily_change = 0
|
|
|
|
# Get market status
|
|
latest_analysis = MarketAnalysis.objects.filter(
|
|
period='hourly'
|
|
).order_by('-timestamp').first()
|
|
market_status = latest_analysis.status if latest_analysis else 'neutral'
|
|
|
|
# Get event count
|
|
events_count = MarketAnalysis.objects.filter(
|
|
is_event=True,
|
|
timestamp__gte=yesterday
|
|
).count()
|
|
|
|
# Get system stats
|
|
price_fetches = prices.count()
|
|
successful_fetches = SystemStatus.objects.filter(
|
|
is_healthy=True,
|
|
timestamp__gte=yesterday
|
|
).count()
|
|
total_checks = SystemStatus.objects.filter(
|
|
timestamp__gte=yesterday
|
|
).count()
|
|
uptime_percentage = (successful_fetches / total_checks * 100) if total_checks > 0 else 0
|
|
|
|
# Get today's events
|
|
events_today = []
|
|
for event in MarketAnalysis.objects.filter(
|
|
is_event=True,
|
|
timestamp__gte=yesterday
|
|
).order_by('-timestamp')[:5]:
|
|
events_today.append({
|
|
'type': event.event_type,
|
|
'price': float(event.current_price),
|
|
'time': event.timestamp,
|
|
})
|
|
|
|
# Prepare context
|
|
context = {
|
|
'date': datetime.now(timezone.utc),
|
|
'summary_period': 'Last 24 hours',
|
|
'market_status': market_status,
|
|
'current_price': float(current_price),
|
|
'daily_high': float(daily_high) if daily_high else 0,
|
|
'daily_low': float(daily_low) if daily_low else 0,
|
|
'daily_change': daily_change,
|
|
'events_count': events_count,
|
|
'price_fetches': price_fetches,
|
|
'uptime_percentage': uptime_percentage,
|
|
'events_today': events_today,
|
|
'market_insight': self._get_daily_market_insight(market_status),
|
|
}
|
|
|
|
# Send to each recipient
|
|
for pref in preferences:
|
|
try:
|
|
email_notification = self.create_email_notification_record(
|
|
recipient=pref.email_address,
|
|
subject=f"📊 Bitcoin Daily Digest - {datetime.now(timezone.utc).strftime('%b %d, %Y')}",
|
|
notification_type='digest'
|
|
)
|
|
|
|
self._send_single_email(
|
|
email_notification,
|
|
'daily_digest.html',
|
|
context
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send daily digest to {pref.email_address}: {e}")
|
|
|
|
logger.info(f"Daily digest sent to {preferences.count()} recipients")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error sending daily digest: {e}")
|
|
# Try to send system alert about the failure
|
|
self.send_system_alert(
|
|
alert_title="Daily Digest Failed",
|
|
alert_message=f"Failed to send daily digest: {str(e)}",
|
|
severity='error',
|
|
affected_component='email_service'
|
|
)
|
|
|
|
def send_test_email(self, recipient_email):
|
|
"""Send a test email to verify configuration."""
|
|
try:
|
|
email_notification = self.create_email_notification_record(
|
|
recipient=recipient_email,
|
|
subject="✅ Bitcoin Monitor - Test Email",
|
|
notification_type='test'
|
|
)
|
|
|
|
context = {
|
|
'test_message': 'This is a test email from Bitcoin Monitor.',
|
|
'timestamp': datetime.now(timezone.utc),
|
|
}
|
|
|
|
self._send_single_email(
|
|
email_notification,
|
|
'system_alert.html', # Reuse system template for test
|
|
context
|
|
)
|
|
|
|
return True, "Test email sent successfully"
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error sending test email: {e}")
|
|
return False, str(e)
|
|
|
|
def _send_single_email(self, email_notification, template_name, context):
|
|
"""Send a single email and update notification record."""
|
|
try:
|
|
# Render email content
|
|
html_content, text_content = self.render_email_template(template_name, context)
|
|
|
|
# Create email
|
|
email = EmailMultiAlternatives(
|
|
subject=email_notification.subject,
|
|
body=text_content,
|
|
from_email=self.from_email,
|
|
to=[email_notification.recipient],
|
|
)
|
|
email.attach_alternative(html_content, "text/html")
|
|
|
|
# Send email
|
|
email.send()
|
|
|
|
# Update notification record
|
|
email_notification.content_html = html_content
|
|
email_notification.content_text = text_content
|
|
self.update_notification_status(email_notification, 'sent')
|
|
|
|
logger.info(f"Email sent to {email_notification.recipient}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send email to {email_notification.recipient}: {e}")
|
|
email_notification.retry_count += 1
|
|
|
|
if email_notification.retry_count < 3:
|
|
self.update_notification_status(
|
|
email_notification,
|
|
'retrying',
|
|
f"Retry {email_notification.retry_count}/3: {str(e)}"
|
|
)
|
|
else:
|
|
self.update_notification_status(
|
|
email_notification,
|
|
'failed',
|
|
f"Failed after 3 retries: {str(e)}"
|
|
)
|
|
|
|
raise
|
|
|
|
def _get_event_recommendation(self, analysis):
|
|
"""Get recommendation based on event type."""
|
|
if analysis.status == 'dip':
|
|
return "Price is significantly below yearly average. Consider buying opportunity."
|
|
elif analysis.status == 'peak':
|
|
return "Price is significantly above yearly average. Consider taking profits."
|
|
else:
|
|
return "Price has returned to normal range. Monitor for further changes."
|
|
|
|
def _get_troubleshooting_steps(self, severity):
|
|
"""Get troubleshooting steps based on severity."""
|
|
steps = [
|
|
"Check the admin panel for detailed error logs",
|
|
"Verify internet connection and API access",
|
|
"Check Redis and Celery worker status",
|
|
]
|
|
|
|
if severity == 'critical':
|
|
steps.append("Restart the monitoring services if necessary")
|
|
steps.append("Check database connection and disk space")
|
|
|
|
return steps
|
|
|
|
def _get_daily_market_insight(self, market_status):
|
|
"""Get daily market insight based on status."""
|
|
insights = {
|
|
'dip': "Market shows buying opportunity with prices below yearly average. "
|
|
"Monitor for further dips before entering.",
|
|
'peak': "Market at elevated levels. Consider profit-taking strategies "
|
|
"and set stop-loss orders.",
|
|
'neutral': "Market trading within normal range. Good time to review "
|
|
"portfolio and set alerts for future movements.",
|
|
}
|
|
return insights.get(market_status, "Monitor market for emerging trends.")
|