# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Messaging service for sending messages across different channels. Provides subscription-based messaging functionality. """ import logging import re from typing import List, Optional, Callable from modules.datamodels.datamodelMessaging import ( MessagingSubscription, MessagingSubscriptionRegistration, MessagingDelivery, MessagingChannel, MessagingEventParameters, MessagingSendResult, MessagingSubscriptionExecutionResult, DeliveryStatus ) from modules.interfaces.interfaceMessaging import getInterface as getMessagingInterface from modules.shared.timeUtils import getUtcTimestamp logger = logging.getLogger(__name__) class MessagingService: """ Messaging service providing subscription-based messaging functionality. """ def __init__(self, services): """Initialize messaging service with service center access. Args: services: Service center instance providing access to interfaces """ self.services = services self._messagingInterface = None def sendMessage( self, subject: str, message: str, registration: MessagingSubscriptionRegistration ) -> MessagingSendResult: """ Sendet eine Nachricht über einen Channel an einen User. Erstellt MessagingDelivery Record. Args: subject: Subject der Nachricht (für E-Mail, leer für SMS) message: Nachrichtentext registration: MessagingSubscriptionRegistration mit Channel-Info und userId Returns: MessagingSendResult mit Status und Delivery-ID """ # Erstelle Delivery Record delivery = MessagingDelivery( subscriptionId=registration.subscriptionId, userId=registration.userId, channel=registration.channel, status=DeliveryStatus.PENDING ) # Speichere Delivery Record try: deliveryRecord = self.services.interfaceDbComponent.createDelivery(delivery) except Exception as e: logger.error(f"Failed to create delivery record: {str(e)}") return MessagingSendResult( success=False, errorMessage=f"Failed to create delivery record: {str(e)}" ) try: # Versende über interfaceMessaging success = self._getMessagingInterface().send( channel=registration.channel, recipient=registration.channelConfig, subject=subject, message=message ) if success: # Update Delivery Record self.services.interfaceDbComponent.updateDelivery( deliveryRecord["id"], { "status": DeliveryStatus.SENT, "sentAt": getUtcTimestamp() } ) return MessagingSendResult( success=True, deliveryId=deliveryRecord["id"] ) else: # Update Delivery Record mit Fehler self.services.interfaceDbComponent.updateDelivery( deliveryRecord["id"], { "status": DeliveryStatus.FAILED, "errorMessage": "Failed to send message" } ) return MessagingSendResult( success=False, deliveryId=deliveryRecord["id"], errorMessage="Failed to send message" ) except Exception as e: logger.error(f"Error sending message: {str(e)}") # Update Delivery Record mit Fehler try: self.services.interfaceDbComponent.updateDelivery( deliveryRecord["id"], { "status": DeliveryStatus.FAILED, "errorMessage": str(e) } ) except Exception as updateError: logger.error(f"Failed to update delivery record: {str(updateError)}") return MessagingSendResult( success=False, deliveryId=deliveryRecord["id"], errorMessage=str(e) ) def executeSubscription( self, subscriptionId: str, eventParameters: MessagingEventParameters ) -> MessagingSubscriptionExecutionResult: """ Führt eine Subscription-Funktion aus. Args: subscriptionId: ID der Subscription eventParameters: Parameter vom Trigger (als Pydantic Model) Returns: MessagingSubscriptionExecutionResult Raises: ValueError: Wenn Subscription nicht existiert oder nicht enabled ist FileNotFoundError: Wenn Subscription-Funktion nicht gefunden wird """ # Prüfe ob Subscription existiert und enabled ist subscription = self.services.interfaceDbComponent.getSubscription(subscriptionId) if not subscription: raise ValueError(f"Subscription {subscriptionId} not found") if not subscription.enabled: logger.warning(f"Subscription {subscriptionId} is disabled, skipping execution") return MessagingSubscriptionExecutionResult( success=False, messagesSent=0, errorMessage="Subscription is disabled" ) # Hole alle aktiven Registrierungen für diese Subscription registrations = self._getSubscribers(subscriptionId) if not registrations: logger.info(f"No active registrations for subscription {subscriptionId}") return MessagingSubscriptionExecutionResult( success=True, messagesSent=0 ) # Lade Subscription-Funktion dynamisch subscriptionFunction = self._loadSubscriptionFunction(subscriptionId) if not subscriptionFunction: errorMsg = f"Subscription function not found for {subscriptionId}" logger.error(errorMsg) raise FileNotFoundError(errorMsg) # Führe Funktion aus mit Registrierungen try: return subscriptionFunction.execute(eventParameters, registrations, self) except Exception as e: logger.error(f"Error executing subscription {subscriptionId}: {str(e)}", exc_info=True) return MessagingSubscriptionExecutionResult( success=False, messagesSent=0, errorMessage=str(e) ) def _getSubscribers( self, subscriptionId: str, channel: Optional[MessagingChannel] = None ) -> List[MessagingSubscriptionRegistration]: """Holt alle aktiven Subscriber einer Subscription""" filters = {"enabled": True} if channel: filters["channel"] = channel.value registrations = self.services.interfaceDbComponent.getAllRegistrations( subscriptionId=subscriptionId ) # Filter nach enabled und channel filteredRegistrations = [] for reg in registrations: if reg.enabled and (not channel or reg.channel == channel): filteredRegistrations.append(reg) return filteredRegistrations def _loadSubscriptionFunction(self, subscriptionId: str) -> Optional[Callable]: """ Lädt die Subscription-Funktion dynamisch. Returns: Callable mit execute-Methode oder None wenn nicht gefunden Note: subscriptionId wird direkt als Dateiname verwendet (z.B. "SystemErrors" -> subSubscriptionSystemErrors.py) """ # Format: subSubscription{subscriptionId}.py functionName = f"subSubscription{subscriptionId}" moduleName = f"modules.services.serviceMessaging.subscriptions.{functionName}" try: # Dynamisches Import import importlib subscriptionModule = importlib.import_module(moduleName) return subscriptionModule except ImportError: # Funktion existiert noch nicht - das ist OK logger.debug(f"Subscription function {moduleName} not found (this is OK if not yet implemented)") return None def _getMessagingInterface(self): """Holt das Messaging-Interface (interfaceMessaging)""" if not self._messagingInterface: self._messagingInterface = getMessagingInterface() return self._messagingInterface