""" 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}