Files
gh-christianlouis-docuelevate/app/utils/notification.py
T
2026-02-06 22:33:16 +00:00

221 lines
7.6 KiB
Python

import apprise
import logging
from typing import List, Optional, Dict, Any, Union
from app.config import settings
logger = logging.getLogger(__name__)
# Global Apprise instance
_apprise = None
def init_apprise() -> apprise.Apprise:
"""Initialize the Apprise instance with configured notification services"""
global _apprise
if _apprise is None:
_apprise = apprise.Apprise()
# Add all configured notification services
if settings.notification_urls:
for url in settings.notification_urls:
try:
_apprise.add(url)
logger.info(f"Added notification service: {_mask_sensitive_url(url)}")
except Exception as e:
logger.error(f"Failed to add notification service: {str(e)}")
else:
logger.warning("No notification services configured")
return _apprise
def _mask_sensitive_url(url: str) -> str:
"""Mask sensitive parts of notification URLs for logging"""
# Simple masking for common URL formats with credentials
import re
# Match patterns like user:pass@host or token in URL parameters
masked = re.sub(r'://([^:]+):([^@]+)@', r'://\1:****@', url)
masked = re.sub(r'(discord://)[^/]+/[^/]+', r'\1webhook_id/****', masked)
masked = re.sub(r'(tgram://)[^/]+/[^/]+', r'\1bot_token/****', masked)
masked = re.sub(r'([?&](token|key|api_key|password|secret)=)([^&]+)', r'\1****', masked)
return masked
def send_notification(
title: str,
message: str,
notification_type: str = "info",
tags: Optional[List[str]] = None,
attachments: Optional[List[str]] = None,
data: Optional[Dict[str, Any]] = None
) -> bool:
"""
Send a notification through all configured channels
Args:
title: The notification title
message: The notification body message
notification_type: Type of notification (info, success, warning, failure)
tags: Optional list of tags for filtering notifications
attachments: Optional list of file paths to attach
data: Optional additional data for the notification
Returns:
bool: True if notification was sent successfully to at least one service
"""
if not settings.notification_urls:
logger.debug(f"Notification not sent (no services configured): {title}")
return False
try:
apprise_obj = init_apprise()
# Set notification type
notify_type = apprise.NotifyType.INFO
if notification_type == "success":
notify_type = apprise.NotifyType.SUCCESS
elif notification_type in ("warning", "warn"):
notify_type = apprise.NotifyType.WARNING
elif notification_type in ("failure", "error", "failed"):
notify_type = apprise.NotifyType.FAILURE
# Send the notification to each service individually for better error reporting
if not apprise_obj.servers: # Access servers as an attribute, not a method
logger.warning("No notification servers available despite having URLs configured")
return False
total_services = len(apprise_obj.servers)
successful_services = 0
for server in apprise_obj.servers: # Iterate through the list directly
try:
service_name = str(server).split("://")[0] if "://" in str(server) else str(server)
service_result = server.notify(
title=title,
body=message,
notify_type=notify_type,
attach=attachments
)
if service_result:
successful_services += 1
logger.debug(f"Notification sent via {service_name}")
else:
logger.warning(f"Failed to send notification via {service_name}")
except Exception as e:
logger.error(f"Error sending notification via {str(server)}: {str(e)}")
overall_result = successful_services > 0
if overall_result:
logger.debug(f"Notification sent: '{title}' (successful: {successful_services}/{total_services})")
else:
logger.warning(f"Failed to send notification to ALL services: '{title}' (0/{total_services})")
return overall_result
except Exception as e:
logger.exception(f"Error sending notification: {e}")
return False
def notify_celery_failure(task_name: str, task_id: str, exc: Exception, args: list, kwargs: dict) -> bool:
"""Send a notification about a failed Celery task"""
if not settings.notify_on_task_failure:
return False
title = f"Task Failed: {task_name}"
message = f"""
Task {task_name} ({task_id}) failed with error:
{type(exc).__name__}: {str(exc)}
Arguments: {args}
Keyword arguments: {kwargs}
"""
return send_notification(
title=title,
message=message,
notification_type="failure",
tags=["celery", "failure", task_name]
)
def notify_credential_failure(service_name: str, error: str) -> bool:
"""Send a notification about a credential failure"""
if not settings.notify_on_credential_failure:
return False
title = f"Credential Failure: {service_name}"
message = f"""
The credentials for {service_name} have failed:
{error}
Please check and update the credentials in the system settings.
"""
return send_notification(
title=title,
message=message,
notification_type="warning",
tags=["credentials", "warning", service_name]
)
def notify_startup() -> bool:
"""Send a notification that the application has started"""
if not settings.notify_on_startup:
return False
title = f"DocuElevate Started"
message = f"DocuElevate has been started successfully on {settings.external_hostname}"
return send_notification(
title=title,
message=message,
notification_type="success",
tags=["system", "startup"]
)
def notify_shutdown() -> bool:
"""Send a notification that the application is shutting down"""
if not settings.notify_on_shutdown:
return False
title = f"DocuElevate Shutting Down"
message = f"DocuElevate on {settings.external_hostname} is shutting down"
return send_notification(
title=title,
message=message,
notification_type="info",
tags=["system", "shutdown"]
)
def notify_file_processed(filename: str, file_size: int, metadata: dict, destinations: list) -> bool:
"""Send a notification that a file has been successfully processed"""
if not settings.notify_on_file_processed:
return False
# Format file size for display
size_mb = file_size / (1024 * 1024)
size_str = f"{size_mb:.2f} MB" if size_mb >= 1 else f"{file_size / 1024:.2f} KB"
# Extract key metadata fields
doc_type = metadata.get('document_type', 'Unknown')
tags = metadata.get('tags', [])
tags_str = ', '.join(tags) if tags else 'None'
# Format destinations
destinations_str = ', '.join(destinations) if destinations else 'None configured'
title = f"File Processed: {filename}"
message = f"""
File: {filename}
Size: {size_str}
Document Type: {doc_type}
Tags: {tags_str}
Destinations: {destinations_str}
The file has been successfully processed and is being uploaded to all configured destinations.
"""
return send_notification(
title=title,
message=message.strip(),
notification_type="success",
tags=["document", "processed", "success"]
)