SecureChat / src /chat.py
ausername-12345
fix GC [no key], camera-off avatar, timestamps, GC remove error handling
e0701ea
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
from pydantic import BaseModel
from typing import Optional, List
import json
from database import get_db
from models import User, Conversation, ConversationMember, Message, Group, GroupMember, GroupMessage
from auth import get_current_user_from_header, user_to_dict
router = APIRouter()
# ─── Users ───────────────────────────────────────────────────────────────────
@router.get("/users/search")
def search_users(q: str, request: Request, db: Session = Depends(get_db)):
me = get_current_user_from_header(request, db)
users = db.query(User).filter(
User.id != me.id,
User.username.ilike(f"%{q}%") if q else True
).limit(20).all()
return [user_to_dict(u) for u in users]
@router.get("/users/{user_id}/public_key")
def get_public_key(user_id: int, request: Request, db: Session = Depends(get_db)):
get_current_user_from_header(request, db)
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return {"public_key": user.public_key}
# ─── DM Conversations ─────────────────────────────────────────────────────────
@router.post("/conversations")
def get_or_create_conversation(target_user_id: int, request: Request, db: Session = Depends(get_db)):
me = get_current_user_from_header(request, db)
# Find existing conversation between the two users
my_convs = db.query(ConversationMember.conversation_id).filter(ConversationMember.user_id == me.id)
their_convs = db.query(ConversationMember.conversation_id).filter(ConversationMember.user_id == target_user_id)
common = my_convs.intersect(their_convs).all()
if common:
conv_id = common[0][0]
conv = db.query(Conversation).filter(Conversation.id == conv_id).first()
else:
conv = Conversation()
db.add(conv)
db.flush()
db.add(ConversationMember(conversation_id=conv.id, user_id=me.id))
db.add(ConversationMember(conversation_id=conv.id, user_id=target_user_id))
db.commit()
db.refresh(conv)
other = db.query(User).filter(User.id == target_user_id).first()
return {
"id": conv.id,
"other_user": user_to_dict(other),
"messages": [msg_to_dict(m, me.id) for m in conv.messages[-50:]]
}
@router.get("/conversations")
def list_conversations(request: Request, db: Session = Depends(get_db)):
me = get_current_user_from_header(request, db)
memberships = db.query(ConversationMember).filter(ConversationMember.user_id == me.id).all()
result = []
for m in memberships:
conv = m.conversation
other_member = next((x for x in conv.members if x.user_id != me.id), None)
if not other_member:
continue
other = other_member.user
last_msg = conv.messages[-1] if conv.messages else None
result.append({
"id": conv.id,
"other_user": user_to_dict(other),
"last_message_at": last_msg.created_at.isoformat() if last_msg and hasattr(last_msg.created_at, 'isoformat') else str(last_msg.created_at) if last_msg else None,
"unread_count": sum(1 for msg in conv.messages if not msg.is_read and msg.sender_id != me.id)
})
result.sort(key=lambda x: x["last_message_at"] or "", reverse=True)
return result
@router.delete("/conversations/{conversation_id}")
def delete_conversation(conversation_id: int, request: Request, db: Session = Depends(get_db)):
me = get_current_user_from_header(request, db)
membership = db.query(ConversationMember).filter(
ConversationMember.conversation_id == conversation_id,
ConversationMember.user_id == me.id
).first()
if not membership:
raise HTTPException(status_code=404, detail="Conversation not found")
db.delete(membership)
db.commit()
return {"ok": True}
# ─── Groups ───────────────────────────────────────────────────────────────────
class CreateGroupRequest(BaseModel):
name: str
description: Optional[str] = None
member_ids: List[int] = []
class AddMemberRequest(BaseModel):
user_id: int
@router.post("/groups")
def create_group(req: CreateGroupRequest, request: Request, db: Session = Depends(get_db)):
me = get_current_user_from_header(request, db)
group = Group(name=req.name, description=req.description, created_by=me.id)
db.add(group)
db.flush()
# Add creator as admin
db.add(GroupMember(group_id=group.id, user_id=me.id, role="admin"))
# Add other members
for uid in req.member_ids:
if uid != me.id:
db.add(GroupMember(group_id=group.id, user_id=uid))
db.commit()
db.refresh(group)
return group_to_dict(group, me.id)
@router.get("/groups")
def list_groups(request: Request, db: Session = Depends(get_db)):
me = get_current_user_from_header(request, db)
memberships = db.query(GroupMember).filter(GroupMember.user_id == me.id).all()
return [group_to_dict(m.group, me.id) for m in memberships]
@router.get("/groups/{group_id}")
def get_group(group_id: int, request: Request, db: Session = Depends(get_db)):
me = get_current_user_from_header(request, db)
group = db.query(Group).filter(Group.id == group_id).first()
if not group:
raise HTTPException(status_code=404, detail="Group not found")
membership = db.query(GroupMember).filter(
GroupMember.group_id == group_id,
GroupMember.user_id == me.id
).first()
if not membership:
raise HTTPException(status_code=403, detail="Not a member")
return {
**group_to_dict(group, me.id),
"messages": [group_msg_to_dict(m, me.id) for m in group.messages[-50:]],
"members": [{"user": user_to_dict(m.user), "role": m.role} for m in group.members]
}
@router.post("/groups/{group_id}/members")
def add_member(group_id: int, req: AddMemberRequest, request: Request, db: Session = Depends(get_db)):
me = get_current_user_from_header(request, db)
membership = db.query(GroupMember).filter(
GroupMember.group_id == group_id,
GroupMember.user_id == me.id
).first()
if not membership:
raise HTTPException(status_code=403, detail="Not a member")
existing = db.query(GroupMember).filter(
GroupMember.group_id == group_id,
GroupMember.user_id == req.user_id
).first()
if existing:
raise HTTPException(status_code=400, detail="Already a member")
db.add(GroupMember(group_id=group_id, user_id=req.user_id))
db.commit()
user = db.query(User).filter(User.id == req.user_id).first()
return {"ok": True, "user": user_to_dict(user)}
@router.delete("/groups/{group_id}/members/{user_id}")
def remove_member(group_id: int, user_id: int, request: Request, db: Session = Depends(get_db)):
me = get_current_user_from_header(request, db)
membership = db.query(GroupMember).filter(
GroupMember.group_id == group_id,
GroupMember.user_id == me.id
).first()
if not membership:
raise HTTPException(status_code=403, detail="Not a member")
m = db.query(GroupMember).filter(GroupMember.group_id == group_id, GroupMember.user_id == user_id).first()
if m:
db.delete(m)
db.commit()
return {"ok": True}
# ─── Helpers ──────────────────────────────────────────────────────────────────
def msg_to_dict(msg: Message, viewer_id: int) -> dict:
is_sender = msg.sender_id == viewer_id
return {
"id": msg.id,
"sender_id": msg.sender_id,
"sender_name": msg.sender.display_name if msg.sender else "Unknown",
"ciphertext": msg.ciphertext_for_sender if is_sender else msg.ciphertext_for_recipient,
"created_at": msg.created_at.isoformat() if hasattr(msg.created_at, 'isoformat') else str(msg.created_at),
"is_read": msg.is_read,
}
def group_msg_to_dict(msg: GroupMessage, viewer_id: int) -> dict:
keys = json.loads(msg.encrypted_aes_keys) if msg.encrypted_aes_keys else {}
return {
"id": msg.id,
"sender_id": msg.sender_id,
"sender_name": msg.sender.display_name if msg.sender else "Unknown",
"ciphertext": msg.ciphertext,
"iv": msg.iv,
"encrypted_aes_key": keys.get(str(viewer_id)),
"created_at": msg.created_at.isoformat() if hasattr(msg.created_at, 'isoformat') else str(msg.created_at),
}
def group_to_dict(group: Group, viewer_id: int) -> dict:
return {
"id": group.id,
"name": group.name,
"description": group.description,
"created_by": group.created_by,
"avatar_color": group.avatar_color,
"member_count": len(group.members),
"created_at": group.created_at.isoformat() if hasattr(group.created_at, 'isoformat') else str(group.created_at),
}