fix(chat): support body and content fields in world and DM chat messages; feat(audio): add sound toggle switch and mute controls
checkpoint / checkpoint (push) Successful in 6m11s
checkpoint / checkpoint (push) Successful in 6m11s
This commit is contained in:
@@ -71,7 +71,7 @@ from datetime import datetime, timezone
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, computed_field, model_validator
|
||||
from sqlalchemy import and_, func, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -97,7 +97,33 @@ _BODY_MAX_LEN = 4000
|
||||
|
||||
|
||||
class SendDMRequest(BaseModel):
|
||||
body: str = Field(..., min_length=1, max_length=_BODY_MAX_LEN)
|
||||
body: str | None = Field(default=None, max_length=_BODY_MAX_LEN)
|
||||
content: str | None = Field(default=None, max_length=_BODY_MAX_LEN)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def resolve_body_or_content(cls, data: object) -> object:
|
||||
if isinstance(data, dict):
|
||||
if not data.get("body") and data.get("content"):
|
||||
data["body"] = data["content"]
|
||||
elif not data.get("content") and data.get("body"):
|
||||
data["content"] = data["body"]
|
||||
return data
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_non_empty(self) -> SendDMRequest:
|
||||
text = self.body or self.content
|
||||
if not text or len(text.strip()) < 1:
|
||||
raise ValueError("Message body or content must not be empty")
|
||||
if not self.body:
|
||||
self.body = text
|
||||
if not self.content:
|
||||
self.content = text
|
||||
return self
|
||||
|
||||
|
||||
class StartConversationRequest(BaseModel):
|
||||
target_user_id: int
|
||||
|
||||
|
||||
class DMMessageResponse(BaseModel):
|
||||
@@ -111,6 +137,11 @@ class DMMessageResponse(BaseModel):
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def content(self) -> str:
|
||||
return self.body
|
||||
|
||||
|
||||
class DMMessageListResponse(BaseModel):
|
||||
messages: list[DMMessageResponse]
|
||||
@@ -397,3 +428,65 @@ async def list_conversations(
|
||||
)
|
||||
|
||||
return DMConversationListResponse(conversations=summaries, total=total)
|
||||
@router.post("/conversations")
|
||||
async def start_or_get_conversation_endpoint(
|
||||
body: StartConversationRequest,
|
||||
current_user: CurrentUser,
|
||||
db: DbDep,
|
||||
):
|
||||
"""Start or get existing conversation with target_user_id."""
|
||||
if body.target_user_id == current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Cannot start a conversation with yourself"
|
||||
)
|
||||
await _get_target_user(db, body.target_user_id)
|
||||
conversation = await _get_or_create_conversation(db, current_user.id, body.target_user_id)
|
||||
return {"id": conversation.id, "target_user_id": body.target_user_id}
|
||||
|
||||
|
||||
@router.get("/conversations/{conversation_id}/messages", response_model=DMMessageListResponse)
|
||||
async def get_dm_conversation_history(
|
||||
conversation_id: int,
|
||||
current_user: CurrentUser,
|
||||
db: DbDep,
|
||||
limit: int = Query(50, ge=1, le=100),
|
||||
before: int | None = Query(None),
|
||||
):
|
||||
"""Get messages for a conversation by conversation_id."""
|
||||
conv_result = await db.execute(
|
||||
select(Conversation).where(
|
||||
Conversation.id == conversation_id,
|
||||
or_(Conversation.user_a_id == current_user.id, Conversation.user_b_id == current_user.id),
|
||||
)
|
||||
)
|
||||
conversation = conv_result.scalar_one_or_none()
|
||||
if conversation is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Conversation not found")
|
||||
peer_id = _peer_id(conversation, current_user.id)
|
||||
return await get_dm_history(peer_id, current_user, db, limit=limit, before=before)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/conversations/{conversation_id}/messages",
|
||||
response_model=DMMessageResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
dependencies=[Depends(chat_dm_send_rate_limit)],
|
||||
)
|
||||
async def send_dm_to_conversation(
|
||||
conversation_id: int,
|
||||
body: SendDMRequest,
|
||||
current_user: CurrentUser,
|
||||
db: DbDep,
|
||||
):
|
||||
"""Send a message to a conversation by conversation_id."""
|
||||
conv_result = await db.execute(
|
||||
select(Conversation).where(
|
||||
Conversation.id == conversation_id,
|
||||
or_(Conversation.user_a_id == current_user.id, Conversation.user_b_id == current_user.id),
|
||||
)
|
||||
)
|
||||
conversation = conv_result.scalar_one_or_none()
|
||||
if conversation is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Conversation not found")
|
||||
peer_id = _peer_id(conversation, current_user.id)
|
||||
return await send_dm(peer_id, body, current_user, db)
|
||||
|
||||
@@ -39,7 +39,7 @@ from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, computed_field, model_validator
|
||||
from sqlalchemy import and_, or_, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import joinedload
|
||||
@@ -68,9 +68,31 @@ _BODY_MAX_LEN = 4000
|
||||
|
||||
|
||||
class SendWorldMessageRequest(BaseModel):
|
||||
body: str = Field(..., min_length=1, max_length=_BODY_MAX_LEN)
|
||||
body: str | None = Field(default=None, max_length=_BODY_MAX_LEN)
|
||||
content: str | None = Field(default=None, max_length=_BODY_MAX_LEN)
|
||||
room_id: int | None = None
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def resolve_body_or_content(cls, data: object) -> object:
|
||||
if isinstance(data, dict):
|
||||
if not data.get("body") and data.get("content"):
|
||||
data["body"] = data["content"]
|
||||
elif not data.get("content") and data.get("body"):
|
||||
data["content"] = data["body"]
|
||||
return data
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_non_empty(self) -> SendWorldMessageRequest:
|
||||
text = self.body or self.content
|
||||
if not text or len(text.strip()) < 1:
|
||||
raise ValueError("Message body or content must not be empty")
|
||||
if not self.body:
|
||||
self.body = text
|
||||
if not self.content:
|
||||
self.content = text
|
||||
return self
|
||||
|
||||
|
||||
class AuthorInfo(BaseModel):
|
||||
id: int | None
|
||||
@@ -92,6 +114,11 @@ class WorldMessageResponse(BaseModel):
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@computed_field
|
||||
@property
|
||||
def content(self) -> str:
|
||||
return self.body
|
||||
|
||||
|
||||
class WorldMessageListResponse(BaseModel):
|
||||
messages: list[WorldMessageResponse]
|
||||
|
||||
Reference in New Issue
Block a user