Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions backend/src/api/characters_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@
wipe_realm_character_data,
)
from src.characters.roster import RosterError, replace_roster
from src.characters.wills import (
WillError,
ack_will_results,
check_heir_search_rate,
create_will_request,
list_pending_wills,
search_heirs,
)
from src.characters.wardrobe import (
WardrobeError,
ack_wardrobe_slots,
Expand Down Expand Up @@ -99,6 +107,18 @@ class RosterBody(BaseModel):
realm_id: str | None = None


class WillRequestBody(BaseModel):
"""Profile will change: an heir, or null to revoke, against the revision the page showed."""

heir_character_id: str | None
base_revision: int


class WillAppliedBody(BaseModel):
realm_id: str | None = None
results: list[dict] = Field(default_factory=list)


class WardrobeActiveBody(BaseModel):
"""PATCH body for wardrobe active slot. null clears auto-apply."""

Expand Down Expand Up @@ -587,6 +607,47 @@ def delete_lore_item_customise_route(
raise _lore_http(e) from e


def _will_http(exc: WillError) -> HTTPException:
return HTTPException(status_code=exc.status_code, detail=str(exc))


@characters_router.get("/heirs")
def get_heir_search(
q: str | None = None,
character_id: str | None = None,
authorization: str | None = Header(default=None),
):
"""Living characters of the session's realm by name, for naming an heir."""
session = _profile_session_from_auth(authorization)
try:
check_heir_search_rate(session["player_uuid"])
results = search_heirs(session["player_uuid"], session.get("realm_id"), q, character_id)
except WillError as e:
raise _will_http(e) from e
return {"results": results}


@characters_router.post("/{character_id}/will")
def post_character_will(
character_id: str,
body: WillRequestBody,
authorization: str | None = Header(default=None),
):
"""Queue a will change for RPCharacters to apply in game."""
session = _profile_session_from_auth(authorization)
try:
request = create_will_request(
session["player_uuid"],
session.get("realm_id"),
character_id,
body.heir_character_id,
body.base_revision,
)
except WillError as e:
raise _will_http(e) from e
return {"ok": True, "request": request}


@characters_router.get("/{character_id}/wardrobe")
def get_character_wardrobe(
character_id: str,
Expand Down Expand Up @@ -986,6 +1047,32 @@ def plugin_applied_creates(
raise HTTPException(status_code=400, detail=str(e)) from e


@characters_router.get("/plugin/wills/pending")
def plugin_pending_wills(
realm_id: str | None = None,
x_plugin_key: str | None = Header(default=None, alias=HEADER_PLUGIN_KEY),
):
"""Will changes made on Profile for this realm. The realm is required."""
_require_plugin(x_plugin_key)
try:
return {"requests": list_pending_wills(realm_id)}
except WillError as e:
raise _will_http(e) from e


@characters_router.post("/plugin/wills/applied")
def plugin_applied_wills(
body: WillAppliedBody,
x_plugin_key: str | None = Header(default=None, alias=HEADER_PLUGIN_KEY),
):
"""RPCharacters' answer for each pulled will request. The realm must match the request's."""
_require_plugin(x_plugin_key)
try:
return ack_will_results(body.realm_id, body.results)
except WillError as e:
raise _will_http(e) from e


@characters_router.put("/plugin/roster")
def plugin_put_roster(
body: RosterBody,
Expand Down
11 changes: 10 additions & 1 deletion backend/src/auth/test_players.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ def staff(env, role="mod"):

def test_requires_a_staff_role(app, env, world):
assert client(app).get("/admin/players").status_code == 401
assert client(app).get(f"/admin/players/{HAZEL}").status_code == 401
assert client(app, staff(env, "player")).get("/admin/players").status_code == 403
assert client(app, staff(env, "player")).get(f"/admin/players/{HAZEL}").status_code == 403
assert client(app, staff(env, "player")).get(f"/admin/players/{HAZEL}/activity").status_code == 403
assert client(app, staff(env, "mod")).get("/admin/players").status_code == 200

Expand Down Expand Up @@ -245,7 +247,8 @@ def test_profile_includes_the_shared_character_sheet_in_each_realm(app, env, wor


@pytest.mark.parametrize("kind,size", [("face", (8, 8)), ("figure", (16, 32))])
def test_character_pictures_require_staff_and_read_the_requested_realm(app, env, world, monkeypatch, kind, size):
@pytest.mark.parametrize("status", ["ALIVE", "DEAD", "MISSING"])
def test_character_pictures_require_staff_and_read_the_requested_realm(app, env, world, monkeypatch, kind, size, status):
import io
from PIL import Image
from src.characters import wardrobe
Expand All @@ -256,6 +259,8 @@ def test_character_pictures_require_staff_and_read_the_requested_realm(app, env,
cid = "c-Hazel Stonebrook"
character(env, HAZEL, "Tutorial Hazel", realm="tutorial", character_id=cid)
with env.connect() as conn:
conn.execute("UPDATE character_roster SET status = ? WHERE player_uuid = ? AND character_id = ?",
(status, HAZEL, cid))
conn.execute("UPDATE character_roster SET wardrobe_active_slot = CASE realm_id "
"WHEN 'tutorial' THEN 'extra_1' ELSE 'base' END WHERE player_uuid = ?", (HAZEL,))
for slot, colour in [("base", (10, 20, 30, 255)), ("extra_1", (40, 50, 60, 255))]:
Expand Down Expand Up @@ -288,6 +293,10 @@ def test_character_pictures_require_staff_and_read_the_requested_realm(app, env,
assert [tuple(r) for r in conn.execute("SELECT * FROM character_wardrobe_slots")] == before
assert conn.execute("SELECT wardrobe_active_slot FROM character_roster "
"WHERE realm_id = 'tutorial' AND character_id = ?", (cid,)).fetchone()[0] == "extra_1"
if status != "ALIVE":
with pytest.raises(wardrobe.WardrobeError, match="Character is not alive") as error:
wardrobe.get_wardrobe(HAZEL, cid)
assert error.value.status_code == 403


@pytest.mark.parametrize("kind", ["face", "figure"])
Expand Down
10 changes: 10 additions & 0 deletions backend/src/characters/creates.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,7 +611,9 @@ def list_for_player(
get_player_meta,
list_roster,
)
from src.characters.estates import estate_for
from src.characters.rpc_player_meta import resolve_web_entitlements
from src.characters.wills import will_requests_for
from src.skins.codes import normalize_realm_id
from src.skins.db import connect

Expand Down Expand Up @@ -642,6 +644,14 @@ def list_for_player(
(uuid, realm),
).fetchall()

alive_ids = [c["id"] for c in roster if "will" in c]
requests = will_requests_for(conn, realm, uuid, alive_ids)
for c in roster:
if c["id"] in requests:
c["will_request"] = requests[c["id"]]
if str(c.get("status") or "").upper() in ("DEAD", "MISSING"):
c["estate"] = estate_for(conn, realm, c["id"], c.get("death_id"))

catalog = get_catalog()
characters: list[dict[str, Any]] = list(roster)
for row in pending_rows:
Expand Down
Loading
Loading