language-learning-app/api/app/routers/bff/adventure.py

138 lines
4.4 KiB
Python

import uuid
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from ...auth import verify_token
from ...config import settings
from ...outbound.postgres.database import get_db
from ...outbound.postgres.repositories.adventure_repository import (
PostgresAdventureEntryAudioRepository,
PostgresAdventureEntryChoiceRepository,
PostgresAdventureEntryRepository,
PostgresAdventureEntryTranslationRepository,
PostgresAdventureRepository,
)
router = APIRouter(prefix="/adventure", tags=["bff", "adventures"])
class AdventureChoiceItem(BaseModel):
id: str
index: int
label: str
text: str
class AdventureEntryItem(BaseModel):
id: str
adventure_id: str
generated_from_choice_id: str | None
status: str
entry_index: int
story_text: str | None
translation: str | None
audio_url: str | None
created_at: str
class AdventureDetailResponse(BaseModel):
id: str
user_id: str
status: str
language: str
source_language: str
competencies: list[str]
max_entry_count: int
title: str
description: str | None
genres: list[str]
setting: list[str]
vibes: list[str]
protagonist: list[str]
created_at: str
entries: list[AdventureEntryItem]
current_entry_choices: list[AdventureChoiceItem]
def _audio_url(key: str | None) -> str | None:
if key is None:
return None
return f"{settings.api_base_url}/media/{key}"
@router.get("/{adventure_id}", response_model=AdventureDetailResponse, status_code=200)
async def get_adventure(
adventure_id: str,
db: AsyncSession = Depends(get_db),
token_data: dict = Depends(verify_token),
) -> AdventureDetailResponse:
user_id = uuid.UUID(token_data["sub"])
try:
adv_id = uuid.UUID(adventure_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid adventure_id")
adventure = await PostgresAdventureRepository(db).get_by_id(adv_id)
if adventure is None or adventure.user_id != str(user_id):
raise HTTPException(status_code=404, detail="Adventure not found")
entries = await PostgresAdventureEntryRepository(db).list_for_adventure(adv_id)
translation_repo = PostgresAdventureEntryTranslationRepository(db)
audio_repo = PostgresAdventureEntryAudioRepository(db)
entry_items = []
for entry in entries:
eid = uuid.UUID(entry.id)
translation = await translation_repo.get_for_entry(
entry_id=eid,
component_type="story_text",
target_language=adventure.source_language,
)
audio = await audio_repo.get_for_entry(entry_id=eid, component_type="story_text")
entry_items.append(
AdventureEntryItem(
id=entry.id,
adventure_id=entry.adventure_id,
generated_from_choice_id=entry.generated_from_choice_id,
status=entry.status,
entry_index=entry.entry_index,
story_text=entry.story_text,
translation=translation.translated_text if translation else None,
audio_url=_audio_url(audio.file_name if audio else None),
created_at=entry.created_at.isoformat(),
)
)
# Choices for the most recent entry, if it's complete (entries are ordered entry_index asc)
current_entry_choices: list[AdventureChoiceItem] = []
if entries and entries[-1].status == "complete":
choices = await PostgresAdventureEntryChoiceRepository(db).list_for_entry(
uuid.UUID(entries[-1].id)
)
current_entry_choices = [
AdventureChoiceItem(id=c.id, index=c.index, label=c.label, text=c.text)
for c in choices
]
return AdventureDetailResponse(
id=adventure.id,
user_id=adventure.user_id,
status=adventure.status,
language=adventure.language,
source_language=adventure.source_language,
competencies=adventure.competencies,
max_entry_count=adventure.max_entry_count,
title=adventure.title,
description=adventure.description,
genres=adventure.genres,
setting=adventure.setting,
vibes=adventure.vibes,
protagonist=adventure.protagonist,
created_at=adventure.created_at.isoformat(),
entries=entry_items,
current_entry_choices=current_entry_choices,
)