From e7242be7638734c55c8ff1df3ce64f2a41de6a52 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Thu, 29 Jan 2026 20:35:01 +0200 Subject: [PATCH] refactor: update public_chat_routes and schemas for snapshots --- .../app/routes/public_chat_routes.py | 89 ++++++++++--------- surfsense_backend/app/schemas/new_chat.py | 43 ++++----- 2 files changed, 71 insertions(+), 61 deletions(-) diff --git a/surfsense_backend/app/routes/public_chat_routes.py b/surfsense_backend/app/routes/public_chat_routes.py index 4676f2ad0..d79c4dea5 100644 --- a/surfsense_backend/app/routes/public_chat_routes.py +++ b/surfsense_backend/app/routes/public_chat_routes.py @@ -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.responses import StreamingResponse 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 ( - CloneInitResponse, + CloneResponse, PublicChatResponse, ) from app.services.public_chat_service import ( + clone_from_snapshot, get_public_chat, - get_thread_by_share_token, - get_user_default_search_space, + get_snapshot_podcast, ) from app.users import current_active_user @@ -28,57 +32,60 @@ async def read_public_chat( session: AsyncSession = Depends(get_async_session), ): """ - Get a public chat by share token. + Get a public chat snapshot by share token. No authentication required. - Returns sanitized content (citations stripped). + Returns immutable snapshot data (sanitized, citations stripped). """ return await get_public_chat(session, share_token) -@router.post("/{share_token}/clone", response_model=CloneInitResponse) -async def clone_public_chat_endpoint( +@router.post("/{share_token}/clone", response_model=CloneResponse) +async def clone_public_chat( share_token: str, session: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), ): """ - Initialize cloning a public chat to the user's account. - - Creates an empty thread with clone_pending=True. - Frontend should redirect to the new thread and call /complete-clone. + Clone a public chat snapshot to the user's account. + Single-phase clone: creates thread and copies messages in one request. 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: - raise HTTPException(status_code=400, detail="No search space found for user") + No authentication required - the share_token provides access. + 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( - title=source_thread.title, - archived=False, - visibility=ChatVisibility.PRIVATE, - search_space_id=target_search_space_id, - created_by_id=user.id, - public_share_enabled=False, - cloned_from_thread_id=source_thread.id, - cloned_at=datetime.now(UTC), - clone_pending=True, - ) - session.add(new_thread) - await session.commit() - await session.refresh(new_thread) - - return CloneInitResponse( - thread_id=new_thread.id, - search_space_id=target_search_space_id, - share_token=share_token, + if not podcast_info: + raise HTTPException(status_code=404, detail="Podcast not found") + + file_path = podcast_info.get("file_path") + + if not file_path or not os.path.isfile(file_path): + raise HTTPException(status_code=404, detail="Podcast audio file not found") + + def iterfile(): + with open(file_path, mode="rb") as file_like: + yield from file_like + + return StreamingResponse( + iterfile(), + media_type="audio/mpeg", + headers={ + "Accept-Ranges": "bytes", + "Content-Disposition": f"inline; filename={os.path.basename(file_path)}", + }, ) diff --git a/surfsense_backend/app/schemas/new_chat.py b/surfsense_backend/app/schemas/new_chat.py index ab6be9c9f..1c15c5f4d 100644 --- a/surfsense_backend/app/schemas/new_chat.py +++ b/surfsense_backend/app/schemas/new_chat.py @@ -95,9 +95,6 @@ class NewChatThreadRead(NewChatThreadBase, IDModel): search_space_id: int visibility: ChatVisibility created_by_id: UUID | None = None - public_share_enabled: bool = False - public_share_token: str | None = None - clone_pending: bool = False created_at: datetime updated_at: datetime @@ -137,7 +134,6 @@ class ThreadListItem(BaseModel): visibility: ChatVisibility created_by_id: UUID | None = None is_own_thread: bool = False - public_share_enabled: bool = False created_at: datetime = Field(alias="createdAt") updated_at: datetime = Field(alias="updatedAt") @@ -211,22 +207,33 @@ class RegenerateRequest(BaseModel): # ============================================================================= -# Public Sharing Schemas +# Public Chat Snapshot Schemas # ============================================================================= -class PublicShareToggleRequest(BaseModel): - """Request to enable/disable public sharing for a thread.""" +class SnapshotCreateResponse(BaseModel): + """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): - """Response after toggling public sharing.""" +class SnapshotInfo(BaseModel): + """Info about a single snapshot.""" - enabled: bool - public_url: str | None = None - share_token: str | None = None + id: int + share_token: str + 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] -class CloneInitResponse(BaseModel): +class CloneResponse(BaseModel): + """Response after cloning a public snapshot.""" + thread_id: int search_space_id: int - share_token: str - - -class CompleteCloneResponse(BaseModel): - status: str - message_count: int