forked from sass/tipibot
Architecture overview, dev/test commands, the dual-profile and re-export patterns, the economy locking/PocketBase-schema footguns, and the test harness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013VbAVsrZuYesea99mPMmPT
67 lines
5.6 KiB
Markdown
67 lines
5.6 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
## What this is
|
|
|
|
Discord bot for the TipiLAN community (`discord.py`). Two responsibilities: **member management** (syncs nicknames/roles from a Google Sheet, birthday announcements) and the **TipiCOIN economy** (a large game economy stored in PocketBase). User-facing text is **Estonian**; only `docs/PATCHNOTES.md` content is English.
|
|
|
|
See `README.md` for the full feature/gameplay spec and `docs/DEV_NOTES.md` for the developer reference (add-a-command / add-a-shop-item checklists, constants).
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
# Run tests (pure logic + economy flows against an in-memory PocketBase fake)
|
|
python -m pytest tests/ -q
|
|
|
|
# Single file / single test
|
|
python -m pytest tests/test_economy_pure.py -q
|
|
python -m pytest tests/test_economy_flows.py::TestDaily::test_streak -q
|
|
|
|
# Run the bot (requires .env; PocketBase must already be running)
|
|
BOT_PROFILE=dev python bot.py # or BOT_PROFILE=economy
|
|
```
|
|
|
|
Deps: `pip install -r requirements-dev.txt` (dev = runtime + pytest). CI (`.gitea/workflows/deploy.yml`) runs `pytest tests/ -q` on push to `master`, then deploys to the host by restarting `tipibot_dev` (canary) and `tipibot_eco` systemd units. The bot runs on host `tipilan-bots:/root/tipibot`; the local checkout has no `.env`/live DB.
|
|
|
|
## Architecture
|
|
|
|
Three layers, deliberately decoupled:
|
|
|
|
- **`core/`** — domain logic, **no Discord objects**. `core/economy/` is the economy package.
|
|
- **`commands/`** — one Discord slash-command group per file. Each exposes `register_<group>_commands(tree, bot, ...)`.
|
|
- **`bot.py`** — thin wiring layer: Discord client, event handlers (`on_ready`, `on_member_join`), background tasks, shared helpers, and it calls every `register_*_commands(...)` on startup, passing shared helpers down (`coin`, `cd_ts`, `award_exp`, `maybe_remind`, `parse_amount`).
|
|
|
|
### Dual-profile design (dev vs economy)
|
|
|
|
`BOT_PROFILE` env var (`dev` | `economy`) selects **everything** at import time in `config.py`: which Discord token, guild ID, birthday channel, and **PocketBase collection** (`economy_users_dev` vs `economy_users_prod`) are used. Legacy non-suffixed env vars act as fallbacks. Logs and `data/` are also namespaced per profile (`logs/<profile>/`, `data/<profile>/`). `/check`, `/member`, `/birthdays` are **dev-profile only**. When adding config, follow the `_DEV` / `_ECONOMY` + legacy-fallback pattern.
|
|
|
|
### The re-export pattern (important — two places)
|
|
|
|
Both `strings/` and `core/economy/` are packages split into submodules but **re-exported flat** through their `__init__.py`, so callers use `import strings as S; S.NAME` and `from core import economy; economy.do_daily(...)` unchanged.
|
|
|
|
- **Edit the submodule**, not the `__init__`. Strings live in `strings/{common,commands,member,economy,admin,games,fishing}.py`; economy logic in `core/economy/{store,income,gambling,fishing,jail,heist,prestige,shop,levels,quests,leaderboards,house,admin}.py`.
|
|
- `tests/test_strings.py` guards that every submodule name is re-exported — a new string that isn't re-exported fails CI.
|
|
- Caveat (see `tests/conftest.py:93`): the re-exported `economy.house` mutable state is a snapshot; live house state is owned by `economy.house`. Patch the submodule, not the alias.
|
|
|
|
### Economy persistence & concurrency (`core/economy/store.py`)
|
|
|
|
- Every mutation is a **read-modify-write**: `get_user(id)` → mutate the `UserData` dict → `await _commit(id, user)`. Money/EXP changes must also call `_txn(...)` for the transaction log.
|
|
- **Per-user async locks** serialize commits. Public mutating functions are decorated with `@_locked_by(<arg positions of user ids>)`. Two hard rules to avoid deadlock/lost-writes:
|
|
1. A `@_locked_by` function must **never call another `@_locked_by` function**.
|
|
2. House balance changes go through `_credit_house` (an atomic PocketBase increment, no lock) so they're safe to call while holding user locks.
|
|
Multi-user functions (`do_give`, `do_rob`) list both positions and acquire locks in sorted order.
|
|
- **PocketBase silently drops writes to fields not in the collection schema** — this is the #1 footgun. Adding any new persisted field means adding it to `_default_user()` **and** running `python scripts/sync_pb_schema.py` (reconciles both dev + prod collections). `store.missing_schema_fields()` / `/status` surface drift.
|
|
|
|
### Command result convention
|
|
|
|
`core.economy` functions return a dict with a `"reason"` key describing the outcome (success, `"cooldown"`, `"jailed"`, `"banned"`, insufficient funds, ...). The command handler in `commands/` maps every possible `res["reason"]` to a string/embed. On success it calls `award_exp(interaction, economy.EXP_REWARDS["<cmd>"])` and `maybe_remind(...)` where relevant.
|
|
|
|
## Adding an economy command or shop item
|
|
|
|
Follow the exact ordered checklists in `docs/DEV_NOTES.md` ("Adding a New Economy Command", "Adding a New Shop Item"). They enumerate every touchpoint across `core/economy/`, `strings/`, `commands/`, and `bot.py` (cooldowns, EXP rewards, help embed, reminders, item-modified cooldown branches). Missing a step generally means a silently broken feature rather than an error.
|
|
|
|
## Tests
|
|
|
|
No `pytest-asyncio`. Async tests wrap coroutines with the `run(coro)` helper from `tests/conftest.py` (which is `asyncio.run`). The `fake_pb` fixture monkeypatches `core.pb_client` with an in-memory `FakePocketBase` that mimics real PocketBase behaviour — including atomic `field+`/`field-` increments and **silently dropping writes to fields missing from the schema** (use `fake_pb_without_quest_fields` to simulate pre-migration schema drift). Prefer testing `core/` logic directly; there's no Discord in the test path.
|