inital commit, very early alpha stage
This commit is contained in:
138
backend/app/routers/chat.py
Normal file
138
backend/app/routers/chat.py
Normal file
@@ -0,0 +1,138 @@
|
||||
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)}
|
||||
Reference in New Issue
Block a user