Files
wiki-tcg/backend/routers/cards.py
2026-04-01 18:31:33 +02:00

230 lines
7.4 KiB
Python

import asyncio
import uuid
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy import asc, case, desc, func
from sqlalchemy.orm import Session
from game.card import _get_specific_card_async
from core.database import get_db
from services.database_functions import check_boosters, fill_card_pool, BOOSTER_MAX
from core.dependencies import get_current_user, limiter
from core.models import Card as CardModel
from core.models import Deck as DeckModel
from core.models import DeckCard as DeckCardModel
from core.models import User as UserModel
router = APIRouter()
@router.get("/boosters")
def get_boosters(user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
count, countdown = check_boosters(user, db)
return {"count": count, "countdown": countdown, "email_verified": user.email_verified}
@router.get("/cards")
def get_cards(
skip: int = 0,
limit: int = 40,
search: str = "",
rarities: list[str] = Query(default=[]),
types: list[str] = Query(default=[]),
cost_min: int = 1,
cost_max: int = 10,
favorites_only: bool = False,
wtt_only: bool = False,
sort_by: str = "name",
sort_dir: str = "asc",
user: UserModel = Depends(get_current_user),
db: Session = Depends(get_db),
):
q = db.query(CardModel).filter(CardModel.user_id == user.id)
if search:
q = q.filter(CardModel.name.ilike(f"%{search}%"))
if rarities:
q = q.filter(CardModel.card_rarity.in_(rarities))
if types:
q = q.filter(CardModel.card_type.in_(types))
q = q.filter(CardModel.cost >= cost_min, CardModel.cost <= cost_max)
if favorites_only:
q = q.filter(CardModel.is_favorite == True)
if wtt_only:
q = q.filter(CardModel.willing_to_trade == True)
total = q.count()
# case() for rarity ordering matches frontend RARITY_ORDER constant
rarity_order_expr = case(
(CardModel.card_rarity == 'common', 0),
(CardModel.card_rarity == 'uncommon', 1),
(CardModel.card_rarity == 'rare', 2),
(CardModel.card_rarity == 'super_rare', 3),
(CardModel.card_rarity == 'epic', 4),
(CardModel.card_rarity == 'legendary', 5),
else_=0
)
# coalesce mirrors frontend: received_at ?? generated_at
date_received_expr = func.coalesce(CardModel.received_at, CardModel.generated_at)
sort_map = {
"name": CardModel.name,
"cost": CardModel.cost,
"attack": CardModel.attack,
"defense": CardModel.defense,
"rarity": rarity_order_expr,
"date_generated": CardModel.generated_at,
"date_received": date_received_expr,
}
sort_col = sort_map.get(sort_by, CardModel.name)
order_fn = desc if sort_dir == "desc" else asc
# Secondary sort by name keeps pages stable when primary values are tied
q = q.order_by(order_fn(sort_col), asc(CardModel.name))
cards = q.offset(skip).limit(limit).all()
return {
"cards": [
{c.name: getattr(card, c.name) for c in card.__table__.columns}
for card in cards
],
"total": total,
}
@router.get("/cards/in-decks")
def get_cards_in_decks(user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
deck_ids = [d.id for d in db.query(DeckModel).filter(DeckModel.user_id == user.id, DeckModel.deleted == False).all()]
if not deck_ids:
return []
card_ids = db.query(DeckCardModel.card_id).filter(DeckCardModel.deck_id.in_(deck_ids)).distinct().all()
return [str(row.card_id) for row in card_ids]
@router.post("/open_pack")
@limiter.limit("10/minute")
async def open_pack(request: Request, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
if not user.email_verified:
raise HTTPException(status_code=403, detail="You must verify your email before opening packs")
check_boosters(user, db)
if user.boosters == 0:
raise HTTPException(status_code=400, detail="No booster packs available")
cards = (
db.query(CardModel)
.filter(CardModel.user_id == None, CardModel.ai_used == False)
.limit(5)
.all()
)
if len(cards) < 5:
asyncio.create_task(fill_card_pool())
raise HTTPException(status_code=503, detail="Card pool is low, please try again shortly")
now = datetime.now()
for card in cards:
card.user_id = user.id
card.received_at = now
was_full = user.boosters == BOOSTER_MAX
user.boosters -= 1
if was_full:
user.boosters_countdown = datetime.now()
db.commit()
asyncio.create_task(fill_card_pool())
return [
{**{c.name: getattr(card, c.name) for c in card.__table__.columns},
"card_rarity": card.card_rarity,
"card_type": card.card_type}
for card in cards
]
@router.post("/cards/{card_id}/report")
def report_card(card_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
card = db.query(CardModel).filter(
CardModel.id == uuid.UUID(card_id),
CardModel.user_id == user.id
).first()
if not card:
raise HTTPException(status_code=404, detail="Card not found")
card.reported = True
db.commit()
return {"message": "Card reported"}
@router.post("/cards/{card_id}/refresh")
@limiter.limit("5/hour")
async def refresh_card(request: Request, card_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
card = db.query(CardModel).filter(
CardModel.id == uuid.UUID(card_id),
CardModel.user_id == user.id
).first()
if not card:
raise HTTPException(status_code=404, detail="Card not found")
if user.last_refresh_at and datetime.now() - user.last_refresh_at < timedelta(minutes=10):
remaining = (user.last_refresh_at + timedelta(minutes=10)) - datetime.now()
minutes = int(remaining.total_seconds() // 60)
seconds = int(remaining.total_seconds() % 60)
raise HTTPException(
status_code=429,
detail=f"You can refresh again in {minutes}m {seconds}s"
)
new_card = await _get_specific_card_async(card.name)
if not new_card:
raise HTTPException(status_code=502, detail="Failed to regenerate card from Wikipedia")
card.image_link = new_card.image_link
card.card_rarity = new_card.card_rarity.name
card.card_type = new_card.card_type.name
card.text = new_card.text
card.attack = new_card.attack
card.defense = new_card.defense
card.cost = new_card.cost
card.reported = False
card.generated_at = datetime.now()
card.received_at = datetime.now()
user.last_refresh_at = datetime.now()
db.commit()
return {
**{c.name: getattr(card, c.name) for c in card.__table__.columns},
"card_rarity": card.card_rarity,
"card_type": card.card_type,
}
@router.post("/cards/{card_id}/favorite")
def toggle_favorite(card_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
card = db.query(CardModel).filter(
CardModel.id == uuid.UUID(card_id),
CardModel.user_id == user.id
).first()
if not card:
raise HTTPException(status_code=404, detail="Card not found")
card.is_favorite = not card.is_favorite
db.commit()
return {"is_favorite": card.is_favorite}
@router.post("/cards/{card_id}/willing-to-trade")
def toggle_willing_to_trade(card_id: str, user: UserModel = Depends(get_current_user), db: Session = Depends(get_db)):
card = db.query(CardModel).filter(
CardModel.id == uuid.UUID(card_id),
CardModel.user_id == user.id
).first()
if not card:
raise HTTPException(status_code=404, detail="Card not found")
card.willing_to_trade = not card.willing_to_trade
db.commit()
return {"willing_to_trade": card.willing_to_trade}