refactor: update public_chat_routes and schemas for snapshots

This commit is contained in:
CREDO23 2026-01-29 20:35:01 +02:00
parent a45412abad
commit e7242be763
2 changed files with 71 additions and 61 deletions

View file

@ -1,21 +1,25 @@
""" """
Routes for public chat access (unauthenticated and mixed-auth endpoints). Routes for public chat access via immutable snapshots.
All public endpoints use share_token for access - no authentication required
for read operations. Clone requires authentication.
""" """
from datetime import UTC, datetime import os
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.db import ChatVisibility, NewChatThread, User, get_async_session from app.db import User, get_async_session
from app.schemas.new_chat import ( from app.schemas.new_chat import (
CloneInitResponse, CloneResponse,
PublicChatResponse, PublicChatResponse,
) )
from app.services.public_chat_service import ( from app.services.public_chat_service import (
clone_from_snapshot,
get_public_chat, get_public_chat,
get_thread_by_share_token, get_snapshot_podcast,
get_user_default_search_space,
) )
from app.users import current_active_user from app.users import current_active_user
@ -28,57 +32,60 @@ async def read_public_chat(
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
): ):
""" """
Get a public chat by share token. Get a public chat snapshot by share token.
No authentication required. No authentication required.
Returns sanitized content (citations stripped). Returns immutable snapshot data (sanitized, citations stripped).
""" """
return await get_public_chat(session, share_token) return await get_public_chat(session, share_token)
@router.post("/{share_token}/clone", response_model=CloneInitResponse) @router.post("/{share_token}/clone", response_model=CloneResponse)
async def clone_public_chat_endpoint( async def clone_public_chat(
share_token: str, share_token: str,
session: AsyncSession = Depends(get_async_session), session: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user), user: User = Depends(current_active_user),
): ):
""" """
Initialize cloning a public chat to the user's account. Clone a public chat snapshot to the user's account.
Creates an empty thread with clone_pending=True.
Frontend should redirect to the new thread and call /complete-clone.
Single-phase clone: creates thread and copies messages in one request.
Requires authentication. Requires authentication.
""" """
source_thread = await get_thread_by_share_token(session, share_token) return await clone_from_snapshot(session, share_token, user)
if not source_thread:
raise HTTPException(
status_code=404, detail="Chat not found or no longer public"
)
target_search_space_id = await get_user_default_search_space(session, user.id) @router.get("/{share_token}/podcasts/{podcast_id}/stream")
async def stream_public_podcast(
share_token: str,
podcast_id: int,
session: AsyncSession = Depends(get_async_session),
):
"""
Stream a podcast from a public chat snapshot.
if target_search_space_id is None: No authentication required - the share_token provides access.
raise HTTPException(status_code=400, detail="No search space found for user") Looks up podcast by original_id in the snapshot's podcasts array.
"""
podcast_info = await get_snapshot_podcast(session, share_token, podcast_id)
new_thread = NewChatThread( if not podcast_info:
title=source_thread.title, raise HTTPException(status_code=404, detail="Podcast not found")
archived=False,
visibility=ChatVisibility.PRIVATE, file_path = podcast_info.get("file_path")
search_space_id=target_search_space_id,
created_by_id=user.id, if not file_path or not os.path.isfile(file_path):
public_share_enabled=False, raise HTTPException(status_code=404, detail="Podcast audio file not found")
cloned_from_thread_id=source_thread.id,
cloned_at=datetime.now(UTC), def iterfile():
clone_pending=True, with open(file_path, mode="rb") as file_like:
) yield from file_like
session.add(new_thread)
await session.commit() return StreamingResponse(
await session.refresh(new_thread) iterfile(),
media_type="audio/mpeg",
return CloneInitResponse( headers={
thread_id=new_thread.id, "Accept-Ranges": "bytes",
search_space_id=target_search_space_id, "Content-Disposition": f"inline; filename={os.path.basename(file_path)}",
share_token=share_token, },
) )

View file

@ -95,9 +95,6 @@ class NewChatThreadRead(NewChatThreadBase, IDModel):
search_space_id: int search_space_id: int
visibility: ChatVisibility visibility: ChatVisibility
created_by_id: UUID | None = None created_by_id: UUID | None = None
public_share_enabled: bool = False
public_share_token: str | None = None
clone_pending: bool = False
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
@ -137,7 +134,6 @@ class ThreadListItem(BaseModel):
visibility: ChatVisibility visibility: ChatVisibility
created_by_id: UUID | None = None created_by_id: UUID | None = None
is_own_thread: bool = False is_own_thread: bool = False
public_share_enabled: bool = False
created_at: datetime = Field(alias="createdAt") created_at: datetime = Field(alias="createdAt")
updated_at: datetime = Field(alias="updatedAt") updated_at: datetime = Field(alias="updatedAt")
@ -211,22 +207,33 @@ class RegenerateRequest(BaseModel):
# ============================================================================= # =============================================================================
# Public Sharing Schemas # Public Chat Snapshot Schemas
# ============================================================================= # =============================================================================
class PublicShareToggleRequest(BaseModel): class SnapshotCreateResponse(BaseModel):
"""Request to enable/disable public sharing for a thread.""" """Response after creating a public snapshot."""
enabled: bool snapshot_id: int
share_token: str
public_url: str
is_new: bool # False if existing snapshot returned (same content)
class PublicShareToggleResponse(BaseModel): class SnapshotInfo(BaseModel):
"""Response after toggling public sharing.""" """Info about a single snapshot."""
enabled: bool id: int
public_url: str | None = None share_token: str
share_token: str | None = None public_url: str
created_at: datetime
message_count: int
class SnapshotListResponse(BaseModel):
"""List of snapshots for a thread."""
snapshots: list[SnapshotInfo]
# ============================================================================= # =============================================================================
@ -256,12 +263,8 @@ class PublicChatResponse(BaseModel):
messages: list[PublicChatMessage] messages: list[PublicChatMessage]
class CloneInitResponse(BaseModel): class CloneResponse(BaseModel):
"""Response after cloning a public snapshot."""
thread_id: int thread_id: int
search_space_id: int search_space_id: int
share_token: str
class CompleteCloneResponse(BaseModel):
status: str
message_count: int