stepstead-backend/routers/quests.py
jts ea049b8ccd İlk commit: Stepstead backend (FastAPI)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 11:30:24 +03:00

258 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Görevler — ana + yan görevler ("lore" izni; loremaster + admin).
Her görevin: görevi veren NPC, kıta, bölge (map_tags region), veriliş noktası (location/pin)
ve "nerede yapılacağı" (çok seçmeli objective location'lar) bilgisi tutulur.
Sahiplik: admin herkesinkini, diğerleri sadece kendi görevini düzenler/siler.
WAF nedeniyle sadece GET/POST.
GET /quests → liste (giver + konum + bölge adları + objective'ler)
GET /quests/meta → dropdown verisi (npcs, locations, regions, continents)
POST /quests → yeni görev
POST /quests/{id} → güncelle
POST /quests/{id}/delete → sil
"""
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
from deps import require_perm, get_user_perms, has_perm
from models import User, get_db
router = APIRouter(prefix="/quests", tags=["quests"])
TYPES = {"main", "side"}
def _author(user: User) -> str:
return (user.display_name or "").strip() or user.email
async def _assert_owner_or_admin(db: AsyncSession, user: User, row_id: int):
perms = await get_user_perms(user, db)
if has_perm(perms, "*"):
return
owner = (await db.execute(text(
"SELECT author_id FROM quests WHERE id=:id"
), {"id": row_id})).scalar_one_or_none()
if owner is None:
raise HTTPException(404, "Görev yok")
if owner != user.id:
raise HTTPException(403, "Sadece kendi görevini düzenleyebilir/silebilirsin")
# ---- Şemalar ----
class ObjectiveIn(BaseModel):
map_tag_id: int | None = None
note: str | None = Field(default=None, max_length=500)
class QuestIn(BaseModel):
title: str = Field(min_length=1, max_length=200)
quest_type: str = "side"
summary: str | None = Field(default=None, max_length=20000)
giver_npc_id: int | None = None
continent: str | None = Field(default=None, max_length=80)
region_tag_id: int | None = None
location_tag_id: int | None = None
objectives: list[ObjectiveIn] = Field(default_factory=list)
class ObjectiveOut(BaseModel):
id: int
map_tag_id: int | None
location_name: str | None
location_kind: str | None
note: str | None
class QuestOut(BaseModel):
id: int
title: str
quest_type: str
summary: str | None
giver_npc_id: int | None
giver_npc_name: str | None
continent: str | None
region_tag_id: int | None
region_name: str | None
location_tag_id: int | None
location_name: str | None
author: str | None
author_id: int | None
objectives: list[ObjectiveOut]
created_at: str
updated_at: str
# ---- Meta (dropdown) ----
class MetaNpc(BaseModel):
id: int
name: str
map_tag_id: int | None
location_name: str | None
class MetaPlace(BaseModel):
id: int
name: str
kind: str
continent: str | None
class MetaRegion(BaseModel):
id: int
name: str
continent: str | None
class MetaOut(BaseModel):
npcs: list[MetaNpc]
places: list[MetaPlace]
regions: list[MetaRegion]
continents: list[str]
@router.get("/meta", response_model=MetaOut)
async def quest_meta(user: User = Depends(require_perm("lore")), db: AsyncSession = Depends(get_db)):
npcs = (await db.execute(text(
"SELECT n.id,n.name,n.map_tag_id,t.name AS location_name "
"FROM npcs n LEFT JOIN map_tags t ON t.id=n.map_tag_id ORDER BY n.name"
))).mappings().all()
places = (await db.execute(text(
"SELECT id,name,kind,continent FROM map_tags WHERE kind IN ('region','poi') "
"ORDER BY kind DESC, continent NULLS LAST, name"
))).mappings().all()
regions = (await db.execute(text(
"SELECT id,name,continent FROM map_tags WHERE kind='region' ORDER BY continent NULLS LAST,name"
))).mappings().all()
conts = (await db.execute(text(
"SELECT name FROM map_tags WHERE kind='continent' ORDER BY name"
))).scalars().all()
return MetaOut(
npcs=[MetaNpc(**dict(r)) for r in npcs],
places=[MetaPlace(**dict(r)) for r in places],
regions=[MetaRegion(**dict(r)) for r in regions],
continents=list(conts),
)
# ---- Yardımcı: tek görevi topla ----
async def _load_quest(db: AsyncSession, quest_id: int) -> QuestOut | None:
r = (await db.execute(text(
"SELECT q.id,q.title,q.quest_type,q.summary,"
"q.giver_npc_id,n.name AS giver_npc_name,"
"q.continent,q.region_tag_id,t.name AS region_name,"
"q.location_tag_id,lt.name AS location_name,"
"q.author,q.author_id,q.created_at,q.updated_at "
"FROM quests q "
"LEFT JOIN npcs n ON n.id=q.giver_npc_id "
"LEFT JOIN map_tags t ON t.id=q.region_tag_id "
"LEFT JOIN map_tags lt ON lt.id=q.location_tag_id "
"WHERE q.id=:id"
), {"id": quest_id})).mappings().one_or_none()
if r is None:
return None
objs = (await db.execute(text(
"SELECT o.id,o.map_tag_id,mt.name AS location_name,mt.kind AS location_kind,o.note "
"FROM quest_objectives o LEFT JOIN map_tags mt ON mt.id=o.map_tag_id "
"WHERE o.quest_id=:id ORDER BY o.ordering,o.id"
), {"id": quest_id})).mappings().all()
d = dict(r)
d["created_at"] = d["created_at"].isoformat()
d["updated_at"] = d["updated_at"].isoformat()
d["objectives"] = [ObjectiveOut(**dict(o)) for o in objs]
return QuestOut(**d)
async def _validate(db: AsyncSession, q: QuestIn):
if q.quest_type not in TYPES:
raise HTTPException(400, "quest_type: main | side")
if q.giver_npc_id is not None:
ok = (await db.execute(text("SELECT 1 FROM npcs WHERE id=:id"), {"id": q.giver_npc_id})).scalar()
if not ok:
raise HTTPException(400, "Görev veren NPC yok")
if q.region_tag_id is not None:
ok = (await db.execute(text("SELECT 1 FROM map_tags WHERE id=:id AND kind='region'"),
{"id": q.region_tag_id})).scalar()
if not ok:
raise HTTPException(400, "Bölge yok")
if q.location_tag_id is not None:
ok = (await db.execute(text("SELECT 1 FROM map_tags WHERE id=:id AND kind IN ('region','poi')"),
{"id": q.location_tag_id})).scalar()
if not ok:
raise HTTPException(400, "Veriliş noktası yok (map editörü alan/pin olmalı)")
for o in q.objectives:
if o.map_tag_id is not None:
ok = (await db.execute(text("SELECT 1 FROM map_tags WHERE id=:id AND kind IN ('region','poi')"),
{"id": o.map_tag_id})).scalar()
if not ok:
raise HTTPException(400, "Görev noktası yok (map editörü alan/pin olmalı)")
async def _save_objectives(db: AsyncSession, quest_id: int, objectives: list[ObjectiveIn]):
await db.execute(text("DELETE FROM quest_objectives WHERE quest_id=:id"), {"id": quest_id})
for i, o in enumerate(objectives):
if o.map_tag_id is None and not (o.note or "").strip():
continue
await db.execute(text(
"INSERT INTO quest_objectives(quest_id,map_tag_id,note,ordering) "
"VALUES(:q,:tag,:note,:ord)"
), {"q": quest_id, "tag": o.map_tag_id, "note": (o.note or None), "ord": i})
# ---- CRUD ----
@router.get("", response_model=list[QuestOut])
async def list_quests(user: User = Depends(require_perm("lore")), db: AsyncSession = Depends(get_db)):
ids = (await db.execute(text(
"SELECT id FROM quests ORDER BY quest_type,sort_order,id"
))).scalars().all()
return [await _load_quest(db, i) for i in ids]
@router.post("", response_model=QuestOut)
async def create_quest(q: QuestIn, user: User = Depends(require_perm("lore")),
db: AsyncSession = Depends(get_db)):
await _validate(db, q)
qid = (await db.execute(text(
"INSERT INTO quests(title,quest_type,summary,giver_npc_id,continent,region_tag_id,"
"location_tag_id,author,author_id) "
"VALUES(:title,:qt,:summary,:giver,:cont,:region,:loc,:author,:aid) RETURNING id"
), {"title": q.title, "qt": q.quest_type, "summary": q.summary, "giver": q.giver_npc_id,
"cont": q.continent, "region": q.region_tag_id, "loc": q.location_tag_id,
"author": _author(user), "aid": user.id})).scalar_one()
await _save_objectives(db, qid, q.objectives)
await db.commit()
return await _load_quest(db, qid)
@router.post("/{quest_id}", response_model=QuestOut)
async def update_quest(quest_id: int, q: QuestIn, user: User = Depends(require_perm("lore")),
db: AsyncSession = Depends(get_db)):
await _assert_owner_or_admin(db, user, quest_id)
await _validate(db, q)
res = await db.execute(text(
"UPDATE quests SET title=:title,quest_type=:qt,summary=:summary,giver_npc_id=:giver,"
"continent=:cont,region_tag_id=:region,location_tag_id=:loc,updated_at=now() WHERE id=:id"
), {"id": quest_id, "title": q.title, "qt": q.quest_type, "summary": q.summary,
"giver": q.giver_npc_id, "cont": q.continent, "region": q.region_tag_id,
"loc": q.location_tag_id})
if res.rowcount == 0:
raise HTTPException(404, "Görev yok")
await _save_objectives(db, quest_id, q.objectives)
await db.commit()
return await _load_quest(db, quest_id)
@router.post("/{quest_id}/delete")
async def delete_quest(quest_id: int, user: User = Depends(require_perm("lore")),
db: AsyncSession = Depends(get_db)):
await _assert_owner_or_admin(db, user, quest_id)
res = await db.execute(text("DELETE FROM quests WHERE id=:id"), {"id": quest_id})
await db.commit()
if res.rowcount == 0:
raise HTTPException(404, "Görev yok")
return {"deleted": quest_id}