258 lines
9.6 KiB
Python
258 lines
9.6 KiB
Python
"""
|
||
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}
|