Vanity shop (/vanity): cosmetic badges/titles as a pure whale coin sink - purchases burn coins (not credited to the house) and equip a badge shown on /profile. New vanity_owned/vanity_active fields, schema sync, and tests. Money-safety and robustness fixes from a codebase review: - _parse_amount now rejects negative amounts. Every bet/give/request flows through it, so a negative value can no longer mint coins on a loss/transfer path that trusts the caller's sign (all call sites already guarded <= 0; this closes the source). - do_blackjack_payout no longer raises on a DB failure. The stake was already deducted in do_blackjack_bet, so it now logs critical with the owed amount (for admin reconciliation) and returns db_error; all payout call sites render a clear "payout failed" notice instead of crashing the interaction. - Instant "kohv" consumable now cancels the pending reminder DMs for the cooldowns it wipes (via new INSTANT_RESET_COMMANDS), so no stale/duplicate reminders fire. - Renamed the misleadingly-named _refund_user_safe -> _debit_house_safe (it debits the house) and dropped its ignored first arg. - Added __all__ to vanity.py and consumables.py so `import *` no longer leaks incidental imports into the economy namespace. - Documented Kõrvaklapid's +25 coin daily bonus in README and DEV_NOTES. Tests: blackjack payout DB-failure safety and INSTANT_RESET_COMMANDS lockstep. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VbAVsrZuYesea99mPMmPT
93 lines
3.2 KiB
Python
93 lines
3.2 KiB
Python
"""House account: the bot's own balance, fed by fines and lost bets."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .. import pb_client
|
|
from ..pb_client import DatabaseError
|
|
from .store import _log, _now, get_user, _commit
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# House account (bot user)
|
|
# ---------------------------------------------------------------------------
|
|
HOUSE_ID: int | None = None
|
|
_house_pb_id: str | None = None
|
|
|
|
|
|
def set_house(user_id: int) -> None:
|
|
"""Register the bot's Discord user ID as the house account."""
|
|
global HOUSE_ID, _house_pb_id
|
|
if HOUSE_ID != user_id:
|
|
_house_pb_id = None
|
|
HOUSE_ID = user_id
|
|
|
|
|
|
async def _house_record_id() -> str | None:
|
|
"""PocketBase record id of the house account (cached; creates the record on first use)."""
|
|
global _house_pb_id
|
|
if HOUSE_ID is None:
|
|
return None
|
|
if _house_pb_id is None:
|
|
house = await get_user(HOUSE_ID)
|
|
_house_pb_id = house.get("_pb_id") # type: ignore[typeddict-item]
|
|
return _house_pb_id
|
|
|
|
|
|
async def _credit_house(amount: int) -> None:
|
|
"""Add `amount` coins to the house via an atomic PocketBase increment.
|
|
|
|
Deliberately lock-free: callers hold per-user locks, so this must never
|
|
acquire one itself (see the locking rules above)."""
|
|
if amount <= 0:
|
|
return
|
|
record_id = await _house_record_id()
|
|
if record_id is None:
|
|
return
|
|
await pb_client.update_record(record_id, {"balance+": amount})
|
|
|
|
|
|
async def get_heist_global_cd() -> float:
|
|
"""Return unix timestamp until which no new heist can start. Persisted on house record."""
|
|
if HOUSE_ID is None:
|
|
return 0.0
|
|
house = await get_user(HOUSE_ID)
|
|
return float(house.get("heist_global_cd_until") or 0)
|
|
|
|
|
|
async def set_heist_global_cd(until: float) -> None:
|
|
"""Persist heist global cooldown expiry to the house account in PocketBase."""
|
|
record_id = await _house_record_id()
|
|
if record_id is None:
|
|
return
|
|
await pb_client.update_record(record_id, {"heist_global_cd_until": until})
|
|
|
|
|
|
async def _refund_house_safe(amount: int, context: str, related_uid: int) -> None:
|
|
"""Best-effort refund of `amount` to the house. Logs critical if it fails."""
|
|
if HOUSE_ID is None or amount <= 0:
|
|
return
|
|
try:
|
|
await _credit_house(amount)
|
|
except DatabaseError as exc:
|
|
_log.critical(
|
|
"House compensation failed (%s, related uid %s, amount %s): %s",
|
|
context, related_uid, amount, exc,
|
|
)
|
|
|
|
|
|
async def _debit_house_safe(amount: int, context: str, uid: int) -> None:
|
|
"""Best-effort atomic debit of `amount` from the house. Compensates a fine
|
|
that was credited to the house but whose matching user debit failed to persist
|
|
(the coins must be pulled back out of the house). Logs critical if it fails."""
|
|
if HOUSE_ID is None or amount <= 0:
|
|
return
|
|
try:
|
|
record_id = await _house_record_id()
|
|
if record_id:
|
|
await pb_client.update_record(record_id, {"balance-": amount})
|
|
except DatabaseError as exc:
|
|
_log.critical(
|
|
"House debit compensation failed (%s, related uid %s, amount %s): %s",
|
|
context, uid, amount, exc,
|
|
)
|