feat: add per-user avatar seed regeneration with irreversible random avatar replacement

Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
This commit is contained in:
2026-06-27 22:31:34 +00:00
parent e773067106
commit 6f340a6818
60 changed files with 1626 additions and 97 deletions
+80
View File
@@ -103,6 +103,48 @@ def test_steal_empty_plot_returns_400(app_server, seeded_db):
assert r.status_code == 400
def _set_stars(username, stars):
refresh_snapshot()
user = get_table("users").find_one(username=username)
farm = store.get_farm(user["uid"])
get_table("game_farms").update({"uid": farm["uid"], "stars": stars}, ["uid"])
refresh_snapshot()
def test_legacy_requires_auth(app_server, seeded_db):
r = requests.post(f"{BASE_URL}/game/legacy", data={"key": "multiplier"}, headers=JSON)
assert r.status_code in (401, 303)
def test_legacy_upgrade_spends_stars(app_server, seeded_db):
session, name = _signup()
_reset_farm(name)
_set_stars(name, 1000)
r = session.post(f"{BASE_URL}/game/legacy", data={"key": "multiplier"}, headers=JSON)
assert r.status_code == 200, r.text[:200]
refresh_snapshot()
user = get_table("users").find_one(username=name)
farm = store.get_farm(user["uid"])
assert int(farm.get("legacy_multiplier") or 0) == 1
assert int(farm.get("stars") or 0) < 1000
def test_legacy_insufficient_stars_returns_400(app_server, seeded_db):
session, name = _signup()
_reset_farm(name)
_set_stars(name, 0)
r = session.post(f"{BASE_URL}/game/legacy", data={"key": "multiplier"}, headers=JSON)
assert r.status_code == 400
def test_legacy_unknown_key_returns_400(app_server, seeded_db):
session, name = _signup()
_reset_farm(name)
_set_stars(name, 1000)
r = session.post(f"{BASE_URL}/game/legacy", data={"key": "telekinesis"}, headers=JSON)
assert r.status_code == 400
def test_steal_ready_unprotected_crop_transfers_coins(app_server, seeded_db):
owner_session, owner = _signup()
_reset_farm(owner, coins=100000)
@@ -132,3 +174,41 @@ def test_steal_ready_unprotected_crop_transfers_coins(app_server, seeded_db):
thief_farm = store.get_farm(thief_user["uid"])
assert int(thief_farm.get("coins", 0)) > 0
assert (get_table("game_plots").find_one(uid=plot["uid"]).get("crop_key") or "") == ""
def _ripen_owner_plot(owner, slot=0):
refresh_snapshot()
owner_user = get_table("users").find_one(username=owner)
farm = store.get_farm(owner_user["uid"])
plot = get_table("game_plots").find_one(farm_uid=farm["uid"], slot_index=slot)
past = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
get_table("game_plots").update(
{"uid": plot["uid"], "planted_at": past, "ready_at": past}, ["uid"]
)
refresh_snapshot()
return plot
def test_steal_cooldown_blocks_second_raid(app_server, seeded_db):
owner_session, owner = _signup()
_reset_farm(owner, coins=100000)
thief_session, thief = _signup()
_reset_farm(thief, coins=0)
owner_session.post(
f"{BASE_URL}/game/plant", data={"slot": 0, "crop": "shell"}, headers=JSON
)
_ripen_owner_plot(owner)
first = thief_session.post(
f"{BASE_URL}/game/farm/{owner}/steal", data={"slot": 0}, headers=JSON
)
assert first.status_code == 200, first.text[:200]
owner_session.post(
f"{BASE_URL}/game/plant", data={"slot": 0, "crop": "shell"}, headers=JSON
)
_ripen_owner_plot(owner)
second = thief_session.post(
f"{BASE_URL}/game/farm/{owner}/steal", data={"slot": 0}, headers=JSON
)
assert second.status_code == 400