Files
leocrm/app/plugins/builtins/kommunikation/interactions.py
T

160 lines
4.0 KiB
Python

"""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 ───