from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import HTMLResponse, PlainTextResponse, RedirectResponse from sqlalchemy.orm import Session, joinedload from app.config import settings from app.database import get_db from app.models import EventEdition, EventType, NewsletterSubscriber, OutboundClick, PlatformLink from app.schemas import EventEditionPublic, PlatformLinkPublic, SiteConfigPublic, SubscribeRequest from app.services.alerts import maybe_notify_click_spike from app.services.calendar import build_ics from app.services.editions import edition_to_public from app.services.outbound import validate_outbound_url from app.services.share_preview import render_share_html router = APIRouter(prefix="/api/public", tags=["public"]) TAGLINE = "Bengaluru's home for Holi, Garba, Dandiya & bhajan nights — across the city's best venues." @router.get("/health") def health(): return {"status": "ok", "city": "Bengaluru"} @router.get("/config", response_model=SiteConfigPublic) def site_config(db: Session = Depends(get_db)): links = ( db.query(PlatformLink) .filter(PlatformLink.is_active.is_(True)) .order_by(PlatformLink.sort_order, PlatformLink.id) .all() ) editions = ( db.query(EventEdition) .options(joinedload(EventEdition.media)) .filter(EventEdition.is_published.is_(True)) .order_by(EventEdition.is_featured.desc(), EventEdition.sort_order, EventEdition.event_date.desc().nullslast()) .all() ) featured = next((e for e in editions if e.is_featured), editions[0] if editions else None) recent = [edition_to_public(e) for e in editions[:6]] return SiteConfigPublic( city="Bengaluru", tagline=TAGLINE, platform_links=[PlatformLinkPublic.model_validate(l) for l in links], featured_edition=edition_to_public(featured) if featured else None, recent_editions=recent, ) @router.get("/editions", response_model=list[EventEditionPublic]) def list_editions( db: Session = Depends(get_db), event_type: EventType | None = Query(default=None), ): q = ( db.query(EventEdition) .options(joinedload(EventEdition.media)) .filter(EventEdition.is_published.is_(True)) ) if event_type is not None: q = q.filter(EventEdition.event_type == event_type) editions = q.order_by(EventEdition.event_date.desc().nullslast(), EventEdition.id.desc()).all() return [edition_to_public(e) for e in editions] @router.get("/editions/{slug}", response_model=EventEditionPublic) def get_edition(slug: str, db: Session = Depends(get_db)): edition = ( db.query(EventEdition) .options(joinedload(EventEdition.media)) .filter(EventEdition.slug == slug, EventEdition.is_published.is_(True)) .first() ) if not edition: raise HTTPException(status_code=404, detail="Edition not found") return edition_to_public(edition) @router.get("/go") def outbound_redirect( request: Request, url: str = Query(..., min_length=8, max_length=512), kind: str = Query(default="other", max_length=64), edition_id: int | None = Query(default=None), db: Session = Depends(get_db), ): try: destination = validate_outbound_url(url) except ValueError: raise HTTPException(status_code=400, detail="Invalid destination URL") db.add( OutboundClick( link_kind=kind[:64], destination_url=destination, edition_id=edition_id, referrer=request.headers.get("referer"), user_agent=(request.headers.get("user-agent") or "")[:512], ) ) db.commit() maybe_notify_click_spike(db) return RedirectResponse(destination, status_code=302) @router.post("/subscribe") def subscribe(body: SubscribeRequest, db: Session = Depends(get_db)): existing = db.query(NewsletterSubscriber).filter(NewsletterSubscriber.email == body.email).first() if existing: return {"ok": True, "message": "Already on the list — we'll see you in Bengaluru."} db.add(NewsletterSubscriber(email=body.email, neighborhood=body.neighborhood)) db.commit() return {"ok": True, "message": "You're in! We'll email you when the next edition drops."} @router.get("/editions/{slug}/calendar.ics", response_class=PlainTextResponse) def edition_calendar(slug: str, db: Session = Depends(get_db)): edition = ( db.query(EventEdition) .filter(EventEdition.slug == slug, EventEdition.is_published.is_(True)) .first() ) if not edition: raise HTTPException(status_code=404, detail="Edition not found") page_url = f"{settings.public_site_url.rstrip('/')}/events/{slug}" return PlainTextResponse( build_ics(edition, page_url), media_type="text/calendar", headers={"Content-Disposition": f'attachment; filename="{slug}.ics"'}, ) def _share_html_for_slug(slug: str, db: Session) -> str: edition = ( db.query(EventEdition) .options(joinedload(EventEdition.media)) .filter(EventEdition.slug == slug, EventEdition.is_published.is_(True)) .first() ) if not edition: raise HTTPException(status_code=404, detail="Edition not found") return render_share_html(edition_to_public(edition), slug) @router.get("/share/events/{slug}", response_class=HTMLResponse) def share_event_preview_api(slug: str, db: Session = Depends(get_db)): return HTMLResponse(_share_html_for_slug(slug, db))