refactor(kommunikation): split god-object services.py into 6 focused sub-modules with re-export facade — behavior identical (comm suite 132P/1F/6E pre-existing, failures byte-identical to pre-split baseline)
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
"""Plugin room creation and system channels for the kommunikation plugin."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import and_, select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.event_bus import get_event_bus
|
||||
from app.plugins.builtins.kommunikation.conversations import get_conversation
|
||||
from app.plugins.builtins.kommunikation.models import (
|
||||
CommConversation,
|
||||
CommConversationPin,
|
||||
CommMessage,
|
||||
CommMessageBlock,
|
||||
CommParticipant,
|
||||
)
|
||||
from app.plugins.builtins.kommunikation.serializers import conversation_to_response
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
# ─── plugin_rooms ───
|
||||
|
||||
|
||||
|
||||
async def find_locked_room_id(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
plugin_name: str,
|
||||
title: str,
|
||||
) -> uuid.UUID | None:
|
||||
"""Find the conversation ID of a locked plugin room by tenant and title.
|
||||
|
||||
Matches the same room semantics as ``create_plugin_room``: locked rooms
|
||||
are owned by the plugin (``locked_by == plugin_name``) and soft-deleted
|
||||
conversations are excluded. Returns ``None`` when no room exists.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(CommConversation.id).where(
|
||||
CommConversation.tenant_id == tenant_id,
|
||||
CommConversation.title == title,
|
||||
CommConversation.is_locked.is_(True),
|
||||
CommConversation.locked_by == plugin_name,
|
||||
CommConversation.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def create_plugin_room(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
plugin_name: str,
|
||||
title: str,
|
||||
participant_type: str,
|
||||
user_role: str = "member",
|
||||
) -> dict[str, Any]:
|
||||
"""Create a locked, pinned room for a plugin (System, Live KI, Assistent).
|
||||
|
||||
The room is locked (users can't change title/participants) and pinned for the user.
|
||||
"""
|
||||
# Check if room already exists for this user + plugin
|
||||
result = await db.execute(
|
||||
select(CommConversation).where(
|
||||
CommConversation.tenant_id == tenant_id,
|
||||
CommConversation.title == title,
|
||||
CommConversation.is_locked.is_(True),
|
||||
CommConversation.locked_by == plugin_name,
|
||||
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),
|
||||
)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
if existing:
|
||||
# Already exists — return it
|
||||
return await get_conversation(db, tenant_id, existing.id, user_id) or {}
|
||||
|
||||
# Create conversation
|
||||
conv = CommConversation(
|
||||
tenant_id=tenant_id,
|
||||
title=title,
|
||||
owner_id=user_id,
|
||||
is_locked=True,
|
||||
locked_by=plugin_name,
|
||||
is_direct=False,
|
||||
created_by=None,
|
||||
created_by_type="plugin",
|
||||
metadata_={"plugin": plugin_name},
|
||||
)
|
||||
db.add(conv)
|
||||
await db.flush()
|
||||
|
||||
# Add plugin as participant
|
||||
plugin_p = CommParticipant(
|
||||
tenant_id=tenant_id,
|
||||
conversation_id=conv.id,
|
||||
participant_id=None,
|
||||
participant_type=participant_type,
|
||||
role="admin",
|
||||
display_name=title,
|
||||
)
|
||||
db.add(plugin_p)
|
||||
|
||||
# Add user as participant
|
||||
user_p = CommParticipant(
|
||||
tenant_id=tenant_id,
|
||||
conversation_id=conv.id,
|
||||
participant_id=user_id,
|
||||
participant_type="user",
|
||||
role=user_role,
|
||||
)
|
||||
db.add(user_p)
|
||||
|
||||
# Pin for user
|
||||
pin = CommConversationPin(
|
||||
tenant_id=tenant_id,
|
||||
conversation_id=conv.id,
|
||||
user_id=user_id,
|
||||
)
|
||||
db.add(pin)
|
||||
|
||||
await db.flush()
|
||||
|
||||
# Publish event
|
||||
event_bus = get_event_bus()
|
||||
await event_bus.publish("conversation.created", {
|
||||
"conversation_id": str(conv.id),
|
||||
"tenant_id": str(tenant_id),
|
||||
"created_by_type": "plugin",
|
||||
"plugin_name": plugin_name,
|
||||
})
|
||||
|
||||
parts = [plugin_p, user_p]
|
||||
return conversation_to_response(conv, parts, is_pinned_by_user=True)
|
||||
|
||||
|
||||
|
||||
# ─── System Channel ───
|
||||
|
||||
|
||||
async def get_or_create_system_channel(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
) -> CommConversation:
|
||||
"""Get or create the tenant-wide system channel.
|
||||
|
||||
The system channel is a locked, is_system=True conversation that serves as
|
||||
the central destination for system notifications, user alerts, and agent messages.
|
||||
All users of the tenant are automatically added as participants.
|
||||
"""
|
||||
result = await db.execute(
|
||||
select(CommConversation).where(
|
||||
CommConversation.tenant_id == tenant_id,
|
||||
CommConversation.is_system.is_(True),
|
||||
CommConversation.deleted_at.is_(None),
|
||||
)
|
||||
)
|
||||
conv = result.scalar_one_or_none()
|
||||
if conv is not None:
|
||||
return conv
|
||||
|
||||
# Create the system channel
|
||||
conv = CommConversation(
|
||||
tenant_id=tenant_id,
|
||||
title="System Channel",
|
||||
is_pinned=False,
|
||||
is_locked=True,
|
||||
is_direct=False,
|
||||
is_archived=False,
|
||||
is_system=True,
|
||||
created_by=None,
|
||||
created_by_type="system",
|
||||
metadata_={},
|
||||
)
|
||||
db.add(conv)
|
||||
await db.flush()
|
||||
|
||||
# Add all tenant users as participants
|
||||
from app.models.user import User, UserTenant
|
||||
|
||||
users_result = await db.execute(
|
||||
select(User.id)
|
||||
.join(UserTenant, UserTenant.user_id == User.id)
|
||||
.where(UserTenant.tenant_id == tenant_id)
|
||||
)
|
||||
user_ids = [row[0] for row in users_result.all()]
|
||||
|
||||
for uid in user_ids:
|
||||
p = CommParticipant(
|
||||
tenant_id=tenant_id,
|
||||
conversation_id=conv.id,
|
||||
participant_id=uid,
|
||||
participant_type="user",
|
||||
role="member",
|
||||
)
|
||||
db.add(p)
|
||||
|
||||
await db.flush()
|
||||
return conv
|
||||
|
||||
|
||||
async def post_system_message(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
user_id: uuid.UUID,
|
||||
message_type: str,
|
||||
title: str,
|
||||
body: str | None = None,
|
||||
entity_type: str | None = None,
|
||||
entity_id: uuid.UUID | None = None,
|
||||
severity: str = "info",
|
||||
) -> CommMessage | None:
|
||||
"""Post a typed system message to the tenant system channel.
|
||||
|
||||
Creates a CommMessage in the system channel with:
|
||||
- A text block containing title + body
|
||||
- An action_card block with deep-link if entity_type/entity_id is set
|
||||
- Block/message metadata: notification_type, severity, entity_ref
|
||||
|
||||
Returns the created CommMessage, or None if the user has muted this type.
|
||||
"""
|
||||
# Check user preferences — reuse the notification preference system
|
||||
from app.models.notification import NotificationPreference, NotificationType
|
||||
|
||||
pref = await db.execute(
|
||||
select(NotificationPreference).where(
|
||||
and_(
|
||||
NotificationPreference.user_id == user_id,
|
||||
NotificationPreference.type_key == message_type,
|
||||
NotificationPreference.tenant_id == tenant_id,
|
||||
)
|
||||
)
|
||||
)
|
||||
pref_row = pref.scalar_one_or_none()
|
||||
|
||||
if pref_row and not pref_row.is_enabled:
|
||||
return None
|
||||
|
||||
if not pref_row:
|
||||
type_def = await db.execute(
|
||||
select(NotificationType).where(NotificationType.type_key == message_type)
|
||||
)
|
||||
type_row = type_def.scalar_one_or_none()
|
||||
if type_row and not type_row.is_enabled_by_default:
|
||||
return None
|
||||
|
||||
# Get or create system channel
|
||||
conv = await get_or_create_system_channel(db, tenant_id)
|
||||
|
||||
# Build message content
|
||||
content = title
|
||||
if body:
|
||||
content = f"{title}\n{body}"
|
||||
|
||||
# Build metadata
|
||||
msg_metadata: dict[str, Any] = {
|
||||
"notification_type": message_type,
|
||||
"severity": severity,
|
||||
"target_user_id": str(user_id),
|
||||
}
|
||||
if entity_type and entity_id:
|
||||
msg_metadata["entity_ref"] = {
|
||||
"entity_type": entity_type,
|
||||
"entity_id": str(entity_id),
|
||||
}
|
||||
|
||||
# Build blocks
|
||||
blocks: list[dict[str, Any]] = [
|
||||
{
|
||||
"block_type": "text",
|
||||
"block_data": {"text": content, "title": title, "body": body or ""},
|
||||
}
|
||||
]
|
||||
if entity_type and entity_id:
|
||||
blocks.append(
|
||||
{
|
||||
"block_type": "action_card",
|
||||
"block_data": {
|
||||
"label": "Open",
|
||||
"entity_type": entity_type,
|
||||
"entity_id": str(entity_id),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Create message directly (not via send_message to avoid trigger_depth issues)
|
||||
msg = CommMessage(
|
||||
tenant_id=tenant_id,
|
||||
conversation_id=conv.id,
|
||||
sender_id=None,
|
||||
sender_type="system",
|
||||
content=content,
|
||||
content_format="text",
|
||||
metadata_=msg_metadata,
|
||||
)
|
||||
db.add(msg)
|
||||
await db.flush()
|
||||
|
||||
# Create blocks
|
||||
for i, block in enumerate(blocks):
|
||||
b = CommMessageBlock(
|
||||
tenant_id=tenant_id,
|
||||
message_id=msg.id,
|
||||
block_type=block["block_type"],
|
||||
block_data=block["block_data"],
|
||||
sort_order=i,
|
||||
)
|
||||
db.add(b)
|
||||
|
||||
await db.flush()
|
||||
|
||||
# Update conversation last_msg
|
||||
|
||||
await db.execute(
|
||||
update(CommConversation)
|
||||
.where(CommConversation.id == conv.id)
|
||||
.values(
|
||||
last_msg_at=datetime.now(UTC),
|
||||
last_msg_preview=content[:200],
|
||||
last_msg_sender_type="system",
|
||||
)
|
||||
)
|
||||
|
||||
# Publish event
|
||||
event_bus = get_event_bus()
|
||||
await event_bus.publish("system.message.posted", {
|
||||
"conversation_id": str(conv.id),
|
||||
"message_id": str(msg.id),
|
||||
"tenant_id": str(tenant_id),
|
||||
"user_id": str(user_id),
|
||||
"message_type": message_type,
|
||||
"severity": severity,
|
||||
})
|
||||
|
||||
return msg
|
||||
Reference in New Issue
Block a user