Spaces:
Running
Running
| """Author RAG β Public Chat API (Vanity URL Support). | |
| Serves data needed by the Next.js vanity chat pages: | |
| chat.yourdomain.com/jane-smith β full-page chatbot for author "jane-smith" | |
| Routes (no auth β fully public): | |
| GET /api/public/{author_slug}/profile β name, avatar, bio for OG tags | |
| GET /api/public/{author_slug}/widget-config β bot name, colors, welcome message | |
| GET /api/public/{author_slug}/books β public book list for selector UI | |
| """ | |
| from fastapi import APIRouter, Depends, HTTPException | |
| from sqlalchemy import select | |
| from sqlalchemy.ext.asyncio import AsyncSession | |
| from app.dependencies import get_db | |
| from app.models.user import User | |
| from app.models.book import Book | |
| from app.models.client_access import ClientAccess | |
| from datetime import datetime, timezone | |
| router = APIRouter(prefix="/public", tags=["Public Chat"]) | |
| async def _get_active_author(slug: str, db: AsyncSession) -> User: | |
| """Fetch author by slug (ID), verify they have active access.""" | |
| user = await db.get(User, slug) | |
| if not user or not user.is_active or user.role != "author": | |
| raise HTTPException(404, "Author not found") | |
| # Check active subscription | |
| result = await db.execute( | |
| select(ClientAccess).where( | |
| ClientAccess.author_id == slug, | |
| ClientAccess.is_revoked == False, # noqa: E712 | |
| ClientAccess.expires_at > datetime.now(timezone.utc), | |
| ) | |
| ) | |
| access = result.scalar_one_or_none() | |
| if not access: | |
| raise HTTPException(404, "Author not found") # Don't reveal subscription status | |
| return user | |
| async def get_public_profile( | |
| author_slug: str, | |
| db: AsyncSession = Depends(get_db), | |
| ): | |
| """Public author profile β used for Open Graph meta tags on vanity chat pages. | |
| Powers the rich link previews when authors share their chat links on social media: | |
| Title: "Chat with Jane Smith about her books" | |
| Description: Author's bio | |
| Image: Author's avatar | |
| """ | |
| user = await _get_active_author(author_slug, db) | |
| # Count published books for the description | |
| books_result = await db.execute( | |
| select(Book).where(Book.author_id == author_slug, Book.is_active == True) # noqa: E712 | |
| ) | |
| books = books_result.scalars().all() | |
| return { | |
| "slug": author_slug, | |
| "full_name": user.full_name or "Author", | |
| "bio": user.bio if user.bio and not user.bio.startswith("__meta__") else None, | |
| "avatar_url": user.bot_avatar_path, | |
| "website_url": user.website_url, | |
| "book_count": len(books), | |
| "og_title": f"Chat with {user.full_name or 'this author'} about their books", | |
| "og_description": ( | |
| f"{user.full_name or 'An author'} uses AI to answer your questions about " | |
| f"their {len(books)} book{'s' if len(books) != 1 else ''}. Ask anything!" | |
| ), | |
| } | |
| async def get_widget_config( | |
| author_slug: str, | |
| db: AsyncSession = Depends(get_db), | |
| ): | |
| """Return widget configuration for the vanity chat page. | |
| The Next.js page uses this to initialize the chatbot with the correct | |
| bot name, colors, welcome message, and styling. | |
| """ | |
| user = await _get_active_author(author_slug, db) | |
| return { | |
| "slug": author_slug, | |
| "bot_name": user.bot_name, | |
| "welcome_message": user.welcome_message, | |
| "widget_theme": user.widget_theme, | |
| "widget_position": user.widget_position, | |
| "chatbot_is_active": user.chatbot_is_active, | |
| "remove_branding": user.remove_branding, | |
| "custom_widget_css": user.custom_widget_css, | |
| "response_style": user.response_style, | |
| } | |
| async def get_public_books( | |
| author_slug: str, | |
| db: AsyncSession = Depends(get_db), | |
| ): | |
| """Return public book list for the chat page's book selector UI.""" | |
| await _get_active_author(author_slug, db) | |
| result = await db.execute( | |
| select(Book).where( | |
| Book.author_id == author_slug, | |
| Book.is_active == True, # noqa: E712 | |
| ).order_by(Book.created_at.desc()) | |
| ) | |
| books = result.scalars().all() | |
| return { | |
| "books": [ | |
| { | |
| "id": b.id, | |
| "title": b.title, | |
| "cover_image_path": b.cover_image_path if hasattr(b, "cover_image_path") else None, | |
| "buy_link": b.buy_link if hasattr(b, "buy_link") else None, | |
| "genre": b.genre if hasattr(b, "genre") else None, | |
| } | |
| for b in books | |
| ] | |
| } | |