Compare commits
13 Commits
rewrite-v2
...
93f4d471dc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93f4d471dc | ||
|
|
de7cfce833 | ||
|
|
a4a447867f | ||
|
|
9ae26049c5 | ||
|
|
b998418c14 | ||
|
|
94df54dde2 | ||
|
|
77a3badd41 | ||
|
|
17102ae202 | ||
|
|
cd41bc2a48 | ||
| 802a6a2e8d | |||
|
|
64d9b304a9 | ||
|
|
07360d3f11 | ||
| 8f28832432 |
14
.dockerignore
Normal file
14
.dockerignore
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
data/
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
*.md
|
||||||
|
.dockerignore
|
||||||
|
Dockerfile
|
||||||
|
compose.yaml
|
||||||
|
credentials.json
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
# Bot runtime profile: dev (economy + member tools) or economy (economy-only)
|
|
||||||
BOT_PROFILE=dev
|
|
||||||
|
|
||||||
# Profile-specific Discord bot tokens (from https://discord.com/developers/applications)
|
# Profile-specific Discord bot tokens (from https://discord.com/developers/applications)
|
||||||
DISCORD_TOKEN_DEV=your-dev-bot-token-here
|
DISCORD_TOKEN_DEV=your-dev-bot-token-here
|
||||||
|
|||||||
17
.gitea/workflows/deploy.yml
Normal file
17
.gitea/workflows/deploy.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: Deploy
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: linux
|
||||||
|
steps:
|
||||||
|
- name: Deploy
|
||||||
|
run: |
|
||||||
|
cd ~/tipibot
|
||||||
|
git pull
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
systemctl restart tipibot
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,8 +4,7 @@ __pycache__/
|
|||||||
*.pyc
|
*.pyc
|
||||||
.venv/
|
.venv/
|
||||||
venv/
|
venv/
|
||||||
data/restart_channel.json
|
data/
|
||||||
data/economy.json
|
|
||||||
pocketbase.exe
|
pocketbase.exe
|
||||||
pocketbase
|
pocketbase
|
||||||
pb_data/
|
pb_data/
|
||||||
|
|||||||
257
DEV_NOTES.md
257
DEV_NOTES.md
@@ -1,257 +0,0 @@
|
|||||||
# TipiLAN Bot - Developer Reference
|
|
||||||
|
|
||||||
## File Structure
|
|
||||||
|
|
||||||
| File | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `bot.py` | All Discord commands, views (UI components), event handlers, reminder system |
|
|
||||||
| `economy.py` | All economy logic, data model, constants (SHOP, COOLDOWNS, LEVEL_ROLES, etc.) |
|
|
||||||
| `pb_client.py` | Async PocketBase REST client - auth token cache, CRUD on `economy_users` collection |
|
|
||||||
| `strings.py` | **Single source of truth for all user-facing text.** Edit here to change any message. |
|
|
||||||
| `sheets.py` | Google Sheets integration (member sync) |
|
|
||||||
| `member_sync.py` | Birthday/member sync background task |
|
|
||||||
| `config.py` | Environment variables (TOKEN, GUILD_ID, PB_URL, etc.) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a New Economy Command
|
|
||||||
|
|
||||||
Checklist - do all of these, in order:
|
|
||||||
|
|
||||||
1. **`economy.py`** - add the `do_<cmd>` async function with cooldown check, logic, `_commit`, and `_txn` logging
|
|
||||||
2. **`economy.py`** - add the cooldown to `COOLDOWNS` dict if it has one
|
|
||||||
3. **`economy.py`** - add the EXP reward to `EXP_REWARDS` dict
|
|
||||||
4. **PocketBase** - if the function stores new fields, add them manually in the PB admin UI at `http://127.0.0.1:8090/_/`. Fields not in the PB schema are silently dropped on PATCH.
|
|
||||||
5. **`strings.py` `CMD`** - add the slash command description
|
|
||||||
6. **`strings.py` `OPT`** - add any parameter descriptions
|
|
||||||
7. **`strings.py` `TITLE`** - add embed title(s) for success/fail states
|
|
||||||
8. **`strings.py` `ERR`** - add any error messages (banned, cooldown uses `CD_MSG`, jailed uses `CD_MSG["jailed"]`)
|
|
||||||
9. **`strings.py` `CD_MSG`** - add cooldown message if command has a cooldown
|
|
||||||
10. **`strings.py` `HELP_CATEGORIES["tipibot"]["fields"]`** - add the command to the help embed
|
|
||||||
11. **`bot.py`** - implement the `cmd_<name>` function, handle all `res["reason"]` cases
|
|
||||||
12. **`bot.py`** - call `_maybe_remind` if the command has a cooldown and reminders make sense
|
|
||||||
13. **`bot.py`** - call `_award_exp(interaction, economy.EXP_REWARDS["<cmd>"])` on success
|
|
||||||
14. **`strings.py` `REMINDER_OPTS`** - add a reminder option if the command needs one
|
|
||||||
15. **`bot.py` `_maybe_remind`** and **`_restore_reminders`** - if item-modified cooldown, add `elif` branch
|
|
||||||
16. **`bot.py` `_REMINDER_COOLDOWN_KEYS`** - add `"cmd": "last_cmd"` mapping if reminder-capable
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a New Shop Item
|
|
||||||
|
|
||||||
Checklist:
|
|
||||||
|
|
||||||
1. **`economy.py` `SHOP`** - add the item dict `{name, emoji, cost, description: strings.ITEM_DESCRIPTIONS["key"]}`
|
|
||||||
2. **`economy.py` `SHOP_TIERS`** - add the key to the correct tier list (1/2/3)
|
|
||||||
3. **`economy.py` `SHOP_LEVEL_REQ`** - add minimum level if it is T2 (≥10) or T3 (≥20)
|
|
||||||
4. **`strings.py` `ITEM_DESCRIPTIONS`** - add the item description (Estonian flavour + English effect)
|
|
||||||
5. **`strings.py` `HELP_CATEGORIES["shop"]["fields"]`** - add display entry (sorted by cost)
|
|
||||||
6. If the item modifies a cooldown:
|
|
||||||
- **`economy.py`** - add the `if "item" in user["items"]` branch in the relevant `do_<cmd>` function
|
|
||||||
- **`bot.py` `_maybe_remind`** - add `elif cmd == "<cmd>" and "<item>" in items:` branch with the new delay
|
|
||||||
- **`bot.py` `_restore_reminders`** - add the same `elif` branch
|
|
||||||
- **`bot.py` `cmd_cooldowns`** - add the item annotation to the relevant status line
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a New Level Role
|
|
||||||
|
|
||||||
1. **`economy.py` `LEVEL_ROLES`** - add `(min_level, "RoleName")` in descending level order (highest first)
|
|
||||||
2. **`bot.py` `_ensure_level_role`** - no changes needed (uses `LEVEL_ROLES` dynamically)
|
|
||||||
3. Run **`/economysetup`** in the server to create the role and set its position
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Adding a New Admin Command
|
|
||||||
|
|
||||||
1. **`strings.py` `CMD`** - add `"[Admin] ..."` description
|
|
||||||
2. **`strings.py` `OPT`** - add parameter descriptions
|
|
||||||
3. **`strings.py` `ADMIN`** - add response and DM strings
|
|
||||||
4. **`strings.py` `HELP_CATEGORIES["admin"]["fields"]`** - add the entry
|
|
||||||
5. **`economy.py`** - add `do_admin_<name>` function
|
|
||||||
6. **`bot.py`** - add command 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). `pb_client.py` owns all reads/writes. Each `do_*` function in `economy.py` calls `get_user()` → 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, +5% interest w/ gaming_laptop; prestige daily_plus adds +20% per level |
|
|
||||||
| `/work` | 1h (40min w/ monitor) | 15-75⬡ | ×1.5 w/ gaming_hiir, ×1.25 w/ reguleeritav_laud, ×3 30% chance w/ energiajook; prestige work_plus +20%/level |
|
|
||||||
| `/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; cannot rob TipiBOT directly |
|
|
||||||
| `/heist` | 4h personal + 1h global | 20–55% of house / n players | solo allowed, max 8; 35%+5%/player success (cap 65%); fail = 3h jail + ~15% balance fine |
|
|
||||||
| `/fish` | 2min (90s w/ ussipurk) | varies by fish rarity | Interactive minigame; catches go to inventory; sell with `/fishsell` |
|
|
||||||
| `/slots` | - | varies | pair=+0.5× bet; triple tiered; karikas jackpot ×25; ×1.5 w/ monitor_360; miss=lose bet |
|
|
||||||
| `/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 |
|
|
||||||
|
|
||||||
### Fishing System
|
|
||||||
|
|
||||||
- `/fish` - interactive minigame: cast → wait 5–15s for bite → press button within 2s (3s w/ echolood) → keep or sell
|
|
||||||
- Fish stored in `fish_inventory` (list of `{fish_id, weight, value}` objects)
|
|
||||||
- `/fishbook` - paginated fish collection showing caught species and inventory counts
|
|
||||||
- `/fishsell` - sell all fish from inventory at once
|
|
||||||
- `fish_inventory` and `fish_book` **survive prestige resets**
|
|
||||||
- `kalavork` (T3, 5000⬡): bumps all caught fish up one rarity tier
|
|
||||||
- `ussipurk` (T2, 3500⬡): cooldown 2min → 90s
|
|
||||||
- `echolood` (T3, 8000⬡): bite window 2s → 3s
|
|
||||||
|
|
||||||
### Prestige System
|
|
||||||
|
|
||||||
- Requires level 30 (9000 EXP)
|
|
||||||
- Resets: balance, EXP, items, cooldowns, jail
|
|
||||||
- Preserves: fish_book, fish_inventory, lifetime stats, prestige_points, season_total_exp, prestige_upgrades
|
|
||||||
- Awards prestige_points = max(1, exp ÷ 1000) at time of prestige
|
|
||||||
- Each prestige increments `prestige_level` counter
|
|
||||||
- Prestige coin/exp multipliers apply to all earned values
|
|
||||||
|
|
||||||
**Prestige Shop** (`PRESTIGE_SHOP` in economy.py):
|
|
||||||
|
|
||||||
| Upgrade | Max Level | Cost/level | Effect |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `coin_mult` | 5 | 5 PP | +8% coin multiplier per level |
|
|
||||||
| `exp_mult` | 5 | 5 PP | +8% EXP multiplier per level |
|
|
||||||
| `daily_plus` | 3 | 7 PP | +20% daily base reward per level |
|
|
||||||
| `work_plus` | 3 | 7 PP | +20% work earnings per level |
|
|
||||||
|
|
||||||
### "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⬡)
|
|
||||||
- `karikas` item: streak survives missed days
|
|
||||||
|
|
||||||
### Jail
|
|
||||||
- Normal duration: 30 minutes (`JAIL_DURATION`)
|
|
||||||
- Heist fail duration: 3 hours (`HEIST_JAIL`) + fine 15% of balance (min 150⬡, max 1000⬡)
|
|
||||||
- `gaming_tool`: prevents jail on crime fail
|
|
||||||
- `/jailbreak`: 3 single-button dice rolls (both dice at once), need doubles to escape free. Animated reveal with TipiDICE emoji. On fail after 3 tries - bail = 20-30% of balance, min 350⬡. If balance < 350⬡, stay jailed until timer.
|
|
||||||
|
|
||||||
### EXP Rewards (from `EXP_REWARDS` in economy.py)
|
|
||||||
EXP is awarded on every successful command use. Level formula: `floor(sqrt(exp / 10))`, so Level 5 = 250 EXP, Level 10 = 1000, Level 20 = 4000, Level 30 = 9000.
|
|
||||||
|
|
||||||
Fish EXP is awarded per catch (varies by rarity, defined in `FISH_CATALOGUE`). Prestige `exp_mult` upgrade applies to fish EXP.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Admin Commands Reference
|
|
||||||
|
|
||||||
| Command | What it does |
|
|
||||||
|---|---|
|
|
||||||
| `/pause` | Toggle maintenance mode - blocks all non-admin commands |
|
|
||||||
| `/admincoins @user <amount> <reason>` | Give/take coins (positive/negative). DMs user. |
|
|
||||||
| `/adminexp @user <amount> <reason>` | Give/take EXP (positive/negative). Auto-applies level roles on change. DMs user. |
|
|
||||||
| `/adminitem @user <item_id> <anna\|eemalda>` | Give or remove any shop item for free. DMs user. |
|
|
||||||
| `/adminjail @user <minutes> <reason>` | Manually jail a user. DMs user. |
|
|
||||||
| `/adminunjail @user` | Remove jail from a user. |
|
|
||||||
| `/adminban @user <reason>` | Ban from all economy commands. DMs user. |
|
|
||||||
| `/adminunban @user` | Lift economy ban. |
|
|
||||||
| `/adminreset @user <reason>` | Wipe balance, EXP, items, streak. DMs user. |
|
|
||||||
| `/adminview @user` | Full profile: balance, EXP/level, streak, prestige, fish stats, items, timestamps. |
|
|
||||||
| `/adminseason <top_n>` | End season: DM top N players, reset all EXP. |
|
|
||||||
|
|
||||||
All admin commands require **Manage Guild** permission and work in any channel (bypass pause and channel restrictions).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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`
|
|
||||||
- **`/adminexp`**: automatically re-applies level roles if level changes
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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, **ussipurk** |
|
|
||||||
| T3 | 20 | monitor_360, karikas, gaming_tool, **kalavork**, **echolood** |
|
|
||||||
|
|
||||||
Shop display is sorted by cost (ascending) within each tier.
|
|
||||||
The `SHOP_LEVEL_REQ` dict in `economy.py` controls per-item lock thresholds.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## strings.py Organisation
|
|
||||||
|
|
||||||
| Section | Dict | Usage in bot.py |
|
|
||||||
|---|---|---|
|
|
||||||
| Flavour text | `WORK_JOBS`, `BEG_LINES`, `CRIME_WIN`, `CRIME_LOSE` | Randomised descriptions |
|
|
||||||
| Command descriptions | `CMD["key"]` | `@tree.command(description=S.CMD["key"])` |
|
|
||||||
| Parameter descriptions | `OPT["key"]` | `@app_commands.describe(param=S.OPT["key"])` |
|
|
||||||
| Help embed | `HELP_CATEGORIES["cat"]` | `cmd_help` |
|
|
||||||
| Banned message | `MSG_BANNED` | All banned checks |
|
|
||||||
| Reminder options | `REMINDER_OPTS` | `RemindersSelect` dropdown |
|
|
||||||
| Slots outcomes | `SLOTS_TIERS["tier"]` → `(title, color)` | `cmd_slots` |
|
|
||||||
| Embed titles | `TITLE["key"]` | `discord.Embed(title=S.TITLE["key"])` |
|
|
||||||
| Error messages | `ERR["key"]` | `send_message(S.ERR["key"])` - use `.format(**kwargs)` for dynamic parts |
|
|
||||||
| Cooldown messages | `CD_MSG["cmd"].format(ts=_cd_ts(...))` | Cooldown responses |
|
|
||||||
| Shop UI | `SHOP_UI["key"]` | `_shop_embed` |
|
|
||||||
| Item descriptions | `ITEM_DESCRIPTIONS["item_key"]` | `economy.SHOP[key]["description"]` |
|
|
||||||
| Fish UI | `FISH_UI["key"]` | `/fish`, `/fishbook`, `/fishsell` |
|
|
||||||
| Fish names | `FISH_NAMES["fish_id"]` | Fish display name |
|
|
||||||
| Admin responses | `ADMIN["key"]` | Admin command success/DM messages |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Constants Location Quick-Reference
|
|
||||||
|
|
||||||
| Constant | File | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `SHOP` | `economy.py` | All shop items (name, emoji, cost, description) |
|
|
||||||
| `SHOP_TIERS` | `economy.py` | Which items are in T1/T2/T3 |
|
|
||||||
| `SHOP_LEVEL_REQ` | `economy.py` | Min level per item |
|
|
||||||
| `COOLDOWNS` | `economy.py` | Base cooldown per command |
|
|
||||||
| `JAIL_DURATION` | `economy.py` | How long jail lasts |
|
|
||||||
| `LEVEL_ROLES` | `economy.py` | `[(min_level, "RoleName"), ...]` highest first |
|
|
||||||
| `ECONOMY_ROLE` | `economy.py` | Name of the base economy participation role |
|
|
||||||
| `EXP_REWARDS` | `economy.py` | EXP per command |
|
|
||||||
| `FISH_CATALOGUE` | `economy.py` | All fish species (rarity, weight, coins, exp) |
|
|
||||||
| `PRESTIGE_SHOP` | `economy.py` | Prestige upgrade definitions |
|
|
||||||
| `HOUSE_ID` | `economy.py` | Bot's user ID (house account for /rob) |
|
|
||||||
| `MIN_BAIL` | `economy.py` | Minimum bail payment (350⬡) |
|
|
||||||
| `COIN` | `economy.py` | The coin emoji string |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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 investment
|
|
||||||
- **`gaming_laptop`** (1500⬡) 5% interest, capped 500⬡/day - snowballs with large balance
|
|
||||||
- `anticheat` is consumable (2 uses) - only item that can be re-bought
|
|
||||||
- `karikas` (T3) is the only item that preserves a daily streak across missed days
|
|
||||||
- `reguleeritav_laud` (T2) stacks with `gaming_hiir`: combined ×1.5 × ×1.25 = ×1.875
|
|
||||||
- **Fishing** is a steady passive income; `kalavork` (T3) dramatically increases fish value by bumping rarity
|
|
||||||
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Create data and logs directories
|
||||||
|
RUN mkdir -p data logs
|
||||||
|
|
||||||
|
CMD ["python", "bot.py"]
|
||||||
30
bot.py
30
bot.py
@@ -21,22 +21,20 @@ import psutil
|
|||||||
|
|
||||||
import config
|
import config
|
||||||
import strings as S
|
import strings as S
|
||||||
import economy
|
from core import economy, pb_client, sheets
|
||||||
import pb_client
|
from core.member_sync import SyncResult
|
||||||
import sheets
|
from commands.dev_member_commands import register_dev_member_commands
|
||||||
from dev_member_commands import register_dev_member_commands
|
from commands.dev_member_runtime import handle_member_join, run_birthday_daily
|
||||||
from dev_member_runtime import handle_member_join, run_birthday_daily
|
from commands.economy_admin_commands import register_economy_admin_commands
|
||||||
from economy_admin_commands import register_economy_admin_commands
|
from commands.economy_extra_commands import register_economy_extra_commands
|
||||||
from economy_extra_commands import register_economy_extra_commands
|
from commands.economy_fish_commands import register_economy_fish_commands
|
||||||
from economy_fish_commands import register_economy_fish_commands
|
from commands.economy_games_commands import register_economy_games_commands
|
||||||
from economy_games_commands import register_economy_games_commands
|
from commands.economy_income_commands import register_economy_income_commands
|
||||||
from economy_income_commands import register_economy_income_commands
|
from commands.economy_prestige_commands import register_prestige_commands
|
||||||
from economy_prestige_commands import register_prestige_commands
|
from commands.economy_profile_commands import register_economy_profile_commands
|
||||||
from economy_profile_commands import register_economy_profile_commands
|
from commands.economy_support_commands import register_economy_support_commands
|
||||||
from economy_support_commands import register_economy_support_commands
|
from commands.ops_channel_commands import register_ops_channel_commands
|
||||||
from ops_channel_commands import register_ops_channel_commands
|
from commands.ops_admin_commands import register_ops_admin_commands
|
||||||
from ops_admin_commands import register_ops_admin_commands
|
|
||||||
from member_sync import SyncResult
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ from typing import Callable
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
|
|
||||||
import sheets
|
from core import sheets
|
||||||
import strings as S
|
import strings as S
|
||||||
from member_sync import announce_birthday, sync_member
|
from core.member_sync import announce_birthday, sync_member
|
||||||
|
|
||||||
|
|
||||||
class BirthdayPages(discord.ui.View):
|
class BirthdayPages(discord.ui.View):
|
||||||
@@ -6,8 +6,8 @@ from collections.abc import Callable
|
|||||||
import discord
|
import discord
|
||||||
|
|
||||||
import config
|
import config
|
||||||
import sheets
|
from core import sheets
|
||||||
from member_sync import announce_birthday, is_birthday_today, sync_member
|
from core.member_sync import announce_birthday, is_birthday_today, sync_member
|
||||||
|
|
||||||
|
|
||||||
async def run_birthday_daily(
|
async def run_birthday_daily(
|
||||||
@@ -7,7 +7,7 @@ from collections.abc import Awaitable, Callable
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
|
|
||||||
import economy
|
from core import economy
|
||||||
import strings as S
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ from collections.abc import Awaitable, Callable, MutableSet
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
|
|
||||||
import economy
|
from core import economy
|
||||||
import strings as S
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ from collections.abc import Awaitable, Callable, MutableSet
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
|
|
||||||
import economy
|
from core import economy
|
||||||
import strings as S
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ from collections.abc import Awaitable, Callable, MutableSet
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
|
|
||||||
import economy
|
from core import economy
|
||||||
import strings as S
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ from collections.abc import Awaitable, Callable
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
|
|
||||||
import economy
|
from core import economy
|
||||||
import strings as S
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
|
|
||||||
import economy
|
from core import economy
|
||||||
import strings as S
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ from collections.abc import Awaitable, Callable
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
|
|
||||||
import economy
|
from core import economy
|
||||||
import strings as S
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
@@ -5,7 +5,7 @@ from collections.abc import Callable
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
|
|
||||||
import economy
|
from core import economy
|
||||||
import strings as S
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ from collections.abc import Callable
|
|||||||
import discord
|
import discord
|
||||||
from discord import app_commands
|
from discord import app_commands
|
||||||
|
|
||||||
import economy
|
from core import economy
|
||||||
import strings as S
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
33
compose.yaml
Normal file
33
compose.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
services:
|
||||||
|
pocketbase:
|
||||||
|
image: ghcr.io/muchobien/pocketbase:latest
|
||||||
|
container_name: tipibot-pocketbase
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- pb_data:/pb_data
|
||||||
|
ports:
|
||||||
|
- "8090:8090"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8090/api/health"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
bot:
|
||||||
|
build: .
|
||||||
|
container_name: tipibot
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
pocketbase:
|
||||||
|
condition: service_healthy
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- PB_URL=http://pocketbase:8090
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./logs:/app/logs
|
||||||
|
- ./credentials.json:/app/credentials.json:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pb_data:
|
||||||
@@ -12,13 +12,20 @@ import random
|
|||||||
from datetime import date, datetime, timedelta, timezone
|
from datetime import date, datetime, timedelta, timezone
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
import pb_client
|
import aiohttp
|
||||||
|
|
||||||
|
from . import pb_client
|
||||||
|
|
||||||
import strings
|
import strings
|
||||||
|
|
||||||
_txn_log = logging.getLogger("tipiCOIN.txn")
|
_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:
|
def _txn(event: str, **fields) -> None:
|
||||||
"""Log a single economy transaction to the transactions logger."""
|
"""Log a single economy transaction to the transactions logger."""
|
||||||
body = " ".join(f"{k}={v}" for k, v in fields.items())
|
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:
|
async def get_user(user_id: int) -> UserData:
|
||||||
"""Fetch user data from PocketBase, creating a default record if first seen."""
|
"""Fetch user data from PocketBase, creating a default record if first seen."""
|
||||||
uid = str(user_id)
|
uid = str(user_id)
|
||||||
record = await pb_client.get_record(uid)
|
try:
|
||||||
if record is None:
|
record = await pb_client.get_record(uid)
|
||||||
default = _default_user()
|
if record is None:
|
||||||
default["user_id"] = uid # type: ignore[typeddict-unknown-key]
|
default = _default_user()
|
||||||
record = await pb_client.create_record(default)
|
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()
|
user = _default_user()
|
||||||
for key in list(user.keys()):
|
for key in list(user.keys()):
|
||||||
if key in record:
|
if key in record:
|
||||||
@@ -696,7 +707,10 @@ async def _commit(user_id: int, user: UserData) -> None:
|
|||||||
# /daily
|
# /daily
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
async def do_daily(user_id: int) -> dict:
|
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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
|
|
||||||
@@ -772,7 +786,10 @@ _WORK_JOBS = strings.WORK_JOBS
|
|||||||
|
|
||||||
|
|
||||||
async def do_work(user_id: int) -> dict:
|
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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "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:
|
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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "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:
|
async def do_fish_start(user_id: int) -> dict:
|
||||||
"""Check cooldown + jail, set cooldown. Call before starting the fishing minigame."""
|
"""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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
if jail := _is_jailed(user):
|
if jail := _is_jailed(user):
|
||||||
@@ -928,7 +951,8 @@ async def do_fish_sell(user_id: int, indices: list[int] | None = None) -> dict:
|
|||||||
to_sell = inv
|
to_sell = inv
|
||||||
remaining = []
|
remaining = []
|
||||||
else:
|
else:
|
||||||
to_sell = [inv[i] for i in sorted(set(indices)) if 0 <= i < len(inv)]
|
valid_indices = [i if i >= 0 else len(inv) + i for i in indices]
|
||||||
|
to_sell = [inv[i] for i in sorted(set(valid_indices)) if 0 <= i < len(inv)]
|
||||||
keep_idx = set(range(len(inv))) - set(indices)
|
keep_idx = set(range(len(inv))) - set(indices)
|
||||||
remaining = [inv[i] for i in sorted(keep_idx)]
|
remaining = [inv[i] for i in sorted(keep_idx)]
|
||||||
|
|
||||||
@@ -974,7 +998,10 @@ async def do_fishbook(user_id: int) -> dict:
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
async def do_prestige(user_id: int) -> dict:
|
async def do_prestige(user_id: int) -> dict:
|
||||||
"""Prestige: requires level 30, earns PP, resets balance/exp/items/cooldowns."""
|
"""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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
|
|
||||||
@@ -1022,7 +1049,10 @@ async def do_prestige_buy(user_id: int, upgrade_id: str) -> dict:
|
|||||||
if upgrade_id not in PRESTIGE_SHOP:
|
if upgrade_id not in PRESTIGE_SHOP:
|
||||||
return {"ok": False, "reason": "not_found"}
|
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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
|
|
||||||
@@ -1116,7 +1146,10 @@ _CRIME_LOSE = strings.CRIME_LOSE
|
|||||||
|
|
||||||
|
|
||||||
async def do_crime(user_id: int) -> dict:
|
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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
|
|
||||||
@@ -1209,10 +1242,16 @@ async def do_bail(user_id: int) -> dict:
|
|||||||
# /rob
|
# /rob
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
async def do_rob(robber_id: int, target_id: int) -> dict:
|
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"):
|
if robber.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "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"):
|
if cd := _cooldown_remaining(robber, "rob"):
|
||||||
return {"ok": False, "reason": "cooldown", "remaining": cd}
|
return {"ok": False, "reason": "cooldown", "remaining": cd}
|
||||||
@@ -1284,7 +1323,10 @@ async def do_rob(robber_id: int, target_id: int) -> dict:
|
|||||||
# /roulette
|
# /roulette
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
async def do_roulette(user_id: int, bet: int, colour: str) -> dict:
|
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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
if jail := _is_jailed(user):
|
if jail := _is_jailed(user):
|
||||||
@@ -1324,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:
|
async def do_game_bet(user_id: int, bet: int, outcome: str) -> dict:
|
||||||
"""Settle a simple win/tie/lose bet. outcome: 'win' | 'tie' | 'lose'."""
|
"""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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
if jail := _is_jailed(user):
|
if jail := _is_jailed(user):
|
||||||
@@ -1377,7 +1422,10 @@ def _spin() -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def do_slots(user_id: int, bet: int) -> dict:
|
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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
if jail := _is_jailed(user):
|
if jail := _is_jailed(user):
|
||||||
@@ -1430,7 +1478,10 @@ async def do_slots(user_id: int, bet: int) -> dict:
|
|||||||
# /give
|
# /give
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
async def do_give(giver_id: int, receiver_id: int, amount: int) -> dict:
|
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"):
|
if giver.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
|
|
||||||
@@ -1440,7 +1491,10 @@ async def do_give(giver_id: int, receiver_id: int, amount: int) -> dict:
|
|||||||
if giver["balance"] < amount:
|
if giver["balance"] < amount:
|
||||||
return {"ok": False, "reason": "insufficient"}
|
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
|
giver["balance"] -= amount
|
||||||
receiver["balance"] += amount
|
receiver["balance"] += amount
|
||||||
giver["total_given"] = giver.get("total_given", 0) + amount
|
giver["total_given"] = giver.get("total_given", 0) + amount
|
||||||
@@ -1462,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:
|
async def do_buy(user_id: int, item_id: str) -> dict:
|
||||||
if item_id not in SHOP:
|
if item_id not in SHOP:
|
||||||
return {"ok": False, "reason": "not_found"}
|
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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
|
|
||||||
@@ -1627,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:
|
async def do_blackjack_bet(user_id: int, bet: int) -> dict:
|
||||||
"""Deduct the initial blackjack bet. Returns ok/fail."""
|
"""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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
if jail := _is_jailed(user):
|
if jail := _is_jailed(user):
|
||||||
@@ -1682,7 +1742,10 @@ async def do_get_jailed() -> list[tuple[int, timedelta]]:
|
|||||||
|
|
||||||
async def do_heist_check(user_id: int) -> dict:
|
async def do_heist_check(user_id: int) -> dict:
|
||||||
"""Check whether a user is eligible to join a heist."""
|
"""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"):
|
if user.get("eco_banned"):
|
||||||
return {"ok": False, "reason": "banned"}
|
return {"ok": False, "reason": "banned"}
|
||||||
if jail := _is_jailed(user):
|
if jail := _is_jailed(user):
|
||||||
@@ -9,7 +9,7 @@ from dataclasses import dataclass, field
|
|||||||
import discord
|
import discord
|
||||||
|
|
||||||
import config
|
import config
|
||||||
import sheets
|
from . import sheets
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
_PLACEHOLDER = {"-", "x", "n/a", "none", "ei"}
|
_PLACEHOLDER = {"-", "x", "n/a", "none", "ei"}
|
||||||
@@ -28,7 +28,7 @@ PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
|||||||
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||||
ECONOMY_COLLECTION = config.PB_ECONOMY_COLLECTION
|
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)
|
# Persistent session (created once, reused for the lifetime of the process)
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"2026-03-14": [
|
|
||||||
"650046190972305409"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"allowed_channels": [
|
|
||||||
"1482398641699291357"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
11386
logs/bot.log
11386
logs/bot.log
File diff suppressed because it is too large
Load Diff
@@ -1,108 +0,0 @@
|
|||||||
2026-04-01 18:44:29 | WORK user=272518654715887618 earned=+54 lucky=False bal=24077
|
|
||||||
2026-04-01 20:02:11 | BEG user=272518654715887618 earned=+23 jailed=False bal=24100
|
|
||||||
2026-04-01 20:02:53 | ROULETTE_WIN user=178852380018868224 bet=15214708 colour=punane result=punane mult=1 bal=30429416
|
|
||||||
2026-04-01 20:03:42 | ROULETTE_WIN user=178852380018868224 bet=30429416 colour=must result=must mult=1 bal=60858832
|
|
||||||
2026-04-01 20:04:16 | ROULETTE_LOSE user=178852380018868224 bet=60858832 colour=punane result=must mult=1 bal=0
|
|
||||||
2026-04-01 20:06:37 | BEG user=401373976431165449 earned=+52 jailed=False bal=8446
|
|
||||||
2026-04-01 20:06:39 | WORK user=401373976431165449 earned=+60 lucky=False bal=8506
|
|
||||||
2026-04-01 20:06:52 | DAILY user=401373976431165449 earned=+750 streak=1 bal=9256
|
|
||||||
2026-04-01 20:07:07 | DAILY user=272518654715887618 earned=+825 streak=1 bal=24925
|
|
||||||
2026-04-01 20:07:12 | CRIME_WIN user=272518654715887618 earned=+331 bal=25256
|
|
||||||
2026-04-01 20:07:38 | CRIME_WIN user=401373976431165449 earned=+391 bal=9647
|
|
||||||
2026-04-01 20:07:49 | BUY user=401373976431165449 item=echolood cost=-8000 bal=1647
|
|
||||||
2026-04-01 20:09:16 | ROB_BLOCKED robber=824516445382901800 victim=340451525799182357 fine=-118 robber_bal=891 ac_uses_left=1
|
|
||||||
2026-04-01 20:09:23 | ROB_BLOCKED robber=401373976431165449 victim=340451525799182357 fine=-175 robber_bal=1472 ac_uses_left=0
|
|
||||||
2026-04-01 20:09:52 | ROB_WIN robber=178852380018868224 victim=340451525799182357 stolen=+34868 jackpot=False robber_bal=34868 victim_bal=140238
|
|
||||||
2026-04-01 20:10:48 | DAILY user=367347301322326016 earned=+712 streak=1 bal=8462
|
|
||||||
2026-04-01 20:10:55 | WORK user=367347301322326016 earned=+25 lucky=False bal=8487
|
|
||||||
2026-04-01 20:11:00 | BEG user=367347301322326016 earned=+15 jailed=False bal=8502
|
|
||||||
2026-04-01 20:11:37 | ROB_FAIL robber=272518654715887618 victim=340451525799182357 fine=-140 robber_bal=25116
|
|
||||||
2026-04-01 20:15:19 | HEIST_FAIL user=178852380018868224 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=33868
|
|
||||||
2026-04-01 20:15:19 | HEIST_FAIL user=340451525799182357 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=139238
|
|
||||||
2026-04-01 20:15:19 | HEIST_FAIL user=272518654715887618 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=24116
|
|
||||||
2026-04-01 20:15:19 | HEIST_FAIL user=209554152584380420 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=20112
|
|
||||||
2026-04-01 20:15:19 | HEIST_FAIL user=401373976431165449 fine=-220 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=1252
|
|
||||||
2026-04-01 20:15:19 | HEIST_FAIL user=344531774518591498 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=112701
|
|
||||||
2026-04-01 20:15:19 | HEIST_FAIL user=367347301322326016 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=7502
|
|
||||||
2026-04-01 20:15:45 | JAIL_FREE user=272518654715887618 method=doubles
|
|
||||||
2026-04-01 20:20:07 | JAIL_FREE user=344531774518591498 method=doubles
|
|
||||||
2026-04-01 20:20:14 | DAILY user=344531774518591498 earned=+825 streak=1 bal=113526
|
|
||||||
2026-04-01 20:20:16 | WORK user=344531774518591498 earned=+45 lucky=False bal=113571
|
|
||||||
2026-04-01 20:20:19 | WORK user=272518654715887618 earned=+55 lucky=False bal=24171
|
|
||||||
2026-04-01 20:20:19 | BEG user=344531774518591498 earned=+22 jailed=False bal=113593
|
|
||||||
2026-04-01 20:20:36 | BLACKJACK user=272518654715887618 payout=+0 net=-24171 bal=0
|
|
||||||
2026-04-01 20:21:03 | CRIME_FAIL user=344531774518591498 fine=-90 jailed=True bal=113503
|
|
||||||
2026-04-01 20:21:11 | FISH user=272518654715887618 fish=koger weight=590 value=15
|
|
||||||
2026-04-01 20:21:45 | ROB_WIN robber=344531774518591498 victim=340451525799182357 stolen=+15566 jackpot=False robber_bal=129069 victim_bal=123672
|
|
||||||
2026-04-01 20:25:40 | BAIL_PAID user=178852380018868224 fine=-8760 pct=26% bal=25108
|
|
||||||
2026-04-01 20:28:28 | BEG user=178852380018868224 earned=+28 jailed=False bal=25136
|
|
||||||
2026-04-01 20:28:30 | WORK user=178852380018868224 earned=+92 lucky=False bal=25228
|
|
||||||
2026-04-01 20:28:33 | DAILY user=178852380018868224 earned=+825 streak=1 bal=26053
|
|
||||||
2026-04-01 20:28:38 | CRIME_WIN user=178852380018868224 earned=+640 bal=26693
|
|
||||||
2026-04-01 20:35:35 | BEG user=401373976431165449 earned=+56 jailed=True bal=1308
|
|
||||||
2026-04-01 20:36:20 | JAIL_FREE user=401373976431165449 method=doubles
|
|
||||||
2026-04-01 20:37:47 | FISH user=401373976431165449 fish=angerjas weight=989 value=79
|
|
||||||
2026-04-01 20:46:02 | DAILY user=338622999127261185 earned=+300 streak=1 bal=300
|
|
||||||
2026-04-01 20:56:03 | BEG user=344531774518591498 earned=+60 jailed=False bal=129129
|
|
||||||
2026-04-01 20:56:29 | ROULETTE_LOSE user=344531774518591498 bet=1000 colour=punane result=must mult=1 bal=128129
|
|
||||||
2026-04-01 21:01:23 | BEG user=272518654715887618 earned=+33 jailed=False bal=33
|
|
||||||
2026-04-01 21:01:42 | FISH user=272518654715887618 fish=sarj weight=151 value=6
|
|
||||||
2026-04-01 21:02:38 | FISH user=344531774518591498 fish=viidikas weight=98 value=6
|
|
||||||
2026-04-01 21:02:57 | FISH_SELL user=344531774518591498 count=2 coins=+13 bal=128142
|
|
||||||
2026-04-01 21:03:10 | BEG user=401373976431165449 earned=+54 jailed=False bal=1362
|
|
||||||
2026-04-01 21:03:31 | FISH user=401373976431165449 fish=siig weight=584 value=63
|
|
||||||
2026-04-01 21:05:19 | WORK user=401373976431165449 earned=+131 lucky=False bal=1493
|
|
||||||
2026-04-01 21:05:38 | FISH user=401373976431165449 fish=siig weight=1624 value=112
|
|
||||||
2026-04-01 21:05:48 | FISH_SELL user=401373976431165449 count=3 coins=+254 bal=1747
|
|
||||||
2026-04-01 21:08:31 | BEG user=401373976431165449 earned=+32 jailed=False bal=1779
|
|
||||||
2026-04-01 21:08:45 | FISH user=401373976431165449 fish=tougjas weight=3051 value=207
|
|
||||||
2026-04-01 21:09:01 | ROULETTE_WIN user=401373976431165449 bet=1000 colour=punane result=punane mult=1 bal=2779
|
|
||||||
2026-04-01 21:09:35 | BLACKJACK user=401373976431165449 payout=+3000 net=+1500 bal=4279
|
|
||||||
2026-04-01 21:09:35 | ROULETTE_LOSE user=344531774518591498 bet=1000 colour=punane result=must mult=1 bal=127142
|
|
||||||
2026-04-01 21:10:05 | ROULETTE_WIN user=401373976431165449 bet=1000 colour=punane result=punane mult=1 bal=5279
|
|
||||||
2026-04-01 21:10:46 | ROULETTE_WIN user=344531774518591498 bet=1000 colour=punane result=punane mult=1 bal=128142
|
|
||||||
2026-04-01 21:11:03 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=must result=punane mult=1 bal=4279
|
|
||||||
2026-04-01 21:11:24 | ROULETTE_LOSE user=344531774518591498 bet=1000 colour=punane result=roheline mult=1 bal=127142
|
|
||||||
2026-04-01 21:15:50 | WORK user=338622999127261185 earned=+15 lucky=False bal=315
|
|
||||||
2026-04-01 21:15:54 | CRIME_WIN user=338622999127261185 earned=+453 bal=768
|
|
||||||
2026-04-01 21:16:00 | BEG user=338622999127261185 earned=+20 jailed=False bal=788
|
|
||||||
2026-04-01 21:16:13 | FISH user=338622999127261185 fish=ahven weight=422 value=14
|
|
||||||
2026-04-01 21:18:36 | BEG user=401373976431165449 earned=+20 jailed=False bal=4299
|
|
||||||
2026-04-01 21:18:52 | FISH user=401373976431165449 fish=karpkala weight=1920 value=47
|
|
||||||
2026-04-01 21:20:58 | SLOTS_TRIPLE user=344531774518591498 bet=1000 change=4000 bal=131142
|
|
||||||
2026-04-01 21:21:39 | SLOTS_MISS user=344531774518591498 bet=1000 change=-1000 bal=130142
|
|
||||||
2026-04-01 21:28:25 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=3299
|
|
||||||
2026-04-01 21:29:10 | SLOTS_PAIR user=344531774518591498 bet=1000 change=500 bal=130642
|
|
||||||
2026-04-01 21:29:28 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=2299
|
|
||||||
2026-04-01 21:30:49 | SLOTS_PAIR user=344531774518591498 bet=1000 change=500 bal=131142
|
|
||||||
2026-04-01 21:31:30 | SLOTS_PAIR user=344531774518591498 bet=1000 change=500 bal=131642
|
|
||||||
2026-04-01 21:31:33 | ROULETTE_WIN user=401373976431165449 bet=1000 colour=punane result=punane mult=1 bal=3299
|
|
||||||
2026-04-01 21:31:37 | BEG user=401373976431165449 earned=+68 jailed=False bal=3367
|
|
||||||
2026-04-01 21:32:05 | FISH user=401373976431165449 fish=latikas weight=2351 value=66
|
|
||||||
2026-04-01 21:33:14 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=2367
|
|
||||||
2026-04-01 21:35:09 | SLOTS_MISS user=344531774518591498 bet=1000 change=-1000 bal=130642
|
|
||||||
2026-04-01 21:35:14 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=1367
|
|
||||||
2026-04-01 21:48:35 | ROB_FAIL robber=338622999127261185 victim=340451525799182357 fine=-237 robber_bal=551
|
|
||||||
2026-04-01 21:49:59 | BEG user=401373976431165449 earned=+56 jailed=False bal=1423
|
|
||||||
2026-04-01 21:50:15 | FISH user=401373976431165449 fish=vimb weight=856 value=462
|
|
||||||
2026-04-01 22:11:02 | BEG user=401373976431165449 earned=+52 jailed=False bal=1475
|
|
||||||
2026-04-01 22:11:13 | WORK user=401373976431165449 earned=+73 lucky=False bal=1548
|
|
||||||
2026-04-01 22:11:27 | ROB_WIN robber=401373976431165449 victim=367347301322326016 stolen=+1818 jackpot=False robber_bal=3366 victim_bal=5684
|
|
||||||
2026-04-01 22:11:44 | FISH user=401373976431165449 fish=lohe weight=2973 value=313
|
|
||||||
2026-04-01 22:13:30 | ROULETTE_LOSE user=401373976431165449 bet=2000 colour=punane result=roheline mult=1 bal=1366
|
|
||||||
2026-04-01 22:14:33 | ROULETTE_LOSE user=401373976431165449 bet=1366 colour=punane result=must mult=1 bal=0
|
|
||||||
2026-04-01 22:40:19 | WORK user=367347301322326016 earned=+82 lucky=False bal=5766
|
|
||||||
2026-04-01 22:40:23 | CRIME_FAIL user=367347301322326016 fine=-100 jailed=True bal=5666
|
|
||||||
2026-04-01 22:46:27 | WORK user=344531774518591498 earned=+116 lucky=False bal=130758
|
|
||||||
2026-04-01 22:46:30 | BEG user=344531774518591498 earned=+58 jailed=False bal=130816
|
|
||||||
2026-04-01 22:46:35 | CRIME_WIN user=344531774518591498 earned=+419 bal=131235
|
|
||||||
2026-04-01 22:46:44 | ROB_FAIL robber=344531774518591498 victim=340451525799182357 fine=-246 robber_bal=130989
|
|
||||||
2026-04-01 22:47:01 | FISH user=344531774518591498 fish=viidikas weight=80 value=5
|
|
||||||
2026-04-01 22:48:58 | WORK user=178852380018868224 earned=+106 lucky=True bal=26799
|
|
||||||
2026-04-01 22:49:03 | CRIME_WIN user=178852380018868224 earned=+486 bal=27285
|
|
||||||
2026-04-01 22:49:05 | BEG user=178852380018868224 earned=+76 jailed=False bal=27361
|
|
||||||
2026-04-01 22:52:25 | BEG user=401373976431165449 earned=+44 jailed=False bal=44
|
|
||||||
2026-04-01 22:52:29 | WORK user=401373976431165449 earned=+103 lucky=False bal=147
|
|
||||||
2026-04-01 22:52:39 | CRIME_FAIL user=401373976431165449 fine=-125 jailed=False bal=22
|
|
||||||
2026-04-01 22:53:01 | FISH user=401373976431165449 fish=latikas weight=1217 value=40
|
|
||||||
2026-04-01 22:57:11 | SLOTS_PAIR user=344531774518591498 bet=10000 change=5000 bal=135989
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -23,12 +23,12 @@ from pathlib import Path
|
|||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Ensure the project root is on sys.path so pb_client can be imported
|
# Ensure the project root is on sys.path so core modules can be imported
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
import pb_client # noqa: E402 (needs dotenv loaded first)
|
from core import pb_client # noqa: E402 (needs dotenv loaded first)
|
||||||
|
|
||||||
DATA_FILE = Path("data") / "economy.json"
|
DATA_FILE = Path("data") / "economy.json"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user