- Route Devii-driven AI gateway cost to the action/tool that triggered it instead of a blanket "internal" bucket, so per-feature AI spend is attributable. - Fix the quiz attempt review to show one previously-answered question at a time instead of all of them at once, and stop a quiz endpoint linked from the quiz flow from responding with raw JSON. - Add DB API async query result route and AI Usage Analyzer annotated source/media routes, with traversal-safe uid/path handling and matching tests. - Add Code Farm action audit logging (plant/harvest/buy-plot/upgrade/ fertilize) and related admin workspace/services/trash/gateway route and doc touch-ups. - Drop redundant docstrings from access_tokens.py per the no-comments convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VL9Xn57W5UR3HZbbuuzxdK
132 lines
4.2 KiB
Python
132 lines
4.2 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
from typing import Annotated
|
|
from urllib.parse import quote
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
|
|
|
|
from devplacepy.database import resolve_object_url
|
|
from devplacepy.dependencies import json_or_form
|
|
from devplacepy.models import WarJoinForm
|
|
from devplacepy.responses import action_result, json_error, respond, wants_json
|
|
from devplacepy.schemas import BattlesOut, WarEventsOut, WarOut
|
|
from devplacepy.seo import base_seo_context, site_url, website_schema
|
|
from devplacepy.services.opinionwar import WarError, rules, store
|
|
from devplacepy.utils import get_current_user, not_found, require_user
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _war_error(request: Request, message: str, redirect_url: str):
|
|
if wants_json(request):
|
|
return json_error(400, message)
|
|
separator = "&" if "?" in redirect_url else "?"
|
|
return RedirectResponse(
|
|
url=f"{redirect_url}{separator}error={quote(message)}", status_code=302
|
|
)
|
|
|
|
|
|
def _post_url(war: dict) -> str:
|
|
return resolve_object_url("post", war["post_uid"])
|
|
|
|
|
|
@router.get("", response_class=HTMLResponse)
|
|
async def battles_page(
|
|
request: Request, filter: str = "active", search: str = "", page: int = 1
|
|
):
|
|
user = get_current_user(request)
|
|
current_filter = filter if filter in store.FILTERS else "active"
|
|
battles, pagination = store.list_wars(
|
|
viewer=user,
|
|
war_filter=current_filter,
|
|
search=search,
|
|
page=max(1, page),
|
|
)
|
|
base = site_url(request)
|
|
seo_ctx = base_seo_context(
|
|
request,
|
|
title="Opinion Wars",
|
|
description=(
|
|
"Week-long faction battles between developers. Pick a side, fight once "
|
|
"a day and carry your faction to victory."
|
|
),
|
|
breadcrumbs=[
|
|
{"name": "Home", "url": "/feed"},
|
|
{"name": "Battles", "url": "/battles"},
|
|
],
|
|
schemas=[website_schema(base)],
|
|
)
|
|
return respond(
|
|
request,
|
|
"battles.html",
|
|
{
|
|
**seo_ctx,
|
|
"user": user,
|
|
"battles": battles,
|
|
"current_filter": current_filter,
|
|
"counts": store.filter_counts(user, search),
|
|
"search": search,
|
|
"pagination": pagination,
|
|
},
|
|
model=BattlesOut,
|
|
)
|
|
|
|
|
|
@router.get("/{war_uid}")
|
|
async def battle_state(request: Request, war_uid: str):
|
|
user = get_current_user(request)
|
|
serialized = store.get_war_serialized(store.get_war(war_uid), user)
|
|
if not serialized:
|
|
raise not_found("Battle not found")
|
|
return JSONResponse(WarOut.model_validate(serialized).model_dump())
|
|
|
|
|
|
@router.get("/{war_uid}/events")
|
|
async def battle_events(
|
|
request: Request, war_uid: str, after: int = 0, limit: int = rules.EVENT_LIMIT_DEFAULT
|
|
):
|
|
war = store.resolve_if_due(store.get_war(war_uid))
|
|
if not war:
|
|
raise not_found("Battle not found")
|
|
events = store.events_for(war_uid, after_seq=after, limit=limit)
|
|
return JSONResponse(
|
|
WarEventsOut.model_validate(
|
|
{"events": events, "status": war.get("status") or "active"}
|
|
).model_dump()
|
|
)
|
|
|
|
|
|
@router.post("/{war_uid}/join")
|
|
async def join_battle(
|
|
request: Request,
|
|
war_uid: str,
|
|
data: Annotated[WarJoinForm, Depends(json_or_form(WarJoinForm))],
|
|
):
|
|
user = require_user(request)
|
|
war = store.get_war(war_uid)
|
|
if not war:
|
|
raise not_found("Battle not found")
|
|
url = _post_url(war)
|
|
try:
|
|
war = store.join_war(war, user, data.faction, request)
|
|
except WarError as exc:
|
|
return _war_error(request, str(exc), url)
|
|
serialized = store.get_war_serialized(war, user)
|
|
return action_result(request, url, data={"war": serialized})
|
|
|
|
|
|
@router.post("/{war_uid}/fight")
|
|
async def fight_battle(request: Request, war_uid: str):
|
|
user = require_user(request)
|
|
war = store.get_war(war_uid)
|
|
if not war:
|
|
raise not_found("Battle not found")
|
|
url = _post_url(war)
|
|
try:
|
|
war, damage = store.fight(war, user, request)
|
|
except WarError as exc:
|
|
return _war_error(request, str(exc), url)
|
|
serialized = store.get_war_serialized(war, user)
|
|
return action_result(request, url, data={"war": serialized, "damage": damage})
|