fix(economy): handle db_error in commands, dedupe cooldowns, single leaderboard scan

Three robustness/perf fixes from the review:

1. db_error UX: economy core returns {"ok": False, "reason": "db_error"} on a
   PocketBase outage, but no handler expected it - deferred commands hung on
   "thinking..." and others showed a misleading "you're broke". Added a shared
   reply_db_error helper (commands/_replies.py) + S.ERR["db_error"], and wired a
   db_error branch into every handler that can receive it (daily/work/beg/crime/
   rob/give/buy/roulette/slots/blackjack/heist/fish/prestige/vanity/consumables/
   request-funding). Also fixed a latent KeyError in vs-bot RPS that read
   res["balance"] without checking res["ok"].

2. Deduped the item->cooldown mapping that was copied in do_daily/do_work/do_beg,
   do_fish_start, _maybe_remind and _restore_reminders. Single source of truth:
   store.ITEM_COOLDOWNS + effective_cooldown(cmd, items).

3. /leaderboard did six full-collection scans (one per tab). Added
   get_all_leaderboards() which scans once and builds all six views in memory.

Tests: effective_cooldown cases, and get_all_leaderboards matches the individual
queries + scans the collection exactly once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VbAVsrZuYesea99mPMmPT
This commit is contained in:
Rene Arumetsa
2026-09-04 02:26:07 +03:00
parent 7a28617657
commit eb7346b851
15 changed files with 249 additions and 41 deletions

View File

@@ -3,12 +3,11 @@
from __future__ import annotations
import random
from datetime import timedelta
from ..pb_client import DatabaseError
from .store import (
COOLDOWNS, _cooldown_remaining, _commit, _is_jailed, _locked_by, _now,
_prestige_mult, _txn, get_user,
_cooldown_remaining, _commit, _is_jailed, _locked_by, _now,
_prestige_mult, _txn, effective_cooldown, get_user,
)
@@ -81,7 +80,7 @@ async def do_fish_start(user_id: int) -> dict:
if jail := _is_jailed(user):
return {"ok": False, "reason": "jailed", "remaining": jail}
fish_cd = timedelta(seconds=90) if "ussipurk" in user["items"] else COOLDOWNS["fish"]
fish_cd = effective_cooldown("fish", user["items"])
if cd := _cooldown_remaining(user, "fish", override_cd=fish_cd):
return {"ok": False, "reason": "cooldown", "remaining": cd}

View File

@@ -3,15 +3,16 @@
from __future__ import annotations
import random
from datetime import date, timedelta
from datetime import date
import strings
from ..pb_client import DatabaseError
from . import house
from .store import (
COOLDOWNS, JAIL_DURATION, PRESTIGE_SHOP, _commit, _cooldown_remaining,
_is_jailed, _locked_by, _log, _now, _prestige_mult, _txn, get_user,
JAIL_DURATION, PRESTIGE_SHOP, _commit, _cooldown_remaining,
_is_jailed, _locked_by, _log, _now, _prestige_mult, _txn, effective_cooldown,
get_user,
)
from .house import _credit_house
from .consumables import earn_mult
@@ -29,7 +30,7 @@ async def do_daily(user_id: int) -> dict:
if user.get("eco_banned"):
return {"ok": False, "reason": "banned"}
daily_cd = timedelta(hours=18) if "korvaklapid" in user["items"] else COOLDOWNS["daily"]
daily_cd = effective_cooldown("daily", user["items"])
if cd := _cooldown_remaining(user, "daily", override_cd=daily_cd):
return {"ok": False, "reason": "cooldown", "remaining": cd}
@@ -109,7 +110,7 @@ async def do_work(user_id: int) -> dict:
if user.get("eco_banned"):
return {"ok": False, "reason": "banned"}
work_cd = timedelta(minutes=40) if "monitor" in user["items"] else COOLDOWNS["work"]
work_cd = effective_cooldown("work", user["items"])
if cd := _cooldown_remaining(user, "work", override_cd=work_cd):
return {"ok": False, "reason": "cooldown", "remaining": cd}
if jail := _is_jailed(user):
@@ -163,7 +164,7 @@ async def do_beg(user_id: int) -> dict:
if user.get("eco_banned"):
return {"ok": False, "reason": "banned"}
beg_cd = timedelta(minutes=3) if "hiirematt" in user["items"] else COOLDOWNS["beg"]
beg_cd = effective_cooldown("beg", user["items"])
if cd := _cooldown_remaining(user, "beg", override_cd=beg_cd):
return {"ok": False, "reason": "cooldown", "remaining": cd}

View File

@@ -80,3 +80,32 @@ async def get_leaderboard_fish(top_n: int | None = 10) -> list[tuple[str, int]]:
reverse=True,
)
return result if top_n is None else result[:top_n]
async def get_all_leaderboards() -> dict[str, list[tuple]]:
"""Build every leaderboard view from a SINGLE collection scan.
/leaderboard shows six tabs; calling each get_leaderboard_* separately would
read the whole collection six times. This reads once and sorts in memory,
returning the same tuple shapes the individual functions produce (unbounded -
the command paginates)."""
records = await pb_client.list_all_records()
users = [r for r in records if r.get("user_id")]
def desc(keyfn) -> list[dict]:
return sorted(users, key=keyfn, reverse=True)
return {
"coins": [(r["user_id"], r.get("balance", 0))
for r in desc(lambda r: r.get("balance", 0))],
"exp": [(r["user_id"], r.get("exp", 0), get_level(r.get("exp", 0)))
for r in desc(lambda r: r.get("exp", 0))],
"season": [(r["user_id"], r.get("season_total_exp", 0), r.get("prestige_level", 0))
for r in desc(lambda r: r.get("season_total_exp", 0))],
"prestige": [(r["user_id"], r.get("prestige_level", 0), r.get("prestige_points", 0))
for r in desc(lambda r: (r.get("prestige_level", 0), r.get("prestige_points", 0)))],
"wagered": [(r["user_id"], r.get("total_wagered", 0))
for r in desc(lambda r: r.get("total_wagered", 0))],
"fish": [(r["user_id"], r.get("total_fish_caught", 0))
for r in desc(lambda r: r.get("total_fish_caught", 0))],
}

View File

@@ -87,6 +87,26 @@ COOLDOWNS: dict[str, timedelta] = {
"fish": timedelta(minutes=2),
}
# Items that shorten a command's cooldown: command -> (item_id, reduced cooldown).
# Single source of truth so the cooldown check (do_*), the reminder scheduler
# (_maybe_remind) and the restart restore (_restore_reminders) never drift apart.
ITEM_COOLDOWNS: dict[str, tuple[str, timedelta]] = {
"work": ("monitor", timedelta(minutes=40)),
"beg": ("hiirematt", timedelta(minutes=3)),
"daily": ("korvaklapid", timedelta(hours=18)),
"fish": ("ussipurk", timedelta(seconds=90)),
}
def effective_cooldown(cmd: str, items) -> timedelta | None:
"""The cooldown for `cmd` given the user's owned `items`, applying any
item-based reduction. Returns None for commands with no cooldown."""
override = ITEM_COOLDOWNS.get(cmd)
if override is not None and override[0] in items:
return override[1]
return COOLDOWNS.get(cmd)
JAIL_DURATION = timedelta(minutes=30)
HEIST_JAIL = timedelta(hours=1, minutes=30)