forked from sass/tipibot
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:
25
commands/_replies.py
Normal file
25
commands/_replies.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Shared reply helpers for command handlers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import discord
|
||||
|
||||
import strings as S
|
||||
|
||||
|
||||
async def reply_db_error(interaction: discord.Interaction) -> None:
|
||||
"""Tell the user the database is unavailable.
|
||||
|
||||
Economy core functions return {"ok": False, "reason": "db_error"} on a
|
||||
PocketBase outage. Without this, handlers either fall through silently (a
|
||||
deferred interaction hangs on "thinking...") or show a misleading "you're
|
||||
broke" message. Works whether or not the interaction was already deferred.
|
||||
"""
|
||||
msg = S.ERR["db_error"]
|
||||
try:
|
||||
if interaction.response.is_done():
|
||||
await interaction.followup.send(msg, ephemeral=True)
|
||||
else:
|
||||
await interaction.response.send_message(msg, ephemeral=True)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
@@ -13,6 +13,8 @@ from core import economy
|
||||
from core.emoji import EMOJI as E
|
||||
import strings as S
|
||||
|
||||
from ._replies import reply_db_error
|
||||
|
||||
|
||||
def register_economy_extra_commands(
|
||||
tree: app_commands.CommandTree,
|
||||
@@ -120,6 +122,9 @@ def register_economy_extra_commands(
|
||||
return
|
||||
res = await economy.do_heist_check(interaction.user.id)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "jailed":
|
||||
@@ -270,6 +275,9 @@ def register_economy_extra_commands(
|
||||
return
|
||||
res = await economy.do_heist_check(interaction.user.id)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "jailed":
|
||||
@@ -518,6 +526,9 @@ def register_economy_extra_commands(
|
||||
|
||||
res = await economy.do_give(interaction.user.id, kasutaja.id, summa_int)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "jailed":
|
||||
@@ -751,14 +762,13 @@ def register_economy_extra_commands(
|
||||
@tree.command(name="leaderboard", description=S.CMD["leaderboard"])
|
||||
async def cmd_leaderboard(interaction: discord.Interaction):
|
||||
await interaction.response.defer()
|
||||
coins_raw, exp_raw, season_raw, prestige_raw, wagered_raw, fish_raw = await asyncio.gather(
|
||||
economy.get_leaderboard(top_n=None),
|
||||
economy.get_leaderboard_exp(top_n=None),
|
||||
economy.get_leaderboard_season_exp(top_n=None),
|
||||
economy.get_leaderboard_prestige(top_n=None),
|
||||
economy.get_leaderboard_wagered(top_n=None),
|
||||
economy.get_leaderboard_fish(top_n=None),
|
||||
)
|
||||
lbs = await economy.get_all_leaderboards() # single collection scan for all six tabs
|
||||
coins_raw = lbs["coins"]
|
||||
exp_raw = lbs["exp"]
|
||||
season_raw = lbs["season"]
|
||||
prestige_raw = lbs["prestige"]
|
||||
wagered_raw = lbs["wagered"]
|
||||
fish_raw = lbs["fish"]
|
||||
|
||||
house_entry = None
|
||||
regular = []
|
||||
@@ -865,6 +875,9 @@ def register_economy_extra_commands(
|
||||
async def cmd_buy(interaction: discord.Interaction, ese: app_commands.Choice[str]):
|
||||
res = await economy.do_buy(interaction.user.id, ese.value)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "owned":
|
||||
|
||||
@@ -10,6 +10,8 @@ from discord import app_commands
|
||||
from core import economy
|
||||
import strings as S
|
||||
|
||||
from ._replies import reply_db_error
|
||||
|
||||
|
||||
def register_economy_fish_commands(
|
||||
tree: app_commands.CommandTree,
|
||||
@@ -218,6 +220,9 @@ def register_economy_fish_commands(
|
||||
|
||||
res = await economy.do_fish_start(interaction.user.id)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "cooldown":
|
||||
|
||||
@@ -12,6 +12,8 @@ from core import economy
|
||||
from core.emoji import EMOJI as E
|
||||
import strings as S
|
||||
|
||||
from ._replies import reply_db_error
|
||||
|
||||
|
||||
def register_economy_games_commands(
|
||||
tree: app_commands.CommandTree,
|
||||
@@ -116,6 +118,9 @@ def register_economy_games_commands(
|
||||
res = await economy.do_roulette(interaction.user.id, panus_int, värv.value)
|
||||
if not res["ok"]:
|
||||
active_games.discard(interaction.user.id)
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "jailed":
|
||||
@@ -202,7 +207,14 @@ def register_economy_games_commands(
|
||||
bet_line = ""
|
||||
if self.bet > 0:
|
||||
res = await economy.do_game_bet(interaction.user.id, self.bet, outcome)
|
||||
if outcome == "win":
|
||||
if not res.get("ok"):
|
||||
if res.get("reason") == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
# Player was jailed/went broke since the duel started - show the
|
||||
# result without a bet line rather than crashing on res["balance"].
|
||||
bet_line = ""
|
||||
elif outcome == "win":
|
||||
bet_line = S.RPS_UI["bet_win"].format(amount=coin(self.bet), balance=coin(res["balance"]))
|
||||
elif outcome == "lose":
|
||||
bet_line = S.RPS_UI["bet_lose"].format(amount=coin(self.bet), balance=coin(res["balance"]))
|
||||
@@ -652,6 +664,9 @@ def register_economy_games_commands(
|
||||
res = await economy.do_slots(interaction.user.id, panus_int)
|
||||
if not res["ok"]:
|
||||
active_games.discard(interaction.user.id)
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
return
|
||||
@@ -1017,6 +1032,9 @@ def register_economy_games_commands(
|
||||
try:
|
||||
res = await economy.do_blackjack_bet(self.user_id, self.bet)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
await interaction.response.send_message(
|
||||
S.ERR["broke"].format(bal=coin(res.get("balance", 0))), ephemeral=True
|
||||
)
|
||||
@@ -1042,6 +1060,9 @@ def register_economy_games_commands(
|
||||
try:
|
||||
res = await economy.do_blackjack_bet(self.user_id, self.bet)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
await interaction.response.send_message(
|
||||
S.ERR["broke"].format(bal=coin(res.get("balance", 0))), ephemeral=True
|
||||
)
|
||||
@@ -1101,6 +1122,9 @@ def register_economy_games_commands(
|
||||
|
||||
res = await economy.do_blackjack_bet(interaction.user.id, bet)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "jailed":
|
||||
|
||||
@@ -10,6 +10,8 @@ from discord import app_commands
|
||||
from core import economy
|
||||
import strings as S
|
||||
|
||||
from ._replies import reply_db_error
|
||||
|
||||
|
||||
def register_economy_income_commands(
|
||||
tree: app_commands.CommandTree,
|
||||
@@ -25,6 +27,9 @@ def register_economy_income_commands(
|
||||
await interaction.response.defer()
|
||||
res = await economy.do_daily(interaction.user.id)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.followup.send(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "cooldown":
|
||||
@@ -63,6 +68,9 @@ def register_economy_income_commands(
|
||||
await interaction.response.defer()
|
||||
res = await economy.do_work(interaction.user.id)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.followup.send(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "cooldown":
|
||||
@@ -97,6 +105,9 @@ def register_economy_income_commands(
|
||||
await interaction.response.defer()
|
||||
res = await economy.do_beg(interaction.user.id)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.followup.send(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "cooldown":
|
||||
@@ -128,6 +139,9 @@ def register_economy_income_commands(
|
||||
await interaction.response.defer()
|
||||
res = await economy.do_crime(interaction.user.id)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.followup.send(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "cooldown":
|
||||
@@ -187,6 +201,9 @@ def register_economy_income_commands(
|
||||
await interaction.response.defer()
|
||||
res = await economy.do_rob(interaction.user.id, sihtmärk.id)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.followup.send(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "cooldown":
|
||||
|
||||
@@ -9,6 +9,8 @@ from discord import app_commands
|
||||
from core import economy
|
||||
import strings as S
|
||||
|
||||
from ._replies import reply_db_error
|
||||
|
||||
|
||||
def register_prestige_commands(
|
||||
tree: app_commands.CommandTree,
|
||||
@@ -127,6 +129,9 @@ def register_prestige_commands(
|
||||
return
|
||||
await interaction.response.defer()
|
||||
res = await economy.do_prestige(self.user_id)
|
||||
if not res["ok"] and res.get("reason") == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
self.clear_items()
|
||||
if not res["ok"]:
|
||||
embed = discord.Embed(
|
||||
@@ -166,6 +171,9 @@ def register_prestige_commands(
|
||||
await interaction.response.defer()
|
||||
res = await economy.do_prestige_buy(self.user_id, upgrade_id)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "insufficient_pp":
|
||||
err = S.PRESTIGE_UI["buy_no_pp"].format(have=res["have"], need=res["need"])
|
||||
elif res["reason"] == "maxed":
|
||||
@@ -221,6 +229,9 @@ def register_prestige_commands(
|
||||
return
|
||||
res = await economy.do_prestige_buy(interaction.user.id, upgrade.strip().lower())
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "not_found":
|
||||
|
||||
@@ -8,6 +8,8 @@ from discord import app_commands
|
||||
from core import economy
|
||||
import strings as S
|
||||
|
||||
from ._replies import reply_db_error
|
||||
|
||||
|
||||
def register_economy_support_commands(
|
||||
tree: app_commands.CommandTree,
|
||||
@@ -47,6 +49,9 @@ def register_economy_support_commands(
|
||||
res = await economy.do_give(interaction.user.id, self._view.requester.id, amount)
|
||||
if not res["ok"]:
|
||||
self._view.remaining += amount # roll back the reservation
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
data = await economy.get_user(interaction.user.id)
|
||||
await interaction.response.send_message(
|
||||
S.ERR["broke"].format(bal=coin(data["balance"])), ephemeral=True
|
||||
@@ -212,6 +217,9 @@ def register_economy_support_commands(
|
||||
|
||||
res = await economy.do_buy_consumable(interaction.user.id, ese.value)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "insufficient":
|
||||
@@ -288,6 +296,9 @@ def register_economy_support_commands(
|
||||
|
||||
res = await economy.do_vanity_select(interaction.user.id, ese.value)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
return
|
||||
if res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
elif res["reason"] == "insufficient":
|
||||
|
||||
Reference in New Issue
Block a user