18 KiB
TipiLAN Bot - Developer Reference
File Structure
The codebase is split into core/ (domain logic), commands/ (Discord slash command handlers), and a thin bot.py that wires everything together.
Top level
| File | Purpose |
|---|---|
bot.py |
Discord client, event handlers (on_ready, on_member_join, ...), background tasks (presence rotation, daily birthday loop), shared helpers (_award_exp, _maybe_remind, _parse_amount, _PAUSED), and register_*_commands(...) wiring for every command module |
strings/ |
Single source of truth for all user-facing text, split into domain submodules (common, commands, member, economy, admin, games, fishing) and re-exported via strings/__init__.py so import strings / strings.NAME works unchanged. Edit the relevant submodule to change any message. |
config.py |
Environment variables (TOKEN, GUILD_ID, PB_URL, etc.) |
core/ - domain logic, no Discord coupling
| File | Purpose |
|---|---|
core/economy/ |
Economy business logic package, re-exported via core/economy/__init__.py so callers use from core import economy + attribute access (economy.do_daily, economy.SHOP, ...). Submodules: store.py (user records, per-user locks, COOLDOWNS, ITEM_COOLDOWNS/effective_cooldown, JAIL_DURATION, COIN, get_user/_commit/_txn, pending_wager escrow + reconcile_pending_wagers), income.py, gambling.py, fishing.py, jail.py, heist.py, prestige.py, shop.py, consumables.py (timed buffs + grant_buff), vanity.py (cosmetic badges), lootbox.py (mystery box), bank.py (rob-proof vault), achievements.py (milestone badges), lottery.py (daily draw), levels.py, quests.py, leaderboards.py (incl. get_all_leaderboards/get_economy_stats, net-worth = balance+bank), house.py, admin.py |
core/pb_client.py |
Async PocketBase REST client - auth token cache, CRUD on economy_users collection |
core/sheets.py |
Google Sheets integration (member sync) |
core/member_sync.py |
Birthday/member sync helpers, plus tournament team-role sync + divider placement |
commands/ - one slash-command group per file
Each file exposes a register_<group>_commands(tree, bot, ...) function. bot.py calls them all once on startup, passing in shared helpers (coin, cd_ts, award_exp, maybe_remind, parse_amount, ...).
| File | Commands / Responsibility |
|---|---|
commands/dev_member_commands.py |
/check, /member (dev profile only) |
commands/dev_member_runtime.py |
on_member_join flow + birthday_daily task body |
commands/economy_income_commands.py |
/daily, /work, /beg, /crime, /rob |
commands/economy_games_commands.py |
/roulette, /slots, /blackjack, /rps |
commands/economy_extra_commands.py |
/heist, /jailbreak, /reminders, /request, /give, /lottery, /leaderboard, ... |
commands/economy_fish_commands.py |
/fish, /fishbook, /fishsell |
commands/economy_profile_commands.py |
/balance, /rank, /stats, /cooldowns, /achievements |
commands/economy_support_commands.py |
/shop, /buy, /economysetup, /consumables, /vanity, /lootbox, /bank, /deposit, /withdraw |
commands/economy_prestige_commands.py |
/prestige, /prestigeshop, /prestigebuy |
commands/economy_admin_commands.py |
/admincoins, /adminexp, /adminitem, /adminjail, /adminban, /adminreset, /adminview |
commands/ops_admin_commands.py |
/sync, /restart, /shutdown, /pause, /send, /status |
commands/ops_channel_commands.py |
Channel allowlist commands |
commands/info_commands.py |
/patchnotes and other lightweight info commands |
scripts/
| File | Purpose |
|---|---|
scripts/migrate_to_pb.py |
One-time legacy migration: data/economy.json → PocketBase. Only relevant if you still have a pre-PB JSON store. |
scripts/add_stats_fields.py |
Schema migration: adds new fields to the economy_users collection. Idempotent. |
scripts/reset_pb_collections.py |
Destructive - deletes and recreates the dev + economy collections. Requires --confirm. Use only in dev. |
Adding a New Economy Command
Pick the commands/economy_*_commands.py file that matches the new command's category (income, games, profile, ...) and add the handler inside its register_*_commands function. If none fit, create a new module and register it from bot.py.
Checklist - do all of these, in order:
core/economy/<area>.py(e.g.income.py,gambling.py,fishing.py) - add thedo_<cmd>async function with cooldown check, logic,_commit, and_txnlogging (get_user/_commit/_txnlive instore.py)core/economy/store.py- add the cooldown toCOOLDOWNSdict if it has one. Compute the effective cooldown indo_<cmd>witheffective_cooldown("<cmd>", user["items"])rather than an inlinetimedelta(...) if item in items else ...core/economy/levels.py- add the EXP reward toEXP_REWARDSdictstrings/commands.pyCMD- add the slash command descriptionstrings/commands.pyOPT- add any parameter descriptionsstrings/common.pyTITLE- add embed title(s) for success/fail statesstrings/common.pyERR- add any error messages (banned, cooldown usesCD_MSG, jailed usesCD_MSG["jailed"])strings/common.pyCD_MSG- add cooldown message if command has a cooldownstrings/commands.pyHELP_CATEGORIES["tipibot"]["fields"]- add the command to the help embedcommands/economy_<group>_commands.py- insideregister_*_commands, add@tree.command(name="<cmd>", ...)cmd_<name>; handle allres["reason"]cases. Ifdo_<cmd>can returndb_error(any function that callsget_user/_commit), handle it first in the failure block withawait reply_db_error(interaction); return(import from._replies) - otherwise a DB outage hangs the deferred interaction or shows a misleading messagecommands/economy_<group>_commands.py- callmaybe_remind(user_id, "<cmd>")if the command has a cooldown and reminders make sense (the helper is passed in via theregister_*signature)commands/economy_<group>_commands.py- callawait award_exp(interaction, economy.EXP_REWARDS["<cmd>"])on successstrings/common.pyREMINDER_OPTS- add a reminder option if the command needs onecore/economy/store.pyITEM_COOLDOWNS- if an item shortens the command's cooldown, add"<cmd>": ("<item_id>", timedelta(...))here. This is the single source of truth:effective_cooldown(used bydo_<cmd>),_maybe_remind, and_restore_remindersall read it, so you no longer edit the reminder helpers by hand
Adding a New Shop Item
Checklist:
core/economy/shop.pySHOP- add the item dict{name, emoji, cost, description: strings.ITEM_DESCRIPTIONS["key"]}core/economy/shop.pySHOP_TIERS- add the key to the correct tier list (1/2/3)core/economy/shop.pySHOP_LEVEL_REQ- add minimum level if it is T2 (≥10) or T3 (≥20)strings/economy.pyITEM_DESCRIPTIONS- add the item description (Estonian flavour + English effect)strings/commands.pyHELP_CATEGORIES["shop"]["fields"]- add display entry (sorted by cost)- If the item modifies a cooldown:
core/economy/<area>.py- add theif "item" in user["items"]branch in the relevantdo_<cmd>functionbot.py_maybe_remind- addelif cmd == "<cmd>" and "<item>" in items:branch with the new delaycommands/economy_profile_commands.pycmd_cooldowns- add the item annotation to the relevant status line
Adding a New Level Role
core/economy/levels.pyLEVEL_ROLES- add(min_level, "RoleName")in descending level order (highest first)bot.py_ensure_level_role- no changes needed (usesLEVEL_ROLESdynamically)- Run
/economysetupin the server to create the role and set its position
Adding a New Admin Command
strings/commands.pyCMD- add"[Admin] ..."descriptionstrings/commands.pyHELP_CATEGORIES["admin"]["fields"]- add the entrycommands/economy_admin_commands.py(orcommands/ops_admin_commands.pyfor non-economy ops) - add the handler with@app_commands.default_permissions(manage_guild=True)and@app_commands.guild_only()
Economy System Design
Storage
All economy state is stored in PocketBase (economy_users collection). core/pb_client.py owns all reads/writes. Each do_* function in core/economy/ calls get_user() (from store.py) → mutates the local dict → calls _commit(). _commit does a PATCH to PocketBase.
Currency & Income Sources
| Command | Cooldown | Base Earn | Notes |
|---|---|---|---|
/daily |
20h (18h w/ korvaklapid) | 150⬡ | ×streak multiplier, ×2 w/ lan_pass, +25⬡ w/ korvaklapid, +5% interest w/ gaming_laptop |
/work |
1h (40min w/ monitor) | 15-75⬡ | ×1.5 w/ gaming_hiir, ×1.25 w/ reguleeritav_laud, ×3 30% chance w/ energiajook |
/beg |
5min (3min w/ hiirematt) | 10-40⬡ | ×2 w/ klaviatuur |
/crime |
2h | 200-500⬡ win | 60% success (75% w/ cat6), +30% w/ mikrofon; fail = fine + jail |
/rob |
2h | 10–25% of target | 45% success (60% w/ jellyfin); fail = fine; house rob: 35% success, 5–40% jackpot |
/slots |
- | varies | jackpot=10× (15× w/ monitor_360), triple=4× (6×), pair=1× |
/roulette |
- | 2× red/black, 14× green | 1/37 green chance |
/blackjack |
- | 1:1 win, 3:2 BJ, 2:1 double | Dealer stands on 17+; double down on first action only |
"all" Keyword
Commands that accept a coin amount (/give, /roulette, /rps, /slots, /blackjack) accept "all" to mean the user's full current balance. Parsed by _parse_amount(value, balance) in bot.py.
Daily Streak Multipliers
- 1-2 days: ×1.0 (150⬡)
- 3-6 days: ×1.5 (225⬡)
- 7-13 days: ×2.0 (300⬡)
- 14+ days: ×3.0 (450⬡)
karikasitem: streak survives missed days
Jail
- Duration: 30 minutes (
JAIL_DURATION) gaming_tool: prevents jail on crime fail/jailbreak: 3 dice rolls, need doubles to escape free. On fail - bail = 20-30% of balance, min 350⬡. If balance < 350⬡, player stays jailed until timer.- Blocked while jailed:
/work,/beg,/crime,/rob,/give(checked indo_*functions via_is_jailed)
EXP Rewards (from EXP_REWARDS in core/economy/levels.py)
EXP is awarded on every successful command use. Level formula: level = max(1, floor(sqrt(exp / 6))) (see get_level / exp_for_level). Thresholds: Level 5 = 150 EXP, Level 10 = 600, Level 20 = 2 400, Level 30 = 5 400.
Gambling EXP is bet-scaled via gamble_exp(bet); fish EXP is per-species in FISH_CATALOGUE (common 2–3, uncommon 6–7, rare 10, epic 14–15, legendary 25).
Tournament Team Roles
Economy profile only. Roster-independent: matches Discord usernames straight against TEAM_SHEET_ID, never the member sheet. Entry points are /teamsync (commands/economy_team_commands.py) and the hourly team_sync_hourly task in bot.py; both call sheets.refresh_teams() then member_sync.sync_all_team_roles().
Pipeline
| Step | Where | Notes |
|---|---|---|
| Split a tab into per-game blocks | sheets.parse_team_sections |
Returns TeamSection(title, rosters). The merged title row above each header is the only thing identifying game + year |
Flatten to {team: [players]} |
sheets.parse_team_rosters |
Thin wrapper over parse_team_sections |
| Section title → divider role name | sheets.resolve_divider |
Matches config.TEAM_DIVIDERS; most specific key wins |
| Cache | sheets._refresh_teams_sync |
Populates _team_roster, _team_by_username, _team_names, _team_divider |
| Grant/remove roles per member | member_sync.sync_team_role |
Only names in all_team_names() are ever touched |
| Compute new role positions | member_sync.plan_team_positions |
Pure - list of names ascending + managed set + placements → {name: new position}. Unit-tested without Discord |
| Apply | member_sync.apply_team_role_positions |
One guild.edit_role_positions call per sync |
Adding a game to the divider config
.env- addTEAM_DIVIDER_<GAME>_<YEAR>="<exact Discord role name>". No code change;config._parse_team_dividersdiscovers any var with that prefix at startup.- Restart the bot (env is read once at import).
Position maths gotchas
- Discord positions are ascending from the bottom (
@everyone= 0), so "under the divider in the UI" means a lower number.plan_team_positionstherefore inserts each block reverse-sorted. - The bot can only reorder roles strictly below its own highest role. Dividers at or above it are skipped with an error rather than attempted.
managedroles (integration/bot/booster) are never emitted in the plan.- Roles are matched by name throughout the feature; with duplicate names the lowest-positioned one wins.
- Only roles whose position actually changed are submitted, so a settled guild costs zero API calls.
Role Hierarchy (Discord)
Order top to bottom in server roles:
[Bot managed role] ← bot's own role, always at top of our stack
ECONOMY ← given to everyone who uses any economy command
TipiLEGEND ← level 30+
TipiCHAD ← level 20+
TipiHUSTLER ← level 10+
TipiGRINDER ← level 5+
TipiNOOB ← level 1+
Run /economysetup to auto-create all roles and set their positions. The command is idempotent - safe to run multiple times.
Role assignment:
- ECONOMY role: given automatically on first EXP award (i.e. first successful economy command)
- Level roles: given/swapped automatically on level-up; synced on
/rank
Shop Tiers & Level Requirements
| Tier | Level Required | Items |
|---|---|---|
| T1 | 0 (any) | gaming_hiir, hiirematt, korvaklapid, lan_pass, anticheat, energiajook, gaming_laptop |
| T2 | 10 | reguleeritav_laud, jellyfin, mikrofon, klaviatuur, monitor, cat6 |
| T3 | 20 | monitor_360, karikas, gaming_tool |
Shop display is sorted by cost (ascending) within each tier.
The SHOP_LEVEL_REQ dict in core/economy/shop.py controls per-item lock thresholds.
strings/ Organisation
Imported as import strings as S everywhere. strings/ is a package: the names below live in domain submodules and are re-exported from strings/__init__.py, so S.CMD, S.ERR, etc. resolve unchanged regardless of which submodule they live in. Dicts are read from bot.py and from every commands/*.py module. Edit the submodule shown in the Module column.
| Section | Dict | Module | Typical usage |
|---|---|---|---|
| Flavour text | WORK_JOBS, BEG_LINES, CRIME_WIN, CRIME_LOSE |
economy.py |
Randomised descriptions |
| Command descriptions | CMD["key"] |
commands.py |
@tree.command(description=S.CMD["key"]) |
| Parameter descriptions | OPT["key"] |
commands.py |
@app_commands.describe(param=S.OPT["key"]) |
| Help embed | HELP_CATEGORIES["cat"] |
commands.py |
cmd_help (in bot.py) |
| Banned message | MSG_BANNED |
common.py |
All banned checks |
| Maintenance mode | MSG_MAINTENANCE |
common.py |
Shown when _PAUSED=True in bot.py (toggled by /pause in commands/ops_admin_commands.py) |
| Reminder options | REMINDER_OPTS |
common.py |
RemindersSelect dropdown |
| Slots outcomes | SLOTS_TIERS["tier"] → (title, color) |
games.py |
cmd_slots (in commands/economy_games_commands.py) |
| Embed titles | TITLE["key"] |
common.py |
discord.Embed(title=S.TITLE["key"]) |
| Error messages | ERR["key"] |
common.py |
send_message(S.ERR["key"]) - use .format(**kwargs) for dynamic parts |
| Cooldown messages | CD_MSG["cmd"].format(ts=cd_ts(...)) |
common.py |
Cooldown responses (cd_ts helper passed in by bot.py) |
| Shop UI | SHOP_UI["key"] |
economy.py |
_shop_embed (in commands/economy_support_commands.py) |
| Item descriptions | ITEM_DESCRIPTIONS["item_key"] |
economy.py |
core/economy/shop.py SHOP[key]["description"] |
| Patch notes UI | PATCHNOTES_UI["key"] |
common.py |
commands/info_commands.py (/patchnotes) |
Constants Location Quick-Reference
All are re-exported from core/economy/__init__.py, so code reads them as economy.<NAME> regardless of which submodule defines them. Edit the file in the Defined in column.
| Constant | Defined in | Description |
|---|---|---|
SHOP |
core/economy/shop.py |
All shop items (name, emoji, cost, description) |
SHOP_TIERS |
core/economy/shop.py |
Which items are in T1/T2/T3 |
SHOP_LEVEL_REQ |
core/economy/shop.py |
Min level per item |
COOLDOWNS |
core/economy/store.py |
Base cooldown per command |
JAIL_DURATION |
core/economy/store.py |
How long jail lasts |
LEVEL_ROLES |
core/economy/levels.py |
[(min_level, "RoleName"), ...] highest first |
ECONOMY_ROLE |
core/economy/levels.py |
Name of the base economy participation role |
EXP_REWARDS |
core/economy/levels.py |
EXP per command |
FISH_CATALOGUE |
core/economy/fishing.py |
Fish species table (rarity, weight, coins, exp) |
HOUSE_ID |
core/economy/house.py |
Bot's user ID (house account for /rob) |
MIN_BAIL |
core/economy/jail.py |
Minimum bail payment (350⬡) |
COIN |
core/economy/store.py |
The coin emoji string |
_PAUSED |
bot.py |
In-memory maintenance flag; toggled by /pause; blocks all non-admin commands |
Balance Notes (as of current version)
- Beg is most efficient for active players (3min cooldown + 2× multiplier w/
klaviatuur= high ⬡/hr) - Work is best for passive players (1h cooldown, fire and forget)
- Crime is high risk/reward - best with
cat6+mikrofon lan_pass(1200⬡) doubles daily - good long-term investmentgaming_laptop(1500⬡) 5% interest, capped 500⬡/day - snowballs with large balanceanticheatis consumable (2 uses) - only item that can be re-boughtkarikas(T3) is the only item that preserves a daily streak across missed daysreguleeritav_laud(T2) stacks withgaming_hiir: combined ×1.5 × ×1.25 = ×1.875