"""Reactions and read-state handling for the kommunikation plugin.""" from __future__ import annotations import logging import uuid from datetime import UTC, datetime from typing import Any from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from app.core.event_bus import get_event_bus from app.plugins.builtins.kommunikation.models import ( CommMessage, CommMessageReaction, CommMessageRead, ) logger = logging.getLogger(__name__) # ─── interactions ─── async def add_reaction( db: AsyncSession, tenant_id: uuid.UUID, message_id: uuid.UUID, user_id: uuid.UUID, emoji: str, ) -> dict[str, Any] | None: """Add an emoji reaction to a message.""" existing = await db.execute( select(CommMessageReaction).where( CommMessageReaction.message_id == message_id, CommMessageReaction.user_id == user_id, CommMessageReaction.emoji == emoji, ) ) if existing.scalar_one_or_none() is not None: return None # Already reacted r = CommMessageReaction( tenant_id=tenant_id, message_id=message_id, user_id=user_id, emoji=emoji, ) db.add(r) await db.flush() event_bus = get_event_bus() await event_bus.publish("reaction.added", { "message_id": str(message_id), "emoji": emoji, "user_id": str(user_id), }) return { "id": str(r.id), "message_id": str(r.message_id), "user_id": str(r.user_id), "emoji": r.emoji, } async def remove_reaction( db: AsyncSession, message_id: uuid.UUID, user_id: uuid.UUID, emoji: str, ) -> bool: """Remove an emoji reaction.""" result = await db.execute( select(CommMessageReaction).where( CommMessageReaction.message_id == message_id, CommMessageReaction.user_id == user_id, CommMessageReaction.emoji == emoji, ) ) r = result.scalar_one_or_none() if r is None: return False await db.delete(r) await db.flush() return True # ─── Read State ─── async def mark_read( db: AsyncSession, tenant_id: uuid.UUID, conversation_id: uuid.UUID, user_id: uuid.UUID, last_read_msg_id: str | None = None, ) -> bool: """Mark conversation as read up to a message.""" result = await db.execute( select(CommMessageRead).where( CommMessageRead.conversation_id == conversation_id, CommMessageRead.user_id == user_id, ) ) read = result.scalar_one_or_none() msg_id = uuid.UUID(last_read_msg_id) if last_read_msg_id else None if read is None: read = CommMessageRead( tenant_id=tenant_id, conversation_id=conversation_id, user_id=user_id, last_read_msg_id=msg_id, ) db.add(read) else: read.last_read_msg_id = msg_id read.last_read_at = datetime.now(UTC) await db.flush() return True async def _get_unread_count( db: AsyncSession, tenant_id: uuid.UUID, conversation_id: uuid.UUID, user_id: uuid.UUID, ) -> int: """Get unread message count for a user in a conversation.""" # Get last read message read_result = await db.execute( select(CommMessageRead).where( CommMessageRead.conversation_id == conversation_id, CommMessageRead.user_id == user_id, ) ) read = read_result.scalar_one_or_none() query = select(func.count()).select_from(CommMessage).where( CommMessage.conversation_id == conversation_id, CommMessage.tenant_id == tenant_id, CommMessage.deleted_at.is_(None), CommMessage.sender_type != "system", # Don't count system messages? Or count all? ) if read and read.last_read_at: query = query.where(CommMessage.created_at > read.last_read_at) result = await db.execute(query) return result.scalar() or 0 # ─── Plugin Room Creation ───