diff --git a/core/economy.py b/core/economy.py index b03472b..a53a6ab 100644 --- a/core/economy.py +++ b/core/economy.py @@ -12,6 +12,8 @@ import random from datetime import date, datetime, timedelta, timezone from typing import TypedDict +import aiohttp + from . import pb_client import strings @@ -19,6 +21,11 @@ import strings _txn_log = logging.getLogger("tipiCOIN.txn") +class DatabaseError(Exception): + """Raised when PocketBase is unreachable or returns an error.""" + pass + + def _txn(event: str, **fields) -> None: """Log a single economy transaction to the transactions logger.""" body = " ".join(f"{k}={v}" for k, v in fields.items()) @@ -583,11 +590,15 @@ def format_td(td: timedelta) -> str: async def get_user(user_id: int) -> UserData: """Fetch user data from PocketBase, creating a default record if first seen.""" uid = str(user_id) - record = await pb_client.get_record(uid) - if record is None: - default = _default_user() - default["user_id"] = uid # type: ignore[typeddict-unknown-key] - record = await pb_client.create_record(default) + try: + record = await pb_client.get_record(uid) + if record is None: + default = _default_user() + default["user_id"] = uid # type: ignore[typeddict-unknown-key] + record = await pb_client.create_record(default) + except (aiohttp.ClientError, asyncio.TimeoutError, RuntimeError) as exc: + _log.error("PocketBase unreachable for user %s: %s", user_id, exc) + raise DatabaseError(f"Database unavailable: {exc}") from exc user = _default_user() for key in list(user.keys()): if key in record: @@ -696,7 +707,10 @@ async def _commit(user_id: int, user: UserData) -> None: # /daily # --------------------------------------------------------------------------- async def do_daily(user_id: int) -> dict: - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} @@ -772,7 +786,10 @@ _WORK_JOBS = strings.WORK_JOBS async def do_work(user_id: int) -> dict: - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} @@ -822,7 +839,10 @@ _BEG_JAIL_LINES = strings.BEG_JAIL_LINES async def do_beg(user_id: int) -> dict: - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} @@ -857,7 +877,10 @@ async def do_beg(user_id: int) -> dict: # --------------------------------------------------------------------------- async def do_fish_start(user_id: int) -> dict: """Check cooldown + jail, set cooldown. Call before starting the fishing minigame.""" - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} if jail := _is_jailed(user): @@ -975,7 +998,10 @@ async def do_fishbook(user_id: int) -> dict: # --------------------------------------------------------------------------- async def do_prestige(user_id: int) -> dict: """Prestige: requires level 30, earns PP, resets balance/exp/items/cooldowns.""" - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} @@ -1023,7 +1049,10 @@ async def do_prestige_buy(user_id: int, upgrade_id: str) -> dict: if upgrade_id not in PRESTIGE_SHOP: return {"ok": False, "reason": "not_found"} - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} @@ -1117,7 +1146,10 @@ _CRIME_LOSE = strings.CRIME_LOSE async def do_crime(user_id: int) -> dict: - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} @@ -1210,10 +1242,16 @@ async def do_bail(user_id: int) -> dict: # /rob # --------------------------------------------------------------------------- async def do_rob(robber_id: int, target_id: int) -> dict: - robber = await get_user(robber_id) + try: + robber = await get_user(robber_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if robber.get("eco_banned"): return {"ok": False, "reason": "banned"} - target = await get_user(target_id) + try: + target = await get_user(target_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if cd := _cooldown_remaining(robber, "rob"): return {"ok": False, "reason": "cooldown", "remaining": cd} @@ -1285,7 +1323,10 @@ async def do_rob(robber_id: int, target_id: int) -> dict: # /roulette # --------------------------------------------------------------------------- async def do_roulette(user_id: int, bet: int, colour: str) -> dict: - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} if jail := _is_jailed(user): @@ -1325,7 +1366,10 @@ async def do_roulette(user_id: int, bet: int, colour: str) -> dict: # --------------------------------------------------------------------------- async def do_game_bet(user_id: int, bet: int, outcome: str) -> dict: """Settle a simple win/tie/lose bet. outcome: 'win' | 'tie' | 'lose'.""" - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} if jail := _is_jailed(user): @@ -1378,7 +1422,10 @@ def _spin() -> str: async def do_slots(user_id: int, bet: int) -> dict: - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} if jail := _is_jailed(user): @@ -1431,7 +1478,10 @@ async def do_slots(user_id: int, bet: int) -> dict: # /give # --------------------------------------------------------------------------- async def do_give(giver_id: int, receiver_id: int, amount: int) -> dict: - giver = await get_user(giver_id) + try: + giver = await get_user(giver_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if giver.get("eco_banned"): return {"ok": False, "reason": "banned"} @@ -1441,7 +1491,10 @@ async def do_give(giver_id: int, receiver_id: int, amount: int) -> dict: if giver["balance"] < amount: return {"ok": False, "reason": "insufficient"} - receiver = await get_user(receiver_id) + try: + receiver = await get_user(receiver_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} giver["balance"] -= amount receiver["balance"] += amount giver["total_given"] = giver.get("total_given", 0) + amount @@ -1463,7 +1516,10 @@ async def do_give(giver_id: int, receiver_id: int, amount: int) -> dict: async def do_buy(user_id: int, item_id: str) -> dict: if item_id not in SHOP: return {"ok": False, "reason": "not_found"} - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} @@ -1628,7 +1684,10 @@ async def do_set_reminders(user_id: int, commands: list[str]) -> None: # --------------------------------------------------------------------------- async def do_blackjack_bet(user_id: int, bet: int) -> dict: """Deduct the initial blackjack bet. Returns ok/fail.""" - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} if jail := _is_jailed(user): @@ -1683,7 +1742,10 @@ async def do_get_jailed() -> list[tuple[int, timedelta]]: async def do_heist_check(user_id: int) -> dict: """Check whether a user is eligible to join a heist.""" - user = await get_user(user_id) + try: + user = await get_user(user_id) + except DatabaseError: + return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} if jail := _is_jailed(user): diff --git a/core/pb_client.py b/core/pb_client.py index 59634b8..25d5baa 100644 --- a/core/pb_client.py +++ b/core/pb_client.py @@ -28,7 +28,7 @@ PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD ECONOMY_COLLECTION = config.PB_ECONOMY_COLLECTION -_TIMEOUT = aiohttp.ClientTimeout(total=10) +_TIMEOUT = aiohttp.ClientTimeout(total=4) # --------------------------------------------------------------------------- # Persistent session (created once, reused for the lifetime of the process)