Arag / app /api /public_chat.py
AuthorBot
Add enterprise growth APIs, billing, admin UI, and test suite.
12930b5
Raw
History Blame Contribute Delete
4.63 kB
"""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
@router.get("/{author_slug}/profile")
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!"
),
}
@router.get("/{author_slug}/widget-config")
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,
}
@router.get("/{author_slug}/books")
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
]
}