# TipiLAN Bot Discord bot for the TipiLAN community. Manages member roles and nicknames via Google Sheets, announces birthdays, and runs the TipiCOIN economy. --- ## Table of Contents 1. [Setup](#setup) 2. [Member Management](#member-management) 3. [Admin Commands](#admin-commands) 4. [Birthday System](#birthday-system) 5. [TipiCOIN Economy](#tipicoin-economy) 6. [Project Structure](#project-structure) --- ## Setup ### 1. Discord Application - **Bot token** - Settings → Bot → Token → Copy - **Server Members Intent** must be **ON** - Settings → Bot → Privileged Gateway Intents - Bot invite scopes: `bot` + `applications.commands` - Required permissions: Manage Roles, Manage Nicknames, View Channels, Send Messages, Embed Links, Read Message History > Permissions integer: `402738176` ### 2. Google Service Account 1. [Google Cloud Console](https://console.cloud.google.com/) → create/select project 2. Enable **Google Sheets API** and **Google Drive API** 3. Credentials → Create Credentials → Service Account 4. Download JSON key → save as `credentials.json` in the project root 5. Share your Google Sheet with the service account `client_email` - give **Editor** access ### 3. Google Sheet Format Row 1 = headers (exact names). Row 2 = formula/stats row (skipped by bot). Data starts row 3. | Column | What the bot does with it | |---|---| | **Nimi** | Sets Discord nickname: first name + last initial (`Mari-Liis Tamm` → `Mari-Liis T`) | | **Organisatsioon** | Maps to a Discord role (comma-separated for multiple) | | **Meil** | Read-only | | **Discord** | Username used for initial matching | | **User ID** | Bot writes numeric Discord ID here once matched | | **Sünnipäev** | Birthday - accepts `DD/MM/YYYY`, `YYYY-MM-DD`, `MM-DD`. Years outside 1920-now ignored | | **Telefon** | Read-only | | **Valdkond** | Maps to a Discord role (comma-separated) | | **Roll** | Maps to a Discord role (comma-separated) | | **Discordis synced?** | Bot writes `TRUE`/`FALSE` checkbox after each sync | | **Groupi lisatud?** | Managed externally | **Empty cell values** - the bot treats blank cells, `"-"`, `"x"`, `"n/a"`, `"none"`, `"ei"` as empty/skipped. **Role name mapping** - some sheet values map to different Discord role names. Trailing punctuation (`.`, `,`, `;` etc.) is stripped from sheet values before lookup, so `"Messiala."` correctly matches the `Messiala` Discord role: | Sheet value | Discord role | |---|---| | `Juht` | `Tiimijuht` | | `Admin` | `+` | **Base roles** - two role IDs in `config.py → BASE_ROLE_IDS` are added to every synced member automatically. ### 4. PocketBase (Economy Database) The economy system stores all player data in [PocketBase](https://pocketbase.io/). 1. Download `pocketbase.exe` (Windows) from https://pocketbase.io/docs/ and place it in the project root. 2. Start PocketBase: `.\pocketbase.exe serve` 3. Open the admin UI at http://127.0.0.1:8090/_/ and create a superuser account. 4. Create a collection named `economy_users` - see `docs/POCKETBASE_SETUP.md` for the full schema. 5. Set `PB_URL`, `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD` in `.env`. 6. **One-time data migration** (only if you have an existing `data/economy.json`): `python scripts/migrate_to_pb.py` > PocketBase must be running before the bot starts. `pocketbase.exe` and `pb_data/` are gitignored. ### 5. Environment Variables ```bash # Windows copy .env.example .env # macOS/Linux cp .env.example .env ``` | Variable | Description | |---|---| | `DISCORD_TOKEN` | Bot token from Discord Developer Portal | | `SHEET_ID` | ID from the Google Sheet URL | | `GOOGLE_CREDS_PATH` | Path to `credentials.json` (default: `credentials.json`) | | `GUILD_ID` | Right-click server → Copy Server ID (Developer Mode on) | | `BIRTHDAY_CHANNEL_ID` | Channel for birthday `@here` pings | | `BIRTHDAY_WINDOW_DAYS` | Days before birthday that on-join check flags it (default: 7) | | `PB_URL` | PocketBase base URL (default: `http://127.0.0.1:8090`) | | `PB_ADMIN_EMAIL` | PocketBase superuser e-mail | | `PB_ADMIN_PASSWORD` | PocketBase superuser password | ### 6. Install & Run ```bash python -m venv .venv .venv\Scripts\activate # Windows # source .venv/bin/activate # macOS/Linux pip install -r requirements.txt # Terminal 1 - keep running .\pocketbase.exe serve # Terminal 2 python bot.py ``` --- ## Member Management ### On member join - Bot looks the member up in the sheet by **Discord username** - If found → sets nickname, assigns roles, writes back User ID, marks synced - If **not found** → creates a new sheet row with their `Discord` username and `User ID` pre-filled. Admin fills in the rest, then runs `/check` ### Matching logic 1. Match by **User ID** (numeric, reliable - IDs never change) 2. Fall back to **Discord username** (case-insensitive) if no ID yet 3. Once matched by username, the bot writes the ID back so future matches are by ID ### Nickname format `Nimi` column → first name + last name initial. Hyphenated first names preserved. | Nimi | Nickname | |---|---| | Mari Tamm | Mari T | | Mari-Liis Tamm | Mari-Liis T | | Jaan | Jaan | ### Synced status A member is marked `Discordis synced? = TRUE` when their sync completes with no errors. Admins (bot lacks permission to modify them) are silently skipped and still marked synced. --- ## Admin Commands > These commands are hidden from users who don't have the required permission. Override per-role in **Server Settings → Integrations → Bot**. | Command | Permission | What it does | |---|---|---| | `/check` | Manage Roles | Refreshes sheet data, backfills missing User IDs, syncs nicknames + roles for every member, reports stats | | `/member @user` | Manage Roles | Shows a member's full sheet data + calculated age | | `/sync` | Manage Guild | Re-registers slash commands with Discord | | `/restart` | Manage Guild | Gracefully restarts the bot process; posts ✅ in the same channel when back up | | `/shutdown` | Manage Guild | Shuts the bot down cleanly without restarting | | `/pause` | Manage Guild | Toggles maintenance mode — blocks all non-admin commands; calling again unpauses | | `/send #channel message` | Manage Guild | Sends a message to any channel as the bot | | `/status` | Manage Guild | Bot uptime, RAM, CPU, latency, cache stats, economy user count | | `/admincoins @user ` | Manage Guild | Give (positive) or take (negative) TipiCOINi. Balance floored at 0. User gets a DM with reason. | | `/adminjail @user ` | Manage Guild | Manually jail a user for N minutes. User gets a DM. | | `/adminunjail @user` | Manage Guild | Release a user from jail immediately. | | `/adminban @user ` | Manage Guild | Ban a user from all economy commands. User gets a DM. | | `/adminunban @user` | Manage Guild | Lift an economy ban. | | `/adminreset @user ` | Manage Guild | Wipe a user's balance, items, and streak to zero. User gets a DM. | | `/adminview @user` | Manage Guild | Inspect a user's full economy profile: balance, streak, items, jail status, ban status. | ### `/check` output example ``` 🔑 Täideti 3 puuduvat kasutaja ID-d. ✅ Korras: 54 🔧 Parandatud: 3 ❓ Ei leitud: 1 ⚠️ Vead: 2 Üksikasjad: 🔧 Mari T: +rollid: TipiSÕBER ⚠️ Rolli 'Messiala' ei leitud serverist 📊 Tabeli statistika - 58 liiget ... ``` --- ## Birthday System ### Daily announcement Every day at **09:00 Tallinn time** the bot checks all sheet rows and pings `@here` in `BIRTHDAY_CHANNEL_ID` for anyone whose birthday is today. **Duplicate prevention** - announcements are logged to `birthday_sent.json` keyed by date. Bot restart on a birthday day does **not** re-ping. Log is auto-cleaned after 2 days. ### `/birthdays` Paginated embed with **12 pages** - one per calendar month. Opens on the **current month**. Navigate with ◀/▶. Each entry shows: - Member mention (if their User ID is known) or name - Birthday date - Days until next birthday (or 🎉 if today) ### On member join If a member joins and their birthday is within `BIRTHDAY_WINDOW_DAYS` days, a birthday announcement is sent. --- ## TipiCOIN Economy All economy data is stored in **PocketBase** (`economy_users` collection - see `pb_client.py`). The currency is **TipiCOIN** (⬡), displayed as a custom Discord emoji configured in `economy.py → COIN`. --- ### House account The bot has its own TipiCOIN balance (the "house"). Coins flow **into** the house when players lose: - `/roulette` - lost bets - `/slots` - missed bets - `/blackjack` - lost bets - `/rps` - lost bets (vs bot or PvP) - `/crime` - failure fines - `/rob` - failure fines and anticheat counter-fines The house is listed at **#0** on the leaderboard. Players can attempt to rob it via `/rob @TipiBOT` with special jackpot odds (35% success, 5–40% of the house balance). --- ### Earning coins | Command | Cooldown | Base payout | Notes | |---|---|---|---| | `/daily` | 20h | 150 ⬡ | Streak multiplier applied (see below). Kõrvaklapid reduces cooldown to 18h. LAN Pilet doubles the reward. Botikoobas adds 5% interest on your balance (capped at 500 ⬡/day). | | `/work` | 1h | 15–75 ⬡ | Random job flavour text. Mängurihiir +50%, Reguleeritav laud +25% (stacks). Red Bull: 30% chance of ×3. Ultralai monitor reduces cooldown to 40min. | | `/beg` | 5min | 10–40 ⬡ | XL hiirematt reduces cooldown to 3min. Mehhaaniline klaviatuur multiplies earnings ×2. | | `/crime` | 2h | 200–500 ⬡ | 60% success rate (75% with CAT6). +30% earnings with Mikrofon on win. Fail = fine + 30min jail. Mänguritool skips jail on fail. | ### Daily streak The streak increments each time you claim `/daily` within the cooldown window. Missing a day resets it to 1 **unless** you own the TipiLAN trofee item. | Streak | Multiplier | Payout (base) | |---|---|---| | 1–2 days | ×1.0 | 150 ⬡ | | 3–6 days | ×1.5 | 225 ⬡ | | 7–13 days | ×2.0 | 300 ⬡ | | 14+ days | ×3.0 | 450 ⬡ | > With LAN Pilet (×2 daily) and a 14-day streak (×3.0) the base payout reaches **900 ⬡**. Add Botikoobas 5% interest on top. --- ### EXP & levels Every successful economy action awards EXP: | Action | EXP | |---|---| | `/daily` claimed | +50 | | `/work` completed | +25 | | `/crime` success | +15 | | `/rob` success | +15 | | Gambling win (`/roulette`, `/slots`, `/blackjack`) | Scaled by bet: <10⬡ = 0, 10–99⬡ = +5, 100–999⬡ = +10, 1 000–9 999⬡ = +15, 10 000–99 999⬡ = +20, 100 000+⬡ = +25 | | `/beg` completed | +5 | **Level formula:** `level = floor(√(total_exp ÷ 10))` | Level | EXP required | Milestone | |---|---|---| | 1 | 10 | TipiNOOB role | | 5 | 250 | TipiGRINDER role | | 10 | 1 000 | TipiHUSTLER role · **T2 shop unlocks** | | 20 | 4 000 | TipiCHAD role · **T3 shop unlocks** | | 30 | 9 000 | TipiLEGEND role | Use `/rank` to see your current EXP, level, progress bar to the next level, and leaderboard position. ### Level roles Roles are assigned automatically on level-up and re-synced when you run `/rank`. | Role | Min level | |---|---| | TipiNOOB | 1 | | TipiGRINDER | 5 | | TipiHUSTLER | 10 | | TipiCHAD | 20 | | TipiLEGEND | 30 | The **ECONOMY** role is granted on your first EXP award (i.e. first successful economy command). Run `/economysetup` (admin) once to create all roles and position them correctly below the bot's own role. --- ### Gambling | Command | Notes | |---|---| | `/roulette ` | Red/black pays ×2 (≈50% each). Green pays ×14 at 1/37 chance. Lost bets go to the house. | | `/slots ` | 3 reels. **Pair** = +50% of bet. **Triple** = tiered by symbol rarity (×4 heart → ×15 skull, ×1.5 with 360hz monitor). **Jackpot** (3 karikas) = ×25 (×37 with 360hz monitor). Miss = lose bet. | | `/blackjack ` | Standard rules. Dealer stands on 17+. Natural blackjack pays 3:2. Double down on first action only. Split identical rank cards (one extra bet). Lost bets go to the house. | | `/rps [panus]` | Rock Paper Scissors vs. the bot with optional bet. Bot picks randomly. | | `/rps [panus] @vastane` | PvP duel - both players pick privately via DM, result posted to the server. Bet transferred to winner. | ### Social | Command | Notes | |---|---| | `/rob @user` | 45% success (60% with Jellyfin). Target must have ≥100 ⬡. Steal 10–25% of target's balance. Fail = fine of 100–250 ⬡ to the house. Anticheat blocks the rob and fines the robber (2 charges per purchase). Cannot rob TipiBOT - use `/heist` instead. | | `/heist` | Start a bank robbery. Solo or group (max 8). 5-minute join window. Success: 35% base + 5% per extra player (cap 65%). Win = split **20–55%** of house balance equally. Fail = **1h 30min jail + ~15% balance fine** for all. 4h personal cooldown + 1h global server cooldown after each event. | | `/give @user ` | Transfer coins directly to another player. **Jailed users cannot use this command.** | | `/request [@sihtmärk]` | Post a crowdfunding request. Anyone (or a specific target) can click **Rahasta** to contribute via `/give`. Expires in 5 minutes. | ### Info commands | Command | Notes | |---|---| | `/balance [@user]` | Balance, daily streak, owned items (with Anticheat charges remaining), jail status if jailed. | | `/rank [@user]` | EXP total, current level, progress bar to next level, leaderboard rank. | | `/stats [@user]` | Lifetime statistics: economy totals, work/beg counts, gambling records, crime/heist history, social totals, best streak. | | `/cooldowns` | All cooldowns at a glance with live Discord timestamps. Shows jail timer if jailed. | | `/leaderboard` | Paginated coin leaderboard (10/page). House pinned at #0. ◀/▶ to browse; 📍 **Mina** jumps to your page. Has a separate EXP/level tab. | | `/shop` | Browse all items by tier. Shows owned status, Anticheat charges remaining, and level lock for T2/T3. | | `/buy ` | Purchase an item by name (partial match accepted). | | `/reminders` | Toggle per-command DM notifications. **All reminders are on by default.** Bot DMs you the moment each cooldown expires. | --- ### Jail system `/crime` fail (without Mänguritool) jails you for **30 minutes**. While jailed, `/work`, `/beg`, `/crime`, `/rob`, and `/give` are blocked. #### `/jailbreak` Roll two dice - matching values (doubles) free you instantly. **3 attempts** per jail sentence. If all 3 fail you pay bail: - **20–30% of your current balance** (scales with wealth) - **Minimum 350 ⬡** - if your balance is below this you stay jailed until the timer runs out Cooldowns and jail release times display as live Discord relative timestamps. --- ### Shop items All items are **permanent** once purchased **except Anticheat**, which expires after 2 uses and can be repurchased. #### Tier 1 - any level | Item | Cost | Effect | |---|---|---| | Mängurihiir | 500 ⬡ | `/work` earns +50% | | XL hiirematt | 600 ⬡ | `/beg` cooldown 5min → 3min | | Anticheat | 750 ⬡ | Rob attempts against you fail and fine the robber. **2 uses**, then repurchase. | | Red Bull | 800 ⬡ | `/work` has 30% chance to earn ×3 | | Kõrvaklapid | 1 200 ⬡ | `/daily` cooldown 20h → 18h | | LAN Pilet | 1 200 ⬡ | `/daily` reward ×2 | | Botikoobas | 1 500 ⬡ | `/daily` adds 5% interest on balance (capped at 500 ⬡/day) | #### Tier 2 - level 10 required (TipiHUSTLER+) | Item | Cost | Effect | |---|---|---| | Mehhaaniline klaviatuur | 1 800 ⬡ | `/beg` earns ×2 | | Ultralai monitor | 2 500 ⬡ | `/work` cooldown 1h → 40min | | Mikrofon | 2 800 ⬡ | `/crime` win earns +30% | | Reguleeritav laud | 3 500 ⬡ | `/work` earns +25% (stacks with Mängurihiir → ×1.875 combined) | | CAT6 netikaabel | 3 500 ⬡ | `/crime` success rate 60% → 75% | | Jellyfin server | 4 000 ⬡ | `/rob` success rate 45% → 60% | #### Tier 3 - level 20 required (TipiCHAD+) | Item | Cost | Effect | |---|---|---| | TipiLAN trofee | 6 000 ⬡ | Daily streak survives missed days | | 360hz monitor | 7 500 ⬡ | Slots jackpot 10× → 15×, triple 4× → 6× | | Mänguritool | 9 000 ⬡ | `/crime` fail never sends you to jail | --- ### Amount shortcuts Commands that accept a coin amount (`/give`, `/roulette`, `/rps`, `/slots`, `/blackjack`) accept `"all"` as the amount to wager your entire balance. ### Custom emoji Change `COIN` in `economy.py` to any Discord emoji string: ```python COIN = "<:tipicoin:YOUR_EMOJI_ID>" ``` --- ## Logging All logs are written to the `logs/` directory (auto-created on startup). | File | Rotation | Contents | |---|---|---| | `logs/bot.log` | 5 MB x 5 backups | All INFO+ events: commands, errors, member sync | | `logs/transactions.log` | Daily, 30 days | Economy transactions only: every balance change with user, amount, new balance | The terminal output is **colour-coded** by log level (green = INFO, yellow = WARNING, red = ERROR). Every slash command invocation is logged with the user ID, display name, and all options passed. --- ## Project Structure ``` ├── bot.py # Discord client, all slash commands, event handlers ├── economy.py # TipiCOIN business logic, constants (SHOP, COOLDOWNS, etc.) ├── pb_client.py # Async PocketBase REST client (auth + CRUD for economy_users) ├── strings.py # All user-facing strings, command descriptions, help text ├── member_sync.py # Role/nickname/birthday sync logic ├── sheets.py # Google Sheets read/write + in-memory cache ├── config.py # Environment variable loader ├── requirements.txt # Python dependencies ├── .env.example # Template for secrets ├── .env # Your secrets (gitignored) ├── credentials.json # Google service account key (gitignored) ├── docs/ │ ├── DEV_NOTES.md # Developer reference (architecture, checklists, constants) │ ├── CHANGELOG.md # Version history │ └── POCKETBASE_SETUP.md # PocketBase collection schema + setup instructions ├── scripts/ │ ├── migrate_to_pb.py # One-time migration: economy.json → PocketBase │ └── add_stats_fields.py # Schema migration: add new fields to economy_users collection ├── data/ │ └── birthday_sent.json # Birthday dedup log (auto-created) ├── pb_data/ # PocketBase database files (auto-created, gitignored) └── logs/ ├── bot.log # General rotating log (auto-created) └── transactions.log # Daily economy transaction log (auto-created) ```