"""System Notification plugin — converts system events into chat messages.""" from __future__ import annotations import logging from typing import Any from app.plugins.base import BasePlugin from app.plugins.manifest import PluginManifest logger = logging.getLogger(__name__) class SystemNotifPlugin(BasePlugin): """System notifications as a participant in the kommunikation plugin.""" manifest = PluginManifest( name="system_notif", version="1.0.0", display_name="System Benachrichtigungen", description=( "Wandelt System-Events in Chat-Nachrichten um. " "Registriert sich als 'system' Teilnehmer am kommunikation Plugin." ), dependencies=["kommunikation"], routes=[], events=[ "lead.created", "contact.created", "contact.updated", "task.overdue", "task.created", "mail.received", "user.created", "workflow.completed", "notification.created", ], migrations=[], permissions=["system_notif:read"], is_core=False, ) def __init__(self) -> None: super().__init__() self._system_handler = None async def on_activate(self, db, service_container, event_bus) -> None: """Register as system participant and subscribe to events.""" await super().on_activate(db, service_container, event_bus) from app.plugins.builtins.system_notif.participant_handler import SystemParticipantHandler from app.plugins.builtins.kommunikation.participant_registry import get_participant_registry self._system_handler = SystemParticipantHandler(service_container) registry = get_participant_registry() registry.register("system", self._system_handler) logger.info("System notification plugin activated — registered as 'system' participant") async def on_deactivate(self, db, service_container, event_bus) -> None: """Unregister participant.""" from app.plugins.builtins.kommunikation.participant_registry import get_participant_registry get_participant_registry().unregister("system") self._system_handler = None await super().on_deactivate(db, service_container, event_bus) logger.info("System notification plugin deactivated") # ─── Event Handlers ─── async def on_lead_created(self, payload: dict[str, Any]) -> None: """Handle lead.created event → system message.""" await self._create_system_notification(payload, event_type="lead.created") async def on_contact_created(self, payload: dict[str, Any]) -> None: """Handle contact.created event → system message.""" await self._create_system_notification(payload, event_type="contact.created") async def on_contact_updated(self, payload: dict[str, Any]) -> None: """Handle contact.updated event → system message.""" await self._create_system_notification(payload, event_type="contact.updated") async def on_task_overdue(self, payload: dict[str, Any]) -> None: """Handle task.overdue event → system message (warning).""" await self._create_system_notification(payload, event_type="task.overdue", severity="warning") async def on_task_created(self, payload: dict[str, Any]) -> None: """Handle task.created event → system message.""" await self._create_system_notification(payload, event_type="task.created") async def on_mail_received(self, payload: dict[str, Any]) -> None: """Handle mail.received event → system message.""" await self._create_system_notification(payload, event_type="mail.received") async def on_user_created(self, payload: dict[str, Any]) -> None: """Handle user.created event → system message.""" await self._create_system_notification(payload, event_type="user.created") async def on_workflow_completed(self, payload: dict[str, Any]) -> None: """Handle workflow.completed event → system message.""" await self._create_system_notification(payload, event_type="workflow.completed") async def on_notification_created(self, payload: dict[str, Any]) -> None: """Handle notification.created event → system message. This is the bridge from the core notification system to the kommunikation plugin. Core code publishes 'notification.created' on the EventBus, this plugin converts it into a comm_message in the user's System room. """ await self._create_system_notification(payload, event_type="notification.created") async def _create_system_notification( self, payload: dict[str, Any], event_type: str, severity: str = "info", ) -> None: """Create a system notification message in the user's System room.""" import uuid from app.core.db import create_db_session from app.plugins.builtins.kommunikation.services import create_plugin_room, send_message tenant_id_str = payload.get("tenant_id") user_id_str = payload.get("user_id") if not tenant_id_str or not user_id_str: logger.warning("System notification missing tenant_id or user_id in payload: %s", payload) return try: tenant_id = uuid.UUID(tenant_id_str) user_id = uuid.UUID(user_id_str) except (ValueError, TypeError): logger.warning("System notification invalid UUIDs: tenant=%s user=%s", tenant_id_str, user_id_str) return # Build notification content from payload title = payload.get("title", "") body = payload.get("body", "") action_url = payload.get("action_url", "") if not title: # Generate title from event type event_titles = { "lead.created": "Neuer Lead", "contact.created": "Neuer Kontakt", "contact.updated": "Kontakt aktualisiert", "task.overdue": "Aufgabe überfällig", "task.created": "Neue Aufgabe", "mail.received": "Neue E-Mail", "user.created": "Neuer Benutzer", "workflow.completed": "Workflow abgeschlossen", "notification.created": "Benachrichtigung", } title = event_titles.get(event_type, event_type) content = f"**{title}**" if body: content += f"\n{body}" # Build action_card block if action_url is present blocks = [] if action_url: blocks.append({ "block_type": "action_card", "block_data": { "title": title, "body": body or "", "actions": [ {"label": "Öffnen", "action": action_url, "type": "primary"}, {"label": "Archivieren", "action": "dismiss", "type": "secondary"}, ], }, }) async with create_db_session(tenant_id) as db: # Ensure System room exists for this user await create_plugin_room( db, tenant_id, user_id, plugin_name="system_notif", title="System", participant_type="system", user_role="reader", ) # Find the System room conversation from sqlalchemy import select from app.plugins.builtins.kommunikation.models import CommConversation, CommParticipant result = await db.execute( select(CommConversation).where( CommConversation.tenant_id == tenant_id, CommConversation.title == "System", CommConversation.is_locked == True, CommConversation.locked_by == "system_notif", CommConversation.deleted_at.is_(None), ).join(CommParticipant, CommParticipant.conversation_id == CommConversation.id).where( CommParticipant.participant_id == user_id, CommParticipant.participant_type == "user", CommParticipant.left_at.is_(None), ) ) conv = result.scalar_one_or_none() if conv is None: logger.warning("Could not find System room for user %s", user_id) return # Send the system message await send_message( db, tenant_id, conv.id, sender_id=None, sender_type="system", content=content, content_format="markdown", blocks=blocks if blocks else None, metadata={"event_type": event_type, "severity": severity}, ) logger.info("System notification created: %s for user %s", event_type, user_id) def get_notification_types(self) -> list[dict[str, Any]]: return []