Files
comiaunicaty/backend/app/routers/chat.py

139 lines
6.8 KiB
Python

from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import desc, select
from sqlalchemy.orm import Session
from app.db.base import get_db
from app.models import Group, Member, Message, Thread
from app.schemas import MessageCreate, ThreadCreate
from app.services.auth import CurrentContext, audit, get_current_context, get_member_for_group, get_members_for_context
from app.services.serializers import group_dict, iso, member_dict
router = APIRouter(prefix="/api", tags=["chat"])
LOW_SIGNAL_PHRASES = {"ok", "okay", "yes", "no", "thanks", "thank you", "great", "👍", "+1", "fine", "done"}
def low_signal(body: str) -> bool:
normalized = body.strip().lower()
return len(normalized) <= 24 and (normalized in LOW_SIGNAL_PHRASES or normalized.replace("!", "") in LOW_SIGNAL_PHRASES)
def message_chat_dict(message: Message, members_by_id: dict[str, Member], current_member_id: str | None) -> dict:
author = members_by_id.get(message.author_member_id)
return {
"id": message.id,
"thread_id": message.thread_id,
"author_member_id": message.author_member_id,
"author_name": author.display_name if author else "Member",
"body": message.body,
"created_at": iso(message.created_at),
"mine": message.author_member_id == current_member_id,
"low_signal": low_signal(message.body),
}
def thread_chat_dict(db: Session, thread: Thread, group: Group, current_member_id: str | None, include_messages: bool = True) -> dict:
members = db.scalars(select(Member).where(Member.group_id == group.id)).all()
members_by_id = {member.id: member for member in members}
messages = []
latest_message = None
if include_messages:
rows = list(db.scalars(select(Message).where(Message.thread_id == thread.id).order_by(Message.created_at)).all())
messages = [message_chat_dict(message, members_by_id, current_member_id) for message in rows]
latest_message = messages[-1] if messages else None
else:
row = db.scalar(select(Message).where(Message.thread_id == thread.id).order_by(desc(Message.created_at)).limit(1))
latest_message = message_chat_dict(row, members_by_id, current_member_id) if row else None
return {
"id": thread.id,
"group_id": group.id,
"group_name": group.name,
"title": thread.title,
"kind": thread.kind,
"created_at": iso(thread.created_at),
"updated_at": iso(thread.updated_at),
"latest_message": latest_message,
"messages": messages,
}
@router.get("/chat")
def chat_home(
group_id: str | None = None,
thread_id: str | None = None,
ctx: CurrentContext = Depends(get_current_context),
db: Session = Depends(get_db),
) -> dict:
memberships = get_members_for_context(db, ctx)
if not memberships:
return {"groups": [], "threads": [], "active_group": None, "active_thread": None, "current_member_id": None}
groups = [db.get(Group, member.group_id) for member in memberships]
groups = [group for group in groups if group is not None and not group.archived_at]
active_group = next((group for group in groups if group.id == group_id), groups[0])
active_member = get_member_for_group(db, ctx, active_group.id)
threads = list(db.scalars(select(Thread).where(Thread.group_id == active_group.id).order_by(desc(Thread.updated_at))).all())
if not threads:
thread = Thread(group_id=active_group.id, title="General", kind="discussion", created_by_member_id=active_member.id)
db.add(thread)
db.flush()
db.commit()
threads = [thread]
active_thread = next((thread for thread in threads if thread.id == thread_id), threads[0])
members = list(db.scalars(select(Member).where(Member.group_id == active_group.id, Member.status.in_(["joined", "verified"]))).all())
return {
"groups": [
{"group": group_dict(group), "member": member_dict(next(member for member in memberships if member.group_id == group.id))}
for group in groups
],
"active_group": group_dict(active_group),
"active_member": member_dict(active_member) if active_member else None,
"members": [member_dict(member) for member in members],
"threads": [thread_chat_dict(db, thread, active_group, active_member.id if active_member else None, include_messages=False) for thread in threads],
"active_thread": thread_chat_dict(db, active_thread, active_group, active_member.id if active_member else None, include_messages=True),
"current_member_id": active_member.id if active_member else None,
}
@router.post("/chat/threads")
def create_chat_thread(payload: ThreadCreate, group_id: str, ctx: CurrentContext = Depends(get_current_context), db: Session = Depends(get_db)) -> dict:
group = db.get(Group, group_id)
if not group:
raise HTTPException(status_code=404, detail={"error": {"code": "not_found", "message": "Group not found.", "details": {}}})
member = get_member_for_group(db, ctx, group.id)
if not member:
raise HTTPException(status_code=403, detail={"error": {"code": "permission_denied", "message": "Join this group to continue.", "details": {}}})
thread = Thread(group_id=group.id, title=payload.title, kind=payload.kind, created_by_member_id=member.id)
db.add(thread)
audit(db, ctx=ctx, action="chat_thread_created", resource_type="thread", resource_id=thread.id)
db.commit()
return {"thread": thread_chat_dict(db, thread, group, member.id)}
@router.post("/chat/threads/{thread_id}/messages")
def create_chat_message(thread_id: str, payload: MessageCreate, ctx: CurrentContext = Depends(get_current_context), db: Session = Depends(get_db)) -> dict:
thread = db.get(Thread, thread_id)
if not thread:
raise HTTPException(status_code=404, detail={"error": {"code": "not_found", "message": "Thread not found.", "details": {}}})
group = db.get(Group, thread.group_id)
member = get_member_for_group(db, ctx, group.id)
if not member:
raise HTTPException(status_code=403, detail={"error": {"code": "permission_denied", "message": "Join this group to continue.", "details": {}}})
if thread.kind == "archive":
raise HTTPException(status_code=403, detail={"error": {"code": "archive_read_only", "message": "Imported archive threads are read-only.", "details": {}}})
from app.core.security import utc_now
now = utc_now()
message = Message(thread_id=thread.id, author_member_id=member.id, body=payload.body, created_at=now)
thread.updated_at = now
db.add(message)
audit(db, ctx=ctx, action="chat_message_created", resource_type="thread", resource_id=thread.id)
db.commit()
members = {item.id: item for item in db.scalars(select(Member).where(Member.group_id == group.id)).all()}
return {"message": message_chat_dict(message, members, member.id)}