Compare commits
2 Commits
rewrite
...
rewrite-v2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d1981420d | ||
|
|
14927b610d |
36
.env.example
36
.env.example
@@ -1,5 +1,12 @@
|
|||||||
# Discord bot token (from https://discord.com/developers/applications)
|
# Bot runtime profile: dev (economy + member tools) or economy (economy-only)
|
||||||
DISCORD_TOKEN=your-bot-token-here
|
BOT_PROFILE=dev
|
||||||
|
|
||||||
|
# Profile-specific Discord bot tokens (from https://discord.com/developers/applications)
|
||||||
|
DISCORD_TOKEN_DEV=your-dev-bot-token-here
|
||||||
|
DISCORD_TOKEN_ECONOMY=your-economy-bot-token-here
|
||||||
|
|
||||||
|
# Legacy fallback token (optional, backward compatibility)
|
||||||
|
DISCORD_TOKEN=
|
||||||
|
|
||||||
# Google Sheets spreadsheet ID (the long string in the sheet URL)
|
# Google Sheets spreadsheet ID (the long string in the sheet URL)
|
||||||
SHEET_ID=your-google-sheet-id-here
|
SHEET_ID=your-google-sheet-id-here
|
||||||
@@ -7,11 +14,21 @@ SHEET_ID=your-google-sheet-id-here
|
|||||||
# Path to Google service account credentials JSON
|
# Path to Google service account credentials JSON
|
||||||
GOOGLE_CREDS_PATH=credentials.json
|
GOOGLE_CREDS_PATH=credentials.json
|
||||||
|
|
||||||
# Guild (server) ID - right-click your server with dev mode on
|
# Profile-specific guild (server) IDs - right-click your server with dev mode on
|
||||||
GUILD_ID=your-guild-id-here
|
GUILD_ID_DEV=your-dev-guild-id-here
|
||||||
|
GUILD_ID_ECONOMY=your-economy-guild-id-here
|
||||||
|
|
||||||
# Channel ID where birthday announcements are posted (bot sends @here on the birthday at 09:00 Tallinn time)
|
# Legacy fallback guild ID (optional, backward compatibility)
|
||||||
BIRTHDAY_CHANNEL_ID=your-channel-id-here
|
GUILD_ID=
|
||||||
|
|
||||||
|
# Channel ID where birthday announcements are posted (dev profile)
|
||||||
|
BIRTHDAY_CHANNEL_ID_DEV=your-dev-birthday-channel-id-here
|
||||||
|
|
||||||
|
# Optional birthday channel for economy profile (normally unset for economy-only bot)
|
||||||
|
BIRTHDAY_CHANNEL_ID_ECONOMY=
|
||||||
|
|
||||||
|
# Legacy fallback birthday channel ID (optional, backward compatibility)
|
||||||
|
BIRTHDAY_CHANNEL_ID=
|
||||||
|
|
||||||
# How many days before a birthday the on-join check counts as "coming up"
|
# How many days before a birthday the on-join check counts as "coming up"
|
||||||
BIRTHDAY_WINDOW_DAYS=7
|
BIRTHDAY_WINDOW_DAYS=7
|
||||||
@@ -20,3 +37,10 @@ BIRTHDAY_WINDOW_DAYS=7
|
|||||||
PB_URL=http://127.0.0.1:8090
|
PB_URL=http://127.0.0.1:8090
|
||||||
PB_ADMIN_EMAIL=admin@example.com
|
PB_ADMIN_EMAIL=admin@example.com
|
||||||
PB_ADMIN_PASSWORD=your-pb-admin-password
|
PB_ADMIN_PASSWORD=your-pb-admin-password
|
||||||
|
|
||||||
|
# Profile-specific PocketBase collections
|
||||||
|
PB_ECONOMY_COLLECTION_DEV=economy_users_dev
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY=economy_users_prod
|
||||||
|
|
||||||
|
# Legacy fallback collection name (optional, backward compatibility)
|
||||||
|
PB_ECONOMY_COLLECTION=
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -70,7 +70,7 @@ 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.
|
1. Download `pocketbase.exe` (Windows) from https://pocketbase.io/docs/ and place it in the project root.
|
||||||
2. Start PocketBase: `.\pocketbase.exe serve`
|
2. Start PocketBase: `.\pocketbase.exe serve`
|
||||||
3. Open the admin UI at http://127.0.0.1:8090/_/ and create a superuser account.
|
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.
|
4. Create two collections for profile separation: `economy_users_dev` and `economy_users_prod` - see `docs/POCKETBASE_SETUP.md` for schema notes.
|
||||||
5. Set `PB_URL`, `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD` in `.env`.
|
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`
|
6. **One-time data migration** (only if you have an existing `data/economy.json`): `python scripts/migrate_to_pb.py`
|
||||||
|
|
||||||
@@ -88,15 +88,25 @@ cp .env.example .env
|
|||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `DISCORD_TOKEN` | Bot token from Discord Developer Portal |
|
| `BOT_PROFILE` | Runtime profile: `dev` or `economy` |
|
||||||
|
| `DISCORD_TOKEN_DEV` | Dev bot token from Discord Developer Portal |
|
||||||
|
| `DISCORD_TOKEN_ECONOMY` | Economy bot token from Discord Developer Portal |
|
||||||
|
| `DISCORD_TOKEN` | Legacy fallback token (optional) |
|
||||||
| `SHEET_ID` | ID from the Google Sheet URL |
|
| `SHEET_ID` | ID from the Google Sheet URL |
|
||||||
| `GOOGLE_CREDS_PATH` | Path to `credentials.json` (default: `credentials.json`) |
|
| `GOOGLE_CREDS_PATH` | Path to `credentials.json` (default: `credentials.json`) |
|
||||||
| `GUILD_ID` | Right-click server → Copy Server ID (Developer Mode on) |
|
| `GUILD_ID_DEV` | Dev bot guild ID |
|
||||||
| `BIRTHDAY_CHANNEL_ID` | Channel for birthday `@here` pings |
|
| `GUILD_ID_ECONOMY` | Economy bot guild ID |
|
||||||
|
| `GUILD_ID` | Legacy fallback guild ID (optional) |
|
||||||
|
| `BIRTHDAY_CHANNEL_ID_DEV` | Channel for birthday `@here` pings in dev profile |
|
||||||
|
| `BIRTHDAY_CHANNEL_ID_ECONOMY` | Optional birthday channel in economy profile |
|
||||||
|
| `BIRTHDAY_CHANNEL_ID` | Legacy fallback birthday channel ID (optional) |
|
||||||
| `BIRTHDAY_WINDOW_DAYS` | Days before birthday that on-join check flags it (default: 7) |
|
| `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_URL` | PocketBase base URL (default: `http://127.0.0.1:8090`) |
|
||||||
| `PB_ADMIN_EMAIL` | PocketBase superuser e-mail |
|
| `PB_ADMIN_EMAIL` | PocketBase superuser e-mail |
|
||||||
| `PB_ADMIN_PASSWORD` | PocketBase superuser password |
|
| `PB_ADMIN_PASSWORD` | PocketBase superuser password |
|
||||||
|
| `PB_ECONOMY_COLLECTION_DEV` | PocketBase collection used by `BOT_PROFILE=dev` |
|
||||||
|
| `PB_ECONOMY_COLLECTION_ECONOMY` | PocketBase collection used by `BOT_PROFILE=economy` |
|
||||||
|
| `PB_ECONOMY_COLLECTION` | Legacy fallback collection (optional) |
|
||||||
|
|
||||||
### 6. Install & Run
|
### 6. Install & Run
|
||||||
|
|
||||||
@@ -110,7 +120,12 @@ pip install -r requirements.txt
|
|||||||
# Terminal 1 - keep running
|
# Terminal 1 - keep running
|
||||||
.\pocketbase.exe serve
|
.\pocketbase.exe serve
|
||||||
|
|
||||||
# Terminal 2
|
# Terminal 2 (dev bot)
|
||||||
|
set BOT_PROFILE=dev
|
||||||
|
python bot.py
|
||||||
|
|
||||||
|
# Terminal 3 (economy bot)
|
||||||
|
set BOT_PROFILE=economy
|
||||||
python bot.py
|
python bot.py
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -146,6 +161,8 @@ Admins (bot lacks permission to modify them) are silently skipped and still mark
|
|||||||
## Admin Commands
|
## Admin Commands
|
||||||
|
|
||||||
> These commands are hidden from users who don't have the required permission. Override per-role in **Server Settings → Integrations → Bot**.
|
> These commands are hidden from users who don't have the required permission. Override per-role in **Server Settings → Integrations → Bot**.
|
||||||
|
>
|
||||||
|
> Profile note: `/check`, `/member`, and `/birthdays` are available only when `BOT_PROFILE=dev`.
|
||||||
|
|
||||||
| Command | Permission | What it does |
|
| Command | Permission | What it does |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|||||||
50
config.py
50
config.py
@@ -3,14 +3,58 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
BOT_PROFILE = os.getenv("BOT_PROFILE", "dev").strip().lower() or "dev"
|
||||||
|
if BOT_PROFILE not in {"dev", "economy"}:
|
||||||
|
raise SystemExit("BOT_PROFILE must be either 'dev' or 'economy'.")
|
||||||
|
|
||||||
|
|
||||||
|
def _env_int(name: str, default: int) -> int:
|
||||||
|
raw = os.getenv(name)
|
||||||
|
if raw is None or not raw.strip():
|
||||||
|
return default
|
||||||
|
return int(raw)
|
||||||
|
|
||||||
|
|
||||||
|
_LEGACY_DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
|
||||||
|
DISCORD_TOKEN_DEV = os.getenv("DISCORD_TOKEN_DEV", "")
|
||||||
|
DISCORD_TOKEN_ECONOMY = os.getenv("DISCORD_TOKEN_ECONOMY", "")
|
||||||
|
DISCORD_TOKEN = (
|
||||||
|
DISCORD_TOKEN_ECONOMY if BOT_PROFILE == "economy" else DISCORD_TOKEN_DEV
|
||||||
|
) or _LEGACY_DISCORD_TOKEN
|
||||||
|
|
||||||
SHEET_ID = os.getenv("SHEET_ID")
|
SHEET_ID = os.getenv("SHEET_ID")
|
||||||
GOOGLE_CREDS_PATH = os.getenv("GOOGLE_CREDS_PATH", "credentials.json")
|
GOOGLE_CREDS_PATH = os.getenv("GOOGLE_CREDS_PATH", "credentials.json")
|
||||||
GUILD_ID = int(os.getenv("GUILD_ID", "0"))
|
|
||||||
BIRTHDAY_CHANNEL_ID = int(os.getenv("BIRTHDAY_CHANNEL_ID", "0"))
|
_LEGACY_GUILD_ID = _env_int("GUILD_ID", 0)
|
||||||
|
GUILD_ID_DEV = _env_int("GUILD_ID_DEV", _LEGACY_GUILD_ID)
|
||||||
|
GUILD_ID_ECONOMY = _env_int("GUILD_ID_ECONOMY", _LEGACY_GUILD_ID)
|
||||||
|
GUILD_ID = GUILD_ID_ECONOMY if BOT_PROFILE == "economy" else GUILD_ID_DEV
|
||||||
|
|
||||||
|
_LEGACY_BIRTHDAY_CHANNEL_ID = _env_int("BIRTHDAY_CHANNEL_ID", 0)
|
||||||
|
BIRTHDAY_CHANNEL_ID_DEV = _env_int("BIRTHDAY_CHANNEL_ID_DEV", _LEGACY_BIRTHDAY_CHANNEL_ID)
|
||||||
|
BIRTHDAY_CHANNEL_ID_ECONOMY = _env_int("BIRTHDAY_CHANNEL_ID_ECONOMY", 0)
|
||||||
|
BIRTHDAY_CHANNEL_ID = (
|
||||||
|
BIRTHDAY_CHANNEL_ID_ECONOMY
|
||||||
|
if BOT_PROFILE == "economy"
|
||||||
|
else BIRTHDAY_CHANNEL_ID_DEV
|
||||||
|
)
|
||||||
|
|
||||||
BIRTHDAY_WINDOW_DAYS = int(os.getenv("BIRTHDAY_WINDOW_DAYS", "7"))
|
BIRTHDAY_WINDOW_DAYS = int(os.getenv("BIRTHDAY_WINDOW_DAYS", "7"))
|
||||||
BASE_ROLE_IDS: list[int] = [1478304631930228779, 1478302278862766190]
|
BASE_ROLE_IDS: list[int] = [1478304631930228779, 1478302278862766190]
|
||||||
|
|
||||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
||||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
||||||
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
||||||
|
|
||||||
|
_LEGACY_PB_COLLECTION = os.getenv("PB_ECONOMY_COLLECTION", "").strip()
|
||||||
|
PB_ECONOMY_COLLECTION_DEV = (
|
||||||
|
os.getenv("PB_ECONOMY_COLLECTION_DEV", "").strip()
|
||||||
|
or (_LEGACY_PB_COLLECTION if _LEGACY_PB_COLLECTION else "economy_users_dev")
|
||||||
|
)
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY = (
|
||||||
|
os.getenv("PB_ECONOMY_COLLECTION_ECONOMY", "").strip()
|
||||||
|
or (_LEGACY_PB_COLLECTION if _LEGACY_PB_COLLECTION else "economy_users_prod")
|
||||||
|
)
|
||||||
|
PB_ECONOMY_COLLECTION = (
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY if BOT_PROFILE == "economy" else PB_ECONOMY_COLLECTION_DEV
|
||||||
|
)
|
||||||
|
|||||||
314
dev_member_commands.py
Normal file
314
dev_member_commands.py
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import sheets
|
||||||
|
import strings as S
|
||||||
|
from member_sync import announce_birthday, sync_member
|
||||||
|
|
||||||
|
|
||||||
|
class BirthdayPages(discord.ui.View):
|
||||||
|
def __init__(self, pages: list[discord.Embed], start: int = 0):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self.pages = pages
|
||||||
|
self.current = start
|
||||||
|
self._update_buttons()
|
||||||
|
|
||||||
|
def _update_buttons(self):
|
||||||
|
self.prev_button.disabled = self.current == 0
|
||||||
|
self.next_button.disabled = self.current >= len(self.pages) - 1
|
||||||
|
|
||||||
|
@discord.ui.button(label="◀", style=discord.ButtonStyle.secondary)
|
||||||
|
async def prev_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.current -= 1
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self.pages[self.current], view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="▶", style=discord.ButtonStyle.secondary)
|
||||||
|
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.current += 1
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self.pages[self.current], view=self)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_birthday_pages(
|
||||||
|
guild: discord.Guild | None = None,
|
||||||
|
) -> tuple[list[discord.Embed], int]:
|
||||||
|
"""Build 12 monthly embeds (one per calendar month).
|
||||||
|
|
||||||
|
Returns (pages, start_index) where start_index is the current month.
|
||||||
|
"""
|
||||||
|
rows = sheets.get_cache()
|
||||||
|
today = datetime.date.today()
|
||||||
|
|
||||||
|
by_month: dict[int, list[tuple[int, str, int | None]]] = {m: [] for m in range(1, 13)}
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
name = str(row.get("Nimi", "")).strip()
|
||||||
|
bday_str = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
if not name or not bday_str or bday_str.strip().lower() in ("-", "x", "n/a", "none", "ei"):
|
||||||
|
continue
|
||||||
|
bday = None
|
||||||
|
for fmt in ["%d/%m/%Y", "%Y-%m-%d", "%m-%d"]:
|
||||||
|
try:
|
||||||
|
bday = datetime.datetime.strptime(bday_str, fmt).date()
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if bday is None:
|
||||||
|
continue
|
||||||
|
raw_uid = str(row.get("User ID", "")).strip()
|
||||||
|
try:
|
||||||
|
uid = int(raw_uid) if raw_uid and raw_uid not in ("0", "-") else None
|
||||||
|
except ValueError:
|
||||||
|
uid = None
|
||||||
|
by_month[bday.month].append((bday.day, name, uid))
|
||||||
|
|
||||||
|
pages: list[discord.Embed] = []
|
||||||
|
for month in range(1, 13):
|
||||||
|
entries = sorted(by_month[month], key=lambda x: x[0])
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"🎂 {S.BIRTHDAY_MONTHS[month - 1]}",
|
||||||
|
color=0xF4A261,
|
||||||
|
)
|
||||||
|
if not entries:
|
||||||
|
embed.description = S.BIRTHDAY_UI["no_entries"]
|
||||||
|
else:
|
||||||
|
lines = []
|
||||||
|
for day, name, uid in entries:
|
||||||
|
try:
|
||||||
|
this_year = datetime.date(today.year, month, day)
|
||||||
|
except ValueError:
|
||||||
|
this_year = datetime.date(today.year, month, day - 1)
|
||||||
|
next_bday = this_year if this_year >= today else this_year.replace(year=today.year + 1)
|
||||||
|
days_until = (next_bday - today).days
|
||||||
|
if days_until == 0:
|
||||||
|
when = S.BIRTHDAY_UI["today"]
|
||||||
|
elif days_until == 1:
|
||||||
|
when = S.BIRTHDAY_UI["tomorrow"]
|
||||||
|
else:
|
||||||
|
when = S.BIRTHDAY_UI["in_days"].format(days=days_until)
|
||||||
|
display = name
|
||||||
|
if guild and uid:
|
||||||
|
member = guild.get_member(uid)
|
||||||
|
if member:
|
||||||
|
display = member.mention
|
||||||
|
lines.append(f"{display} - {day:02d}/{month:02d} · {when}")
|
||||||
|
embed.description = "\n".join(lines)
|
||||||
|
embed.set_footer(text=S.BIRTHDAY_UI["footer"].format(month=month, month_name=S.BIRTHDAY_MONTHS[month - 1]))
|
||||||
|
pages.append(embed)
|
||||||
|
|
||||||
|
return pages, today.month - 1
|
||||||
|
|
||||||
|
|
||||||
|
def _sheet_stats(rows: list[dict]) -> str:
|
||||||
|
"""Return a formatted string with sheet completeness statistics."""
|
||||||
|
total = len(rows)
|
||||||
|
missing_uid = []
|
||||||
|
missing_discord = []
|
||||||
|
missing_birthday = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
name = str(row.get("Nimi", "")).strip() or S.CHECK_UI["no_name"]
|
||||||
|
uid = str(row.get("User ID", "")).strip()
|
||||||
|
discord_name = str(row.get("Discord", "")).strip()
|
||||||
|
bday = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
|
||||||
|
if not uid or uid == "0":
|
||||||
|
missing_uid.append(name)
|
||||||
|
if not discord_name:
|
||||||
|
missing_discord.append(name)
|
||||||
|
if not bday:
|
||||||
|
missing_birthday.append(name)
|
||||||
|
|
||||||
|
lines = [S.CHECK_UI["sheet_stats_header"].format(total=total)]
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
def stat_line(label: str, missing: list[str]) -> str:
|
||||||
|
count = len(missing)
|
||||||
|
if count == 0:
|
||||||
|
return S.CHECK_UI["stat_ok"].format(label=label)
|
||||||
|
names = ", ".join(missing[:5])
|
||||||
|
more = S.CHECK_UI["stat_more"].format(count=count - 5) if count > 5 else ""
|
||||||
|
return S.CHECK_UI["stat_warn"].format(label=label, count=count, names=names, more=more)
|
||||||
|
|
||||||
|
lines.append(stat_line(S.CHECK_UI["stat_uid"], missing_uid))
|
||||||
|
lines.append(stat_line(S.CHECK_UI["stat_discord"], missing_discord))
|
||||||
|
lines.append(stat_line(S.CHECK_UI["stat_bday"], missing_birthday))
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def register_dev_member_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
has_announced_today: Callable[[int], bool],
|
||||||
|
mark_announced_today: Callable[[int], None],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="birthdays", description=S.CMD["birthdays"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_birthdays(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
sheets.refresh()
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(S.ERR["sheet_error"].format(error=e), ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
pages, start = _build_birthday_pages(guild=interaction.guild)
|
||||||
|
await interaction.followup.send(embed=pages[start], view=BirthdayPages(pages, start=start))
|
||||||
|
|
||||||
|
@tree.command(name="check", description=S.CMD["check"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_roles=True)
|
||||||
|
async def cmd_check(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
|
||||||
|
guild = interaction.guild
|
||||||
|
if guild is None:
|
||||||
|
await interaction.followup.send(S.ERR["guild_only"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = sheets.refresh()
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(S.ERR["sheet_error"].format(error=e), ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
ids_filled = 0
|
||||||
|
for row in data:
|
||||||
|
uid = str(row.get("User ID", "")).strip()
|
||||||
|
if uid and uid not in ("0", "-"):
|
||||||
|
continue
|
||||||
|
discord_name = str(row.get("Discord", "")).strip()
|
||||||
|
if not discord_name:
|
||||||
|
continue
|
||||||
|
guild_member = discord.utils.find(
|
||||||
|
lambda m, n=discord_name: m.name.lower() == n.lower(),
|
||||||
|
guild.members,
|
||||||
|
)
|
||||||
|
if guild_member:
|
||||||
|
sheets.set_user_id(discord_name, guild_member.id)
|
||||||
|
ids_filled += 1
|
||||||
|
|
||||||
|
data = sheets.get_cache()
|
||||||
|
|
||||||
|
changed_count = 0
|
||||||
|
not_found = 0
|
||||||
|
already_ok = 0
|
||||||
|
errors_total = 0
|
||||||
|
birthday_pings = 0
|
||||||
|
details: list[str] = []
|
||||||
|
sync_updates: list[tuple[int, bool]] = []
|
||||||
|
|
||||||
|
for member in guild.members:
|
||||||
|
if member.bot:
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = await sync_member(member, guild)
|
||||||
|
|
||||||
|
if result.not_found:
|
||||||
|
not_found += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
sync_updates.append((member.id, result.synced))
|
||||||
|
|
||||||
|
if result.errors:
|
||||||
|
errors_total += len(result.errors)
|
||||||
|
for err in result.errors:
|
||||||
|
details.append(S.CHECK_UI["detail_error"].format(error=err))
|
||||||
|
|
||||||
|
if result.changed:
|
||||||
|
changed_count += 1
|
||||||
|
parts = []
|
||||||
|
if result.nickname_changed:
|
||||||
|
parts.append(S.CHECK_UI["detail_nickname"])
|
||||||
|
if result.roles_added:
|
||||||
|
parts.append(S.CHECK_UI["detail_roles_added"].format(roles=", ".join(result.roles_added)))
|
||||||
|
details.append(S.CHECK_UI["detail_changed"].format(name=member.display_name, parts=", ".join(parts)))
|
||||||
|
else:
|
||||||
|
already_ok += 1
|
||||||
|
|
||||||
|
if result.birthday_soon and not has_announced_today(member.id):
|
||||||
|
birthday_pings += 1
|
||||||
|
await announce_birthday(member, bot)
|
||||||
|
mark_announced_today(member.id)
|
||||||
|
|
||||||
|
if sync_updates:
|
||||||
|
try:
|
||||||
|
sheets.batch_set_synced(sync_updates)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("/check batch_set_synced failed: %s", e)
|
||||||
|
|
||||||
|
summary_lines = [
|
||||||
|
S.CHECK_UI["done"],
|
||||||
|
S.CHECK_UI["already_ok"].format(count=already_ok),
|
||||||
|
S.CHECK_UI["fixed"].format(count=changed_count),
|
||||||
|
S.CHECK_UI["not_found"].format(count=not_found),
|
||||||
|
S.CHECK_UI["bday_pings"].format(count=birthday_pings),
|
||||||
|
]
|
||||||
|
if errors_total:
|
||||||
|
summary_lines.append(S.CHECK_UI["errors"].format(count=errors_total))
|
||||||
|
|
||||||
|
summary = "\n".join(summary_lines)
|
||||||
|
|
||||||
|
if details:
|
||||||
|
detail_text = "\n".join(details[:20])
|
||||||
|
summary += f"\n\n{S.CHECK_UI['details_header']}\n{detail_text}"
|
||||||
|
if len(details) > 20:
|
||||||
|
summary += "\n" + S.CHECK_UI["details_more"].format(count=len(details) - 20)
|
||||||
|
|
||||||
|
stats = _sheet_stats(data)
|
||||||
|
id_note = S.CHECK_UI["ids_filled"].format(count=ids_filled) if ids_filled else ""
|
||||||
|
summary = id_note + "\n" + summary + "\n\n" + stats
|
||||||
|
|
||||||
|
await interaction.followup.send(summary.strip(), ephemeral=True)
|
||||||
|
log.info(
|
||||||
|
"/check - OK=%d, Fixed=%d, NotFound=%d, IDs=%d, Errors=%d",
|
||||||
|
already_ok,
|
||||||
|
changed_count,
|
||||||
|
not_found,
|
||||||
|
ids_filled,
|
||||||
|
errors_total,
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="member", description=S.CMD["member"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_roles=True)
|
||||||
|
async def cmd_member(interaction: discord.Interaction, user: discord.Member):
|
||||||
|
row = sheets.find_member(user.id, user.name)
|
||||||
|
if row is None:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["member_not_found"].format(name=user.display_name),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(title=S.MEMBER_UI["title"].format(name=user.display_name), color=user.color)
|
||||||
|
|
||||||
|
bday_str = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
if bday_str and bday_str.lower() not in ("-", "x", "n/a", "none", "ei"):
|
||||||
|
for fmt in ["%d/%m/%Y", "%Y-%m-%d"]:
|
||||||
|
try:
|
||||||
|
bday = datetime.datetime.strptime(bday_str, fmt).date()
|
||||||
|
if 1920 <= bday.year <= datetime.date.today().year:
|
||||||
|
today = datetime.date.today()
|
||||||
|
age = today.year - bday.year - ((today.month, today.day) < (bday.month, bday.day))
|
||||||
|
embed.add_field(name=S.MEMBER_UI["age_field"], value=S.MEMBER_UI["age_val"].format(age=age), inline=True)
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for sheet_key, label in S.MEMBER_FIELDS:
|
||||||
|
val = str(row.get(sheet_key, "")).strip()
|
||||||
|
if val:
|
||||||
|
embed.add_field(name=label, value=val, inline=True)
|
||||||
|
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
93
dev_member_runtime.py
Normal file
93
dev_member_runtime.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
import config
|
||||||
|
import sheets
|
||||||
|
from member_sync import announce_birthday, is_birthday_today, sync_member
|
||||||
|
|
||||||
|
|
||||||
|
async def run_birthday_daily(
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
has_announced_today: Callable[[int], bool],
|
||||||
|
mark_announced_today: Callable[[int], None],
|
||||||
|
) -> None:
|
||||||
|
"""Announce birthdays in the configured guild for users whose birthday is today."""
|
||||||
|
guild = bot.get_guild(config.GUILD_ID)
|
||||||
|
if guild is None:
|
||||||
|
log.warning("Birthday task: guild %s not found", config.GUILD_ID)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = sheets.refresh()
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Birthday task: sheet refresh failed: %s", e)
|
||||||
|
data = sheets.get_cache()
|
||||||
|
|
||||||
|
announced = 0
|
||||||
|
for row in data:
|
||||||
|
bday_str = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
if not is_birthday_today(bday_str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
member = None
|
||||||
|
raw_id = str(row.get("User ID", "")).strip()
|
||||||
|
if raw_id:
|
||||||
|
try:
|
||||||
|
member = guild.get_member(int(raw_id))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if member is None:
|
||||||
|
discord_name = str(row.get("Discord", "")).strip()
|
||||||
|
if discord_name:
|
||||||
|
member = discord.utils.find(
|
||||||
|
lambda m, n=discord_name: m.name.lower() == n.lower(),
|
||||||
|
guild.members,
|
||||||
|
)
|
||||||
|
if member and not has_announced_today(member.id):
|
||||||
|
await announce_birthday(member, bot)
|
||||||
|
mark_announced_today(member.id)
|
||||||
|
announced += 1
|
||||||
|
|
||||||
|
log.info("Sünnipäeva ülesanne: teavitati %d liiget", announced)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_member_join(
|
||||||
|
member: discord.Member,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
has_announced_today: Callable[[int], bool],
|
||||||
|
mark_announced_today: Callable[[int], None],
|
||||||
|
log_sync_result: Callable[[discord.Member, object], None],
|
||||||
|
) -> None:
|
||||||
|
"""Sync a newly joined member against sheet data and trigger birthday notice if needed."""
|
||||||
|
log.info("Member joined: %s (ID: %s)", member, member.id)
|
||||||
|
|
||||||
|
if not sheets.get_cache():
|
||||||
|
sheets.refresh()
|
||||||
|
|
||||||
|
result = await sync_member(member, member.guild)
|
||||||
|
|
||||||
|
if result.not_found:
|
||||||
|
try:
|
||||||
|
sheets.add_new_member_row(member.name, member.id)
|
||||||
|
log.info(
|
||||||
|
" → %s not in sheet, added new row (Discord=%s, ID=%s)",
|
||||||
|
member,
|
||||||
|
member.name,
|
||||||
|
member.id,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(" → Failed to add sheet row for %s: %s", member, e)
|
||||||
|
return
|
||||||
|
|
||||||
|
log_sync_result(member, result)
|
||||||
|
sheets.set_synced(member.id, result.synced)
|
||||||
|
|
||||||
|
if result.birthday_soon and not has_announced_today(member.id):
|
||||||
|
await announce_birthday(member, bot)
|
||||||
|
mark_announced_today(member.id)
|
||||||
@@ -13,9 +13,14 @@ Create your admin account on first launch via the Admin UI.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Create the `economy_users` collection
|
## 2. Create economy collections for both bot profiles
|
||||||
|
|
||||||
In the Admin UI → **Collections** → **New collection** → name it exactly `economy_users`.
|
In the Admin UI → **Collections** → **New collection** and create:
|
||||||
|
|
||||||
|
- `economy_users_dev`
|
||||||
|
- `economy_users_prod`
|
||||||
|
|
||||||
|
Use the same schema for both collections.
|
||||||
|
|
||||||
Add the following fields:
|
Add the following fields:
|
||||||
|
|
||||||
@@ -51,6 +56,8 @@ Add to your `.env`:
|
|||||||
PB_URL=http://127.0.0.1:8090
|
PB_URL=http://127.0.0.1:8090
|
||||||
PB_ADMIN_EMAIL=your-admin@email.com
|
PB_ADMIN_EMAIL=your-admin@email.com
|
||||||
PB_ADMIN_PASSWORD=your-admin-password
|
PB_ADMIN_PASSWORD=your-admin-password
|
||||||
|
PB_ECONOMY_COLLECTION_DEV=economy_users_dev
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY=economy_users_prod
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
310
economy_admin_commands.py
Normal file
310
economy_admin_commands.py
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
async def _dm_user(bot: discord.Client, user_id: int, msg: str) -> None:
|
||||||
|
"""Best-effort DM to a user."""
|
||||||
|
try:
|
||||||
|
user = bot.get_user(user_id) or await bot.fetch_user(user_id)
|
||||||
|
await user.send(msg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_admin_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
cd_ts: Callable[[datetime.timedelta], str],
|
||||||
|
apply_level_role: Callable[[discord.Member, int, int], Awaitable[None]],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="adminseason", description=S.CMD["adminseason"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
@app_commands.describe(top_n=S.OPT["adminseason_top_n"])
|
||||||
|
async def cmd_adminseason(interaction: discord.Interaction, top_n: int = 10):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
top = await economy.do_season_reset(top_n)
|
||||||
|
guild = interaction.guild
|
||||||
|
|
||||||
|
if guild:
|
||||||
|
all_role_names = {name for _, name in economy.LEVEL_ROLES}
|
||||||
|
for role_name in all_role_names:
|
||||||
|
role = discord.utils.find(lambda r: r.name == role_name, guild.roles)
|
||||||
|
if not role:
|
||||||
|
continue
|
||||||
|
for member in list(role.members):
|
||||||
|
try:
|
||||||
|
await member.remove_roles(role, reason="Season reset")
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
|
medals = ["🥇", "🥈", "🥉"]
|
||||||
|
lines = []
|
||||||
|
for i, (uid, exp, lvl) in enumerate(top):
|
||||||
|
member = guild.get_member(int(uid)) if guild else None
|
||||||
|
name = member.display_name if member else S.LEADERBOARD_UI["unknown_user"].format(uid=uid)
|
||||||
|
prefix = medals[i] if i < 3 else f"**{i + 1}.**"
|
||||||
|
lines.append(S.SEASON["entry"].format(prefix=prefix, name=name, exp=exp, level=lvl))
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["adminseason"],
|
||||||
|
description=(S.SEASON["top_header"] + "\n" + "\n".join(lines)) if lines else S.SEASON["no_players"],
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
embed.set_footer(text=S.SEASON["footer"])
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=False)
|
||||||
|
await interaction.followup.send(S.SEASON["done"], ephemeral=True)
|
||||||
|
log.info("/adminseason triggered by %s (top_n=%d)", interaction.user, top_n)
|
||||||
|
|
||||||
|
@tree.command(name="admincoins", description=S.CMD["admincoins"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
kogus=S.OPT["admincoins_kogus"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_admincoins(interaction: discord.Interaction, kasutaja: discord.Member, kogus: int, põhjus: str):
|
||||||
|
if kogus == 0:
|
||||||
|
await interaction.response.send_message(S.ERR["admincoins_zero"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_admin_coins(kasutaja.id, kogus, interaction.user.id, põhjus)
|
||||||
|
verb = f"+{kogus}" if kogus > 0 else str(kogus)
|
||||||
|
emoji = "💰" if kogus > 0 else "💸"
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["coins_done"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=kasutaja.display_name,
|
||||||
|
verb=verb,
|
||||||
|
coin=economy.COIN,
|
||||||
|
balance=f"{res['balance']:,}",
|
||||||
|
reason=põhjus,
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(
|
||||||
|
bot,
|
||||||
|
kasutaja.id,
|
||||||
|
S.ADMIN["coins_dm"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
verb=verb,
|
||||||
|
coin=economy.COIN,
|
||||||
|
reason=põhjus,
|
||||||
|
balance=f"{res['balance']:,}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
log.info("ADMINCOINS %s → %s (%s) by %s", verb, kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminjail", description=S.CMD["adminjail"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
minutid=S.OPT["adminjail_minutid"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminjail(interaction: discord.Interaction, kasutaja: discord.Member, minutid: int, põhjus: str):
|
||||||
|
if minutid <= 0:
|
||||||
|
await interaction.response.send_message(S.ERR["positive_duration"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await economy.do_admin_jail(kasutaja.id, minutid, interaction.user.id, põhjus)
|
||||||
|
until_ts = cd_ts(datetime.timedelta(minutes=minutid))
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["jail_done"].format(name=kasutaja.display_name, minutes=minutid, ts=until_ts, reason=põhjus),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(
|
||||||
|
bot,
|
||||||
|
kasutaja.id,
|
||||||
|
S.ADMIN["jail_dm"].format(minutes=minutid, reason=põhjus, ts=until_ts),
|
||||||
|
)
|
||||||
|
log.info("ADMINJAIL %s %dmin (%s) by %s", kasutaja, minutid, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminunjail", description=S.CMD["adminunjail"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminunjail(interaction: discord.Interaction, kasutaja: discord.Member):
|
||||||
|
await economy.do_admin_unjail(kasutaja.id, interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["unjail_done"].format(name=kasutaja.display_name), ephemeral=True
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["unjail_dm"])
|
||||||
|
log.info("ADMINUNJAIL %s by %s", kasutaja, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminban", description=S.CMD["adminban"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminban(interaction: discord.Interaction, kasutaja: discord.Member, põhjus: str):
|
||||||
|
if bot.user and kasutaja.id == bot.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["admin_ban_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await economy.do_admin_ban(kasutaja.id, interaction.user.id, põhjus)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["ban_done"].format(name=kasutaja.display_name, reason=põhjus),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["ban_dm"].format(reason=põhjus))
|
||||||
|
log.info("ADMINBAN %s (%s) by %s", kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminunban", description=S.CMD["adminunban"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminunban(interaction: discord.Interaction, kasutaja: discord.Member):
|
||||||
|
await economy.do_admin_unban(kasutaja.id, interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["unban_done"].format(name=kasutaja.display_name), ephemeral=True
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["unban_dm"])
|
||||||
|
log.info("ADMINUNBAN %s by %s", kasutaja, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminreset", description=S.CMD["adminreset"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminreset(interaction: discord.Interaction, kasutaja: discord.Member, põhjus: str):
|
||||||
|
if bot.user and kasutaja.id == bot.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["admin_reset_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await economy.do_admin_reset(kasutaja.id, interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["reset_done"].format(name=kasutaja.display_name, reason=põhjus),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["reset_dm"].format(reason=põhjus))
|
||||||
|
log.info("ADMINRESET %s (%s) by %s", kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminview", description=S.CMD["adminview"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminview(interaction: discord.Interaction, kasutaja: discord.Member):
|
||||||
|
res = await economy.do_admin_inspect(kasutaja.id)
|
||||||
|
data = res["data"]
|
||||||
|
items_str = ", ".join(data.get("items", [])) or "-"
|
||||||
|
uses = data.get("item_uses", {})
|
||||||
|
uses_str = ", ".join(f"{k}:{v}" for k, v in uses.items()) if uses else "-"
|
||||||
|
jailed = data.get("jailed_until") or "-"
|
||||||
|
banned = S.ADMINVIEW_UI["banned_yes"] if data.get("eco_banned") else S.ADMINVIEW_UI["banned_no"]
|
||||||
|
exp = data.get("exp", 0)
|
||||||
|
level = economy.get_level(exp)
|
||||||
|
prestige_lvl = data.get("prestige_level", 0)
|
||||||
|
prestige_pp = data.get("prestige_points", 0)
|
||||||
|
total_fish = data.get("total_fish_caught", 0)
|
||||||
|
inv_fish = len(data.get("fish_inventory") or [])
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.ADMINVIEW_UI["title"].format(name=kasutaja.display_name),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_balance"], value=f"{data.get('balance', 0):,} {economy.COIN}", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_exp"], value=S.ADMINVIEW_UI["exp_val"].format(exp=f"{exp:,}", level=level), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_streak"], value=str(data.get("daily_streak", 0)), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_banned"], value=banned, inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_jailed"], value=jailed, inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_prestige"], value=S.ADMINVIEW_UI["prestige_val"].format(level=prestige_lvl, pp=prestige_pp), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_fish"], value=S.ADMINVIEW_UI["fish_val"].format(caught=total_fish, inv=inv_fish), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_items"], value=items_str, inline=False)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_uses"], value=uses_str, inline=False)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_daily"], value=data.get("last_daily") or "-", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_work"], value=data.get("last_work") or "-", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_crime"], value=data.get("last_crime") or "-", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_fish"], value=data.get("last_fish") or "-", inline=True)
|
||||||
|
embed.set_footer(text=S.ADMINVIEW_UI["footer"].format(uid=kasutaja.id))
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
log.info("ADMINVIEW %s by %s", kasutaja, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminexp", description=S.CMD["adminexp"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
kogus=S.OPT["adminexp_kogus"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminexp(interaction: discord.Interaction, kasutaja: discord.Member, kogus: int, põhjus: str):
|
||||||
|
if kogus == 0:
|
||||||
|
await interaction.response.send_message(S.ERR["admincoins_zero"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_admin_exp(kasutaja.id, kogus, interaction.user.id, põhjus)
|
||||||
|
verb = f"+{kogus}" if kogus > 0 else str(kogus)
|
||||||
|
emoji = "📈" if kogus > 0 else "📉"
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["exp_done"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=kasutaja.display_name,
|
||||||
|
verb=verb,
|
||||||
|
exp=f"{res['exp']:,}",
|
||||||
|
level=res["new_level"],
|
||||||
|
reason=põhjus,
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(
|
||||||
|
bot,
|
||||||
|
kasutaja.id,
|
||||||
|
S.ADMIN["exp_dm"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
verb=verb,
|
||||||
|
reason=põhjus,
|
||||||
|
exp=f"{res['exp']:,}",
|
||||||
|
level=res["new_level"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if res["level_changed"]:
|
||||||
|
member = interaction.guild.get_member(kasutaja.id) if interaction.guild else None
|
||||||
|
if member:
|
||||||
|
await apply_level_role(member, res["new_level"], res["old_level"])
|
||||||
|
log.info("ADMINEXP %s → %s (%s) by %s", verb, kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminitem", description=S.CMD["adminitem"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
ese=S.OPT["adminitem_ese"],
|
||||||
|
tegevus=S.OPT["adminitem_tegevus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminitem(interaction: discord.Interaction, kasutaja: discord.Member, ese: str, tegevus: str):
|
||||||
|
action = tegevus.strip().lower()
|
||||||
|
if action not in ("anna", "eemalda"):
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_invalid"].format(item_id=tegevus), ephemeral=True)
|
||||||
|
return
|
||||||
|
action_key = "give" if action == "anna" else "remove"
|
||||||
|
res = await economy.do_admin_item(kasutaja.id, ese, action_key, interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "invalid_item":
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_invalid"].format(item_id=ese), ephemeral=True)
|
||||||
|
elif res["reason"] == "not_owned":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["item_not_owned"].format(name=kasutaja.display_name, item_id=ese),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(S.ERR["generic_error"].format(error=res["reason"]), ephemeral=True)
|
||||||
|
return
|
||||||
|
item_name = economy.SHOP[ese]["name"]
|
||||||
|
if action_key == "give":
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_given"].format(item=item_name, name=kasutaja.display_name), ephemeral=True)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["item_dm_given"].format(item=item_name))
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_removed"].format(item=item_name, name=kasutaja.display_name), ephemeral=True)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["item_dm_removed"].format(item=item_name))
|
||||||
|
log.info("ADMINITEM %s %s %s by %s", action_key, ese, kasutaja, interaction.user)
|
||||||
883
economy_extra_commands.py
Normal file
883
economy_extra_commands.py
Normal file
@@ -0,0 +1,883 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
from collections.abc import Awaitable, Callable, MutableSet
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_extra_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
coin: Callable[[int], str],
|
||||||
|
cd_ts: Callable[[datetime.timedelta], str],
|
||||||
|
parse_amount: Callable[[str, int], tuple[int | None, str | None]],
|
||||||
|
ensure_level_role: Callable[[discord.Member, int], Awaitable[None]],
|
||||||
|
active_games: MutableSet[int],
|
||||||
|
) -> None:
|
||||||
|
active_heist = None
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# /heist - multiplayer group robbery of the house
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
_HEIST_JOIN_WINDOW = 300 # seconds players have to join
|
||||||
|
_HEIST_MIN_PLAYERS = 2
|
||||||
|
_HEIST_GLOBAL_CD = 14400 # seconds between heist events server-wide (4h)
|
||||||
|
_HEIST_MAX_PLAYERS = 8
|
||||||
|
_HEIST_BASE_CHANCE = 0.35 # 35% solo
|
||||||
|
_HEIST_CHANCE_STEP = 0.05 # +5% per extra player
|
||||||
|
_HEIST_MAX_CHANCE = 0.65 # cap at 65%
|
||||||
|
|
||||||
|
def _build_heist_story(participants: list[discord.Member], success: bool) -> list[str]:
|
||||||
|
"""Return a list of story lines for the heist narrative reveal."""
|
||||||
|
story = S.HEIST_STORY
|
||||||
|
leader = participants[0].display_name
|
||||||
|
if len(participants) == 1:
|
||||||
|
names = f"**{leader}**"
|
||||||
|
elif len(participants) == 2:
|
||||||
|
names = S.HEIST_UI["names_duo"].format(
|
||||||
|
a=participants[0].display_name,
|
||||||
|
b=participants[1].display_name,
|
||||||
|
)
|
||||||
|
elif len(participants) <= 4:
|
||||||
|
names = S.HEIST_UI["names_sep"].join(f"**{p.display_name}**" for p in participants)
|
||||||
|
else:
|
||||||
|
names = S.HEIST_UI["names_crew"].format(leader=participants[0].display_name)
|
||||||
|
|
||||||
|
vehicle = random.choice(story["vehicles"])
|
||||||
|
approach = random.choice(["sneaky", "loud"])
|
||||||
|
non_leaders = participants[1:] if len(participants) > 1 else participants
|
||||||
|
|
||||||
|
def fill(tmpl: str) -> str:
|
||||||
|
picked = random.choice(non_leaders).display_name
|
||||||
|
return tmpl.format(
|
||||||
|
leader=f"**{leader}**",
|
||||||
|
member=f"**{picked}**",
|
||||||
|
names=names,
|
||||||
|
vehicle=vehicle,
|
||||||
|
)
|
||||||
|
|
||||||
|
getaway_pool = "getaway_success" if success else "getaway_fail"
|
||||||
|
|
||||||
|
return [
|
||||||
|
fill(random.choice(story["arrival"])),
|
||||||
|
fill(random.choice(story[f"entry_{approach}"])),
|
||||||
|
fill(random.choice(story["inside"])),
|
||||||
|
fill(random.choice(story["vault"])),
|
||||||
|
fill(random.choice(story["vault_open"])),
|
||||||
|
fill(random.choice(story["police_inbound"])),
|
||||||
|
fill(random.choice(story[getaway_pool])),
|
||||||
|
fill(random.choice(story["escape_success" if success else "escape_fail"])),
|
||||||
|
]
|
||||||
|
|
||||||
|
class HeistLobbyView(discord.ui.View):
|
||||||
|
def __init__(self, organizer: discord.Member, organizer_has_jellyfin: bool = False):
|
||||||
|
super().__init__(timeout=_HEIST_JOIN_WINDOW)
|
||||||
|
self.organizer = organizer
|
||||||
|
self.participants: list[discord.Member] = [organizer]
|
||||||
|
self.message: discord.Message | None = None
|
||||||
|
self.resolved = False
|
||||||
|
self.jellyfin_holders: int = 1 if organizer_has_jellyfin else 0
|
||||||
|
|
||||||
|
def _chance(self) -> float:
|
||||||
|
n = len(self.participants)
|
||||||
|
base = min(_HEIST_BASE_CHANCE + _HEIST_CHANCE_STEP * (n - 1), _HEIST_MAX_CHANCE)
|
||||||
|
jelly_bonus = 0.05 if self.jellyfin_holders > 0 else 0.0
|
||||||
|
return min(base + jelly_bonus, _HEIST_MAX_CHANCE)
|
||||||
|
|
||||||
|
def _lobby_embed(self) -> discord.Embed:
|
||||||
|
names = "\n".join(f"• {p.display_name}" for p in self.participants)
|
||||||
|
desc = S.HEIST_UI["lobby_desc"].format(
|
||||||
|
n=len(self.participants),
|
||||||
|
max=_HEIST_MAX_PLAYERS,
|
||||||
|
names=names,
|
||||||
|
chance=int(self._chance() * 100),
|
||||||
|
ts=int(self._timeout_expiry()),
|
||||||
|
)
|
||||||
|
return discord.Embed(title=S.TITLE["heist_lobby"], description=desc, color=0xE67E22)
|
||||||
|
|
||||||
|
def _timeout_expiry(self) -> float:
|
||||||
|
return time.time() + (self.timeout or 0)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.HEIST_UI["btn_join"], style=discord.ButtonStyle.danger)
|
||||||
|
async def join(self, interaction: discord.Interaction, _: discord.ui.Button):
|
||||||
|
if any(p.id == interaction.user.id for p in self.participants):
|
||||||
|
await interaction.response.send_message(S.HEIST_UI["already_joined"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if len(self.participants) >= _HEIST_MAX_PLAYERS:
|
||||||
|
await interaction.response.send_message(S.ERR["heist_full"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if interaction.user.id in active_games:
|
||||||
|
await interaction.response.send_message(S.ERR["already_in_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_heist_check(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])), ephemeral=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["heist"].format(ts=cd_ts(res["remaining"])), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.participants.append(interaction.user)
|
||||||
|
active_games.add(interaction.user.id)
|
||||||
|
joiner_data = await economy.get_user(interaction.user.id)
|
||||||
|
if "jellyfin" in joiner_data.get("items", []):
|
||||||
|
self.jellyfin_holders += 1
|
||||||
|
await interaction.response.edit_message(embed=self._lobby_embed())
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.HEIST_UI["btn_start"], style=discord.ButtonStyle.success)
|
||||||
|
async def start_now(self, interaction: discord.Interaction, _: discord.ui.Button):
|
||||||
|
if interaction.user.id != self.organizer.id:
|
||||||
|
await interaction.response.send_message(S.HEIST_UI["only_organizer"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if len(self.participants) < _HEIST_MIN_PLAYERS:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["heist_min_players"].format(min=_HEIST_MIN_PLAYERS), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await self._resolve(interaction)
|
||||||
|
|
||||||
|
async def _resolve(self, interaction: discord.Interaction | None = None) -> None:
|
||||||
|
nonlocal active_heist
|
||||||
|
if self.resolved:
|
||||||
|
return
|
||||||
|
self.resolved = True
|
||||||
|
active_heist = None
|
||||||
|
self.stop()
|
||||||
|
self.clear_items()
|
||||||
|
|
||||||
|
for p in self.participants:
|
||||||
|
active_games.discard(p.id)
|
||||||
|
|
||||||
|
n = len(self.participants)
|
||||||
|
channel = interaction.channel if interaction else self.message.channel if self.message else None
|
||||||
|
|
||||||
|
if n < _HEIST_MIN_PLAYERS:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["heist_cancel"],
|
||||||
|
description=S.HEIST_UI["cancel_desc"].format(min=_HEIST_MIN_PLAYERS),
|
||||||
|
color=0x99AAB5,
|
||||||
|
)
|
||||||
|
if interaction and not interaction.response.is_done():
|
||||||
|
await interaction.response.edit_message(embed=embed, view=self)
|
||||||
|
elif self.message:
|
||||||
|
try:
|
||||||
|
await self.message.edit(embed=embed, view=self)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
|
success = random.random() < self._chance()
|
||||||
|
story_lines = _build_heist_story(self.participants, success)
|
||||||
|
|
||||||
|
lobby_done = discord.Embed(
|
||||||
|
title=S.HEIST_UI["started_title"],
|
||||||
|
description=S.HEIST_UI["started_desc"].format(n=n),
|
||||||
|
color=0x99AAB5,
|
||||||
|
)
|
||||||
|
if interaction and not interaction.response.is_done():
|
||||||
|
await interaction.response.edit_message(embed=lobby_done, view=self)
|
||||||
|
elif self.message:
|
||||||
|
try:
|
||||||
|
await self.message.edit(embed=lobby_done, view=self)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
story_embed = discord.Embed(title=S.HEIST_UI["story_title"], description="", color=0xE67E22)
|
||||||
|
story_msg = await channel.send(embed=story_embed)
|
||||||
|
accumulated = ""
|
||||||
|
for i, line in enumerate(story_lines):
|
||||||
|
await asyncio.sleep(random.uniform(3.0, 4.5))
|
||||||
|
accumulated += ("\n\n" if i > 0 else "") + line
|
||||||
|
story_embed.description = accumulated
|
||||||
|
try:
|
||||||
|
await story_msg.edit(embed=story_embed)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(2.0)
|
||||||
|
|
||||||
|
res = await economy.do_heist_resolve([p.id for p in self.participants], success)
|
||||||
|
payout_each = res["payout_each"]
|
||||||
|
names_str = "\n".join(f"• {p.display_name}" for p in self.participants)
|
||||||
|
guild = interaction.guild if interaction else self.message.guild if self.message else None
|
||||||
|
|
||||||
|
if success:
|
||||||
|
result_desc = S.HEIST_UI["win_desc"].format(names=names_str, payout=coin(payout_each))
|
||||||
|
result_embed = discord.Embed(
|
||||||
|
title=S.TITLE["heist_win"],
|
||||||
|
description=result_desc,
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
for p in self.participants:
|
||||||
|
exp_res = await economy.award_exp(p.id, economy.EXP_REWARDS["heist_win"])
|
||||||
|
if exp_res["old_level"] != exp_res["new_level"] and guild:
|
||||||
|
gm = guild.get_member(p.id)
|
||||||
|
if gm:
|
||||||
|
asyncio.create_task(ensure_level_role(gm, exp_res["new_level"]))
|
||||||
|
else:
|
||||||
|
result_desc = S.HEIST_UI["fail_desc"].format(names=names_str)
|
||||||
|
result_embed = discord.Embed(
|
||||||
|
title=S.TITLE["heist_fail"],
|
||||||
|
description=result_desc,
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
|
||||||
|
await economy.set_heist_global_cd(time.time() + _HEIST_GLOBAL_CD)
|
||||||
|
|
||||||
|
if channel:
|
||||||
|
await channel.send(embed=result_embed)
|
||||||
|
elif self.message:
|
||||||
|
try:
|
||||||
|
await self.message.channel.send(embed=result_embed)
|
||||||
|
except discord.HTTPException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def on_timeout(self) -> None:
|
||||||
|
await self._resolve()
|
||||||
|
|
||||||
|
@tree.command(name="heist", description=S.CMD["heist"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_heist(interaction: discord.Interaction):
|
||||||
|
nonlocal active_heist
|
||||||
|
if active_heist is not None:
|
||||||
|
await interaction.response.send_message(S.ERR["heist_active"], ephemeral=True)
|
||||||
|
return
|
||||||
|
heist_cd = await economy.get_heist_global_cd()
|
||||||
|
if time.time() < heist_cd:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["heist_global"].format(
|
||||||
|
ts=cd_ts(datetime.timedelta(seconds=heist_cd - time.time()))
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if interaction.user.id in active_games:
|
||||||
|
await interaction.response.send_message(S.ERR["already_in_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_heist_check(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
organizer_data = await economy.get_user(interaction.user.id)
|
||||||
|
view = HeistLobbyView(interaction.user, "jellyfin" in organizer_data.get("items", []))
|
||||||
|
active_heist = view
|
||||||
|
active_games.add(interaction.user.id)
|
||||||
|
await interaction.response.send_message(embed=view._lobby_embed(), view=view)
|
||||||
|
view.message = await interaction.original_response()
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
# /jailbreak - Monopoly-style dice escape
|
||||||
|
# -----------------------------------------------------------------------
|
||||||
|
_DICE_EMOJI = [
|
||||||
|
"<:TipiYKS:1483103190491856916>",
|
||||||
|
"<:TipiKAKS:1483103215841972404>",
|
||||||
|
"<:TipiKOLM:1483103217846980781>",
|
||||||
|
"<:TipiNELI:1483103237585240114>",
|
||||||
|
"<:TipiVIIS:1483103239036469289>",
|
||||||
|
"<:TipiKUUS:1483103253163020348>",
|
||||||
|
]
|
||||||
|
|
||||||
|
class JailbreakView(discord.ui.View):
|
||||||
|
MAX_TRIES = 3
|
||||||
|
|
||||||
|
def __init__(self, user_id: int):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self.user_id = user_id
|
||||||
|
self.tries = 0
|
||||||
|
self._rolling = False
|
||||||
|
self._add_roll_btn()
|
||||||
|
|
||||||
|
def _add_roll_btn(self):
|
||||||
|
self.clear_items()
|
||||||
|
btn = discord.ui.Button(
|
||||||
|
label=S.JAILBREAK_UI["btn_roll"].format(try_=self.tries + 1, max=self.MAX_TRIES),
|
||||||
|
style=discord.ButtonStyle.primary,
|
||||||
|
)
|
||||||
|
btn.callback = self._on_roll
|
||||||
|
self.add_item(btn)
|
||||||
|
|
||||||
|
async def _on_roll(self, interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if self._rolling:
|
||||||
|
await interaction.response.defer()
|
||||||
|
return
|
||||||
|
self._rolling = True
|
||||||
|
|
||||||
|
self.clear_items()
|
||||||
|
rolling_embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak"],
|
||||||
|
description=S.JAILBREAK_UI["rolling_desc"],
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
await interaction.response.edit_message(embed=rolling_embed, view=self)
|
||||||
|
|
||||||
|
d1 = random.randint(1, 6)
|
||||||
|
d2 = random.randint(1, 6)
|
||||||
|
e1, e2 = _DICE_EMOJI[d1 - 1], _DICE_EMOJI[d2 - 1]
|
||||||
|
double = d1 == d2
|
||||||
|
self.tries += 1
|
||||||
|
tries_left = self.MAX_TRIES - self.tries
|
||||||
|
|
||||||
|
await asyncio.sleep(1.5)
|
||||||
|
self._rolling = False
|
||||||
|
|
||||||
|
if double:
|
||||||
|
await economy.do_jail_free(self.user_id)
|
||||||
|
self.stop()
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_free"],
|
||||||
|
description=S.JAILBREAK_UI["free_desc"].format(d1=e1, d2=e2),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
elif tries_left == 0:
|
||||||
|
self.stop()
|
||||||
|
user_data = await economy.get_user(self.user_id)
|
||||||
|
bal = user_data["balance"]
|
||||||
|
if bal >= economy.MIN_BAIL:
|
||||||
|
min_fine = max(economy.MIN_BAIL, int(bal * 0.20))
|
||||||
|
max_fine = max(economy.MIN_BAIL, int(bal * 0.30))
|
||||||
|
desc = S.JAILBREAK_UI["fail_bail_offer"].format(
|
||||||
|
d1=e1,
|
||||||
|
d2=e2,
|
||||||
|
min=coin(min_fine),
|
||||||
|
max=coin(max_fine),
|
||||||
|
bal=coin(bal),
|
||||||
|
)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_fail"],
|
||||||
|
description=desc,
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=BailView(self.user_id))
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_fail"],
|
||||||
|
description=S.JAILBREAK_UI["fail_broke_desc"].format(
|
||||||
|
d1=e1,
|
||||||
|
d2=e2,
|
||||||
|
balance=coin(bal),
|
||||||
|
),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=None)
|
||||||
|
else:
|
||||||
|
self._add_roll_btn()
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_miss"].format(tries=self.tries, max=self.MAX_TRIES),
|
||||||
|
description=S.JAILBREAK_UI["miss_desc"].format(d1=e1, d2=e2, left=tries_left),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
|
||||||
|
class BailView(discord.ui.View):
|
||||||
|
def __init__(self, user_id: int):
|
||||||
|
super().__init__(timeout=60)
|
||||||
|
self.user_id = user_id
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.JAILBREAK_UI["bail_btn"], style=discord.ButtonStyle.danger)
|
||||||
|
async def pay_bail(self, interaction: discord.Interaction, _: discord.ui.Button):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_bail(self.user_id)
|
||||||
|
self.clear_items()
|
||||||
|
self.stop()
|
||||||
|
if not res["ok"] and res.get("reason") == "broke":
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_bail"],
|
||||||
|
description=S.JAILBREAK_UI["bail_broke_desc"].format(
|
||||||
|
min=coin(economy.MIN_BAIL),
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_bail"],
|
||||||
|
description=S.JAILBREAK_UI["bail_paid_desc"].format(
|
||||||
|
fine=coin(res["fine"]),
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await interaction.response.edit_message(embed=embed, view=self)
|
||||||
|
|
||||||
|
@tree.command(name="jailbreak", description=S.CMD["jailbreak"])
|
||||||
|
async def cmd_jailbreak(interaction: discord.Interaction):
|
||||||
|
user_data = await economy.get_user(interaction.user.id)
|
||||||
|
remaining = economy._is_jailed(user_data)
|
||||||
|
if not remaining:
|
||||||
|
await interaction.response.send_message(S.ERR["not_jailed"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if user_data.get("jailbreak_used", False):
|
||||||
|
bal = user_data["balance"]
|
||||||
|
min_fine = max(economy.MIN_BAIL, int(bal * 0.20))
|
||||||
|
max_fine = max(economy.MIN_BAIL, int(bal * 0.30))
|
||||||
|
if bal >= economy.MIN_BAIL:
|
||||||
|
desc = S.JAILBREAK_UI["already_bail"].format(
|
||||||
|
min=coin(min_fine),
|
||||||
|
max=coin(max_fine),
|
||||||
|
bal=coin(bal),
|
||||||
|
ts=cd_ts(remaining),
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_bail"],
|
||||||
|
description=desc,
|
||||||
|
color=0xED4245,
|
||||||
|
),
|
||||||
|
view=BailView(interaction.user.id),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
desc = S.JAILBREAK_UI["already_broke"].format(
|
||||||
|
min=coin(economy.MIN_BAIL),
|
||||||
|
bal=coin(bal),
|
||||||
|
ts=cd_ts(remaining),
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak_bail"],
|
||||||
|
description=desc,
|
||||||
|
color=0xED4245,
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
await economy.set_jailbreak_used(interaction.user.id)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["jailbreak"],
|
||||||
|
description=S.JAILBREAK_UI["intro_desc"].format(
|
||||||
|
ts=cd_ts(remaining),
|
||||||
|
tries=JailbreakView.MAX_TRIES,
|
||||||
|
),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=embed,
|
||||||
|
view=JailbreakView(interaction.user.id),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="give", description=S.CMD["give"])
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["give_kasutaja"], summa=S.OPT["give_summa"])
|
||||||
|
async def cmd_give(interaction: discord.Interaction, kasutaja: discord.Member, summa: str):
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
summa_int, err = parse_amount(summa, data["balance"])
|
||||||
|
if err:
|
||||||
|
await interaction.response.send_message(err, ephemeral=True)
|
||||||
|
return
|
||||||
|
if summa_int is None or summa_int <= 0:
|
||||||
|
await interaction.response.send_message(S.ERR["positive_amount"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if kasutaja.id == interaction.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["give_self"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if kasutaja.bot:
|
||||||
|
await interaction.response.send_message(S.ERR["give_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await economy.do_give(interaction.user.id, kasutaja.id, summa_int)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["give_jailed"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["broke"].format(bal=coin(data["balance"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{economy.COIN} {S.TITLE['give']}",
|
||||||
|
description=S.GIVE_UI["desc"].format(
|
||||||
|
giver=interaction.user.display_name,
|
||||||
|
amount=coin(summa_int),
|
||||||
|
receiver=kasutaja.display_name,
|
||||||
|
),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
|
||||||
|
class LeaderboardView(discord.ui.View):
|
||||||
|
PER_PAGE = 10
|
||||||
|
|
||||||
|
def __init__(self, data: dict, guild: discord.Guild | None, bot_user: discord.ClientUser | None):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self.data = data
|
||||||
|
self.guild = guild
|
||||||
|
self.bot_user = bot_user
|
||||||
|
self.page = 0
|
||||||
|
self.mode = "coins"
|
||||||
|
self.max_page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
|
||||||
|
def _current_list(self) -> list:
|
||||||
|
return self.data.get(self.mode, [])
|
||||||
|
|
||||||
|
def _update_buttons(self):
|
||||||
|
current = self._current_list()
|
||||||
|
self.max_page = max(0, (len(current) - 1) // self.PER_PAGE) if current else 0
|
||||||
|
self.prev_btn.disabled = self.page == 0
|
||||||
|
self.next_btn.disabled = self.page >= self.max_page
|
||||||
|
for m, btn in [
|
||||||
|
("coins", self.coins_btn),
|
||||||
|
("exp", self.exp_btn),
|
||||||
|
("season", self.season_btn),
|
||||||
|
("prestige", self.prestige_btn),
|
||||||
|
("wagered", self.wagered_btn),
|
||||||
|
("fish", self.fish_btn),
|
||||||
|
]:
|
||||||
|
btn.style = discord.ButtonStyle.primary if m == self.mode else discord.ButtonStyle.secondary
|
||||||
|
|
||||||
|
def _name(self, uid: str, highlight_uid: int | None = None) -> str:
|
||||||
|
if self.guild:
|
||||||
|
member = self.guild.get_member(int(uid))
|
||||||
|
name = member.display_name if member else S.LEADERBOARD_UI["unknown_user"].format(uid=uid)
|
||||||
|
else:
|
||||||
|
name = S.LEADERBOARD_UI["unknown_user"].format(uid=uid)
|
||||||
|
if highlight_uid and int(uid) == highlight_uid:
|
||||||
|
name = f"**› {name} ‹**"
|
||||||
|
return name
|
||||||
|
|
||||||
|
def _make_embed(self, highlight_uid: int | None = None) -> discord.Embed:
|
||||||
|
title_map = {
|
||||||
|
"coins": f"{economy.COIN} {S.TITLE['leaderboard_coins']}",
|
||||||
|
"exp": S.TITLE["leaderboard_exp"],
|
||||||
|
"season": S.TITLE["leaderboard_season"],
|
||||||
|
"prestige": S.TITLE["leaderboard_prestige"],
|
||||||
|
"wagered": S.TITLE["leaderboard_wagered"],
|
||||||
|
"fish": S.TITLE["leaderboard_fish"],
|
||||||
|
}
|
||||||
|
color_map = {"coins": 0xF4C430, "wagered": 0xED4245, "fish": 0x57F287}
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=title_map.get(self.mode, "Edetabel"),
|
||||||
|
color=color_map.get(self.mode, 0x5865F2),
|
||||||
|
)
|
||||||
|
lines = []
|
||||||
|
|
||||||
|
if self.mode == "coins" and self.page == 0 and self.data.get("house_entry"):
|
||||||
|
_, bal = self.data["house_entry"]
|
||||||
|
house_name = self.bot_user.display_name if self.bot_user else S.LEADERBOARD_UI["house_default_name"]
|
||||||
|
lines.append(S.LEADERBOARD_UI["house_entry"].format(name=house_name, balance=coin(bal)))
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
start = self.page * self.PER_PAGE
|
||||||
|
medals = ["🥇", "🥈", "🥉"]
|
||||||
|
current = self._current_list()
|
||||||
|
slice_ = current[start : start + self.PER_PAGE]
|
||||||
|
|
||||||
|
if not slice_:
|
||||||
|
lines.append(S.LEADERBOARD_UI["no_entries"])
|
||||||
|
else:
|
||||||
|
for i, entry in enumerate(slice_):
|
||||||
|
rank = start + i
|
||||||
|
uid = entry[0]
|
||||||
|
prefix = medals[rank] if rank < 3 else f"**{rank + 1}.**"
|
||||||
|
name = self._name(uid, highlight_uid)
|
||||||
|
if self.mode == "coins":
|
||||||
|
lines.append(f"{prefix} {name} - {coin(entry[1])}")
|
||||||
|
elif self.mode == "exp":
|
||||||
|
lines.append(
|
||||||
|
S.LEADERBOARD_UI["exp_entry"].format(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
exp=entry[1],
|
||||||
|
level=entry[2],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif self.mode == "season":
|
||||||
|
lines.append(
|
||||||
|
S.LEADERBOARD_UI["season_entry"].format(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
exp=entry[1],
|
||||||
|
prestige=entry[2],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif self.mode == "prestige":
|
||||||
|
lines.append(
|
||||||
|
S.LEADERBOARD_UI["prestige_entry"].format(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
prestige=entry[1],
|
||||||
|
pp=entry[2],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif self.mode == "wagered":
|
||||||
|
lines.append(
|
||||||
|
S.LEADERBOARD_UI["wagered_entry"].format(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
wagered=coin(entry[1]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif self.mode == "fish":
|
||||||
|
lines.append(
|
||||||
|
S.LEADERBOARD_UI["fish_entry"].format(
|
||||||
|
prefix=prefix,
|
||||||
|
name=name,
|
||||||
|
caught=entry[1],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
total = self.max_page + 1
|
||||||
|
embed.description = "\n".join(lines)
|
||||||
|
embed.set_footer(
|
||||||
|
text=S.LEADERBOARD_UI["footer"].format(
|
||||||
|
page=self.page + 1,
|
||||||
|
total=total,
|
||||||
|
count=len(current),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
@discord.ui.button(label="◄", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def prev_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.page -= 1
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="►", style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def next_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.page += 1
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_find_me"], style=discord.ButtonStyle.secondary, row=0)
|
||||||
|
async def find_me_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
uid = interaction.user.id
|
||||||
|
for i, entry in enumerate(self._current_list()):
|
||||||
|
if int(entry[0]) == uid:
|
||||||
|
self.page = i // self.PER_PAGE
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(
|
||||||
|
embed=self._make_embed(highlight_uid=uid),
|
||||||
|
view=self,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await interaction.response.send_message(S.ERR["not_in_leaderboard"], ephemeral=True)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_coins"], style=discord.ButtonStyle.primary, row=1)
|
||||||
|
async def coins_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "coins"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_exp"], style=discord.ButtonStyle.secondary, row=1)
|
||||||
|
async def exp_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "exp"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_season"], style=discord.ButtonStyle.secondary, row=1)
|
||||||
|
async def season_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "season"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_prestige"], style=discord.ButtonStyle.secondary, row=1)
|
||||||
|
async def prestige_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "prestige"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_wagered"], style=discord.ButtonStyle.secondary, row=1)
|
||||||
|
async def wagered_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "wagered"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label=S.LEADERBOARD_UI["btn_fish"], style=discord.ButtonStyle.secondary, row=2)
|
||||||
|
async def fish_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.mode = "fish"
|
||||||
|
self.page = 0
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self._make_embed(), view=self)
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
for child in self.children:
|
||||||
|
child.disabled = True
|
||||||
|
|
||||||
|
@tree.command(name="leaderboard", description=S.CMD["leaderboard"])
|
||||||
|
async def cmd_leaderboard(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
coins_raw, exp_raw, season_raw, prestige_raw, wagered_raw, fish_raw = await asyncio.gather(
|
||||||
|
economy.get_leaderboard(top_n=None),
|
||||||
|
economy.get_leaderboard_exp(top_n=None),
|
||||||
|
economy.get_leaderboard_season_exp(top_n=None),
|
||||||
|
economy.get_leaderboard_prestige(top_n=None),
|
||||||
|
economy.get_leaderboard_wagered(top_n=None),
|
||||||
|
economy.get_leaderboard_fish(top_n=None),
|
||||||
|
)
|
||||||
|
|
||||||
|
house_entry = None
|
||||||
|
regular = []
|
||||||
|
bot_id = bot.user.id if bot.user else None
|
||||||
|
for uid, bal in coins_raw:
|
||||||
|
if bot_id and int(uid) == bot_id:
|
||||||
|
house_entry = (uid, bal)
|
||||||
|
else:
|
||||||
|
regular.append((uid, bal))
|
||||||
|
|
||||||
|
def _no_bot(entries: list) -> list:
|
||||||
|
return [e for e in entries if not (bot_id and int(e[0]) == bot_id)]
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"coins": regular,
|
||||||
|
"exp": _no_bot(exp_raw),
|
||||||
|
"season": _no_bot(season_raw),
|
||||||
|
"prestige": _no_bot(prestige_raw),
|
||||||
|
"wagered": _no_bot(wagered_raw),
|
||||||
|
"fish": _no_bot(fish_raw),
|
||||||
|
"house_entry": house_entry,
|
||||||
|
}
|
||||||
|
view = LeaderboardView(data, interaction.guild, bot.user)
|
||||||
|
await interaction.followup.send(embed=view._make_embed(), view=view)
|
||||||
|
|
||||||
|
def _shop_embed(tier: int, user_data: dict) -> discord.Embed:
|
||||||
|
owned = set(user_data.get("items", []))
|
||||||
|
item_uses = user_data.get("item_uses", {})
|
||||||
|
tier_names = {1: S.SHOP_UI["tier_1"], 2: S.SHOP_UI["tier_2"], 3: S.SHOP_UI["tier_3"]}
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{economy.COIN} TipiBOTi pood · {tier_names[tier]}",
|
||||||
|
description=S.SHOP_UI["desc"].format(bal=coin(user_data["balance"])),
|
||||||
|
color=[0x57F287, 0xF4C430, 0xED4245][tier - 1],
|
||||||
|
)
|
||||||
|
for item_id in sorted(economy.SHOP_TIERS[tier], key=lambda k: economy.SHOP[k]["cost"]):
|
||||||
|
item = economy.SHOP[item_id]
|
||||||
|
anticheat_uses = item_uses.get("anticheat", 0) if item_id == "anticheat" else 0
|
||||||
|
min_lvl = economy.SHOP_LEVEL_REQ.get(item_id, 0)
|
||||||
|
user_lvl = economy.get_level(user_data.get("exp", 0))
|
||||||
|
if item_id in owned and not (item_id == "anticheat" and anticheat_uses <= 0):
|
||||||
|
if item_id == "anticheat":
|
||||||
|
key = "owned_uses_1" if anticheat_uses == 1 else "owned_uses_n"
|
||||||
|
status = S.SHOP_UI[key].format(uses=anticheat_uses)
|
||||||
|
else:
|
||||||
|
status = S.SHOP_UI["owned"]
|
||||||
|
elif min_lvl > 0 and user_lvl < min_lvl:
|
||||||
|
status = S.SHOP_UI["locked"].format(min_lvl=min_lvl, user_lvl=user_lvl)
|
||||||
|
else:
|
||||||
|
status = f"{item['cost']} {economy.COIN}"
|
||||||
|
embed.add_field(
|
||||||
|
name=f"{item['emoji']} {item['name']} · {status}",
|
||||||
|
value=item["description"],
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
class ShopView(discord.ui.View):
|
||||||
|
def __init__(self, user_data: dict, tier: int = 1):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self._user_data = user_data
|
||||||
|
self._tier = tier
|
||||||
|
self._update_buttons()
|
||||||
|
|
||||||
|
def _update_buttons(self):
|
||||||
|
self.clear_items()
|
||||||
|
for t, label in [(1, S.SHOP_BTN[1]), (2, S.SHOP_BTN[2]), (3, S.SHOP_BTN[3])]:
|
||||||
|
btn = discord.ui.Button(
|
||||||
|
label=label,
|
||||||
|
style=discord.ButtonStyle.primary if t == self._tier else discord.ButtonStyle.secondary,
|
||||||
|
custom_id=f"shop_tier_{t}",
|
||||||
|
)
|
||||||
|
btn.callback = self._make_callback(t)
|
||||||
|
self.add_item(btn)
|
||||||
|
|
||||||
|
def _make_callback(self, tier: int):
|
||||||
|
async def callback(interaction: discord.Interaction):
|
||||||
|
self._tier = tier
|
||||||
|
self._update_buttons()
|
||||||
|
self._user_data = await economy.get_user(interaction.user.id)
|
||||||
|
await interaction.response.edit_message(
|
||||||
|
embed=_shop_embed(self._tier, self._user_data),
|
||||||
|
view=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
return callback
|
||||||
|
|
||||||
|
@tree.command(name="shop", description=S.CMD["shop"])
|
||||||
|
async def cmd_shop(interaction: discord.Interaction):
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=_shop_embed(1, data),
|
||||||
|
view=ShopView(data, tier=1),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="buy", description=S.CMD["buy"])
|
||||||
|
@app_commands.describe(ese=S.OPT["buy_ese"])
|
||||||
|
@app_commands.choices(
|
||||||
|
ese=[
|
||||||
|
app_commands.Choice(name=f"{v['name']} ({v['cost']} TipiCOINi)", value=k)
|
||||||
|
for k, v in economy.SHOP.items()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
async def cmd_buy(interaction: discord.Interaction, ese: app_commands.Choice[str]):
|
||||||
|
res = await economy.do_buy(interaction.user.id, ese.value)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "owned":
|
||||||
|
await interaction.response.send_message(S.ERR["item_owned"], ephemeral=True)
|
||||||
|
elif res["reason"] == "level_required":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["item_level_req"].format(
|
||||||
|
min_level=res["min_level"],
|
||||||
|
user_level=res["user_level"],
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "insufficient":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["broke_need"].format(need=coin(res["need"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(S.ERR["item_not_found"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
item = res["item"]
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.BUY_UI["title"].format(emoji=item["emoji"], name=item["name"]),
|
||||||
|
description=S.BUY_UI["desc"].format(
|
||||||
|
description=item["description"],
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
380
economy_fish_commands.py
Normal file
380
economy_fish_commands.py
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import random
|
||||||
|
from collections.abc import Awaitable, Callable, MutableSet
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_fish_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
coin: Callable[[int], str],
|
||||||
|
cd_ts: Callable,
|
||||||
|
check_cmd_rate: Callable[[discord.Interaction], Awaitable[bool]],
|
||||||
|
maybe_remind: Callable[[int, str], Awaitable[None]],
|
||||||
|
award_exp: Callable[[discord.Interaction, int], Awaitable[None]],
|
||||||
|
active_games: MutableSet[int],
|
||||||
|
) -> None:
|
||||||
|
class FishCatchView(discord.ui.View):
|
||||||
|
"""Shown after a successful pull - lets user sell or keep the fish."""
|
||||||
|
|
||||||
|
def __init__(self, user_id: int, res: dict, fish_id: str, weight: int):
|
||||||
|
super().__init__(timeout=60)
|
||||||
|
self.user_id = user_id
|
||||||
|
self._res = res
|
||||||
|
self._fish_id = fish_id
|
||||||
|
self._weight = weight
|
||||||
|
self._done = False
|
||||||
|
|
||||||
|
def _catch_embed(self, color: int = 0x57F287) -> discord.Embed:
|
||||||
|
rarity = economy.FISH_CATALOGUE[self._fish_id]["rarity"]
|
||||||
|
emoji = S.FISH_RARITY_EMOJI[rarity]
|
||||||
|
fish_name = S.FISH_NAMES[self._fish_id]
|
||||||
|
desc = S.FISH_UI["catch_desc"].format(
|
||||||
|
name=fish_name,
|
||||||
|
weight=self._weight,
|
||||||
|
exp=self._res["exp"],
|
||||||
|
value=coin(self._res["value"]),
|
||||||
|
)
|
||||||
|
if self._res.get("is_new"):
|
||||||
|
desc += S.FISH_UI["new_fish"]
|
||||||
|
return discord.Embed(title=f"{emoji} {fish_name}!", description=desc, color=color)
|
||||||
|
|
||||||
|
@discord.ui.button(label="", style=discord.ButtonStyle.success)
|
||||||
|
async def sell_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if self._done:
|
||||||
|
await interaction.response.defer()
|
||||||
|
return
|
||||||
|
self._done = True
|
||||||
|
self.stop()
|
||||||
|
for child in self.children:
|
||||||
|
child.disabled = True
|
||||||
|
sell_res = await economy.do_fish_sell(self.user_id, [-1])
|
||||||
|
rarity = economy.FISH_CATALOGUE[self._fish_id]["rarity"]
|
||||||
|
emoji = S.FISH_RARITY_EMOJI[rarity]
|
||||||
|
fish_name = S.FISH_NAMES[self._fish_id]
|
||||||
|
desc = S.FISH_UI["catch_sold"].format(
|
||||||
|
name=fish_name,
|
||||||
|
weight=self._weight,
|
||||||
|
coins=coin(sell_res["coins"]),
|
||||||
|
exp=self._res["exp"],
|
||||||
|
balance=coin(sell_res["balance"]),
|
||||||
|
)
|
||||||
|
if self._res.get("is_new"):
|
||||||
|
desc += S.FISH_UI["new_fish"]
|
||||||
|
embed = discord.Embed(title=f"{emoji} {fish_name}!", description=desc, color=0x57F287)
|
||||||
|
await interaction.response.edit_message(embed=embed, view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="", style=discord.ButtonStyle.secondary)
|
||||||
|
async def keep_btn(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if self._done:
|
||||||
|
await interaction.response.defer()
|
||||||
|
return
|
||||||
|
self._done = True
|
||||||
|
self.stop()
|
||||||
|
for child in self.children:
|
||||||
|
child.disabled = True
|
||||||
|
rarity = economy.FISH_CATALOGUE[self._fish_id]["rarity"]
|
||||||
|
emoji = S.FISH_RARITY_EMOJI[rarity]
|
||||||
|
fish_name = S.FISH_NAMES[self._fish_id]
|
||||||
|
desc = S.FISH_UI["catch_kept"].format(
|
||||||
|
name=fish_name,
|
||||||
|
weight=self._weight,
|
||||||
|
exp=self._res["exp"],
|
||||||
|
)
|
||||||
|
if self._res.get("is_new"):
|
||||||
|
desc += S.FISH_UI["new_fish"]
|
||||||
|
embed = discord.Embed(title=f"{emoji} {fish_name}!", description=desc, color=0x5865F2)
|
||||||
|
await interaction.response.edit_message(embed=embed, view=self)
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
for child in self.children:
|
||||||
|
child.disabled = True
|
||||||
|
|
||||||
|
class FishingView(discord.ui.View):
|
||||||
|
BITE_WINDOW = 2.0
|
||||||
|
|
||||||
|
def __init__(self, user_id: int, fish_id: str, weight: int):
|
||||||
|
super().__init__(timeout=40)
|
||||||
|
self.user_id = user_id
|
||||||
|
self._fish_id = fish_id
|
||||||
|
self._weight = weight
|
||||||
|
self._clicked = False
|
||||||
|
self._bite_active = False
|
||||||
|
self._msg: discord.Message | None = None
|
||||||
|
|
||||||
|
self.pull_btn = discord.ui.Button(
|
||||||
|
label=S.FISH_UI["btn_wait"],
|
||||||
|
style=discord.ButtonStyle.secondary,
|
||||||
|
disabled=True,
|
||||||
|
)
|
||||||
|
self.pull_btn.callback = self._pull
|
||||||
|
self.add_item(self.pull_btn)
|
||||||
|
|
||||||
|
async def start(self, msg: discord.Message) -> None:
|
||||||
|
self._msg = msg
|
||||||
|
wait = random.uniform(5, 15)
|
||||||
|
await asyncio.sleep(wait)
|
||||||
|
if self._clicked or self.is_finished():
|
||||||
|
return
|
||||||
|
self._bite_active = True
|
||||||
|
self.pull_btn.disabled = False
|
||||||
|
self.pull_btn.label = S.FISH_UI["btn_bite"]
|
||||||
|
self.pull_btn.style = discord.ButtonStyle.success
|
||||||
|
try:
|
||||||
|
await msg.edit(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title=S.TITLE["fish_bite"],
|
||||||
|
description=S.FISH_UI["bite_desc"],
|
||||||
|
color=0xED4245,
|
||||||
|
),
|
||||||
|
view=self,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
await asyncio.sleep(self.BITE_WINDOW)
|
||||||
|
if not self._clicked:
|
||||||
|
self.stop()
|
||||||
|
active_games.discard(self.user_id)
|
||||||
|
self.pull_btn.disabled = True
|
||||||
|
try:
|
||||||
|
await msg.edit(
|
||||||
|
embed=discord.Embed(
|
||||||
|
title=S.TITLE["fish_escape"],
|
||||||
|
description=S.FISH_UI["escape_desc"],
|
||||||
|
color=0x99AAB5,
|
||||||
|
),
|
||||||
|
view=self,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def _pull(self, interaction: discord.Interaction) -> None:
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if not self._bite_active:
|
||||||
|
await interaction.response.send_message(S.FISH_UI["too_early"], ephemeral=True)
|
||||||
|
return
|
||||||
|
self._clicked = True
|
||||||
|
self.stop()
|
||||||
|
active_games.discard(self.user_id)
|
||||||
|
self.pull_btn.disabled = True
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
|
if self._fish_id == "junk":
|
||||||
|
junk_text = random.choice(S.FISH_JUNK_LINES)
|
||||||
|
user_data = await economy.get_user(interaction.user.id)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fish_junk"],
|
||||||
|
description=S.FISH_UI["junk_desc"].format(
|
||||||
|
text=junk_text,
|
||||||
|
balance=coin(user_data.get("balance", 0)),
|
||||||
|
),
|
||||||
|
color=0x99AAB5,
|
||||||
|
)
|
||||||
|
await self._msg.edit(embed=embed, view=self)
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await economy.do_fish_resolve(self.user_id, self._fish_id, self._weight)
|
||||||
|
if not res["ok"]:
|
||||||
|
await self._msg.edit(
|
||||||
|
embed=discord.Embed(title=S.ERR["generic_error"], color=0xED4245),
|
||||||
|
view=self,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
catch_view = FishCatchView(self.user_id, res, self._fish_id, self._weight)
|
||||||
|
catch_view.sell_btn.label = S.FISH_UI["btn_sell"]
|
||||||
|
catch_view.keep_btn.label = S.FISH_UI["btn_keep"]
|
||||||
|
await self._msg.edit(embed=catch_view._catch_embed(), view=catch_view)
|
||||||
|
if res.get("exp", 0) > 0:
|
||||||
|
asyncio.create_task(award_exp(interaction, res["exp"]))
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
for child in self.children:
|
||||||
|
child.disabled = True
|
||||||
|
active_games.discard(self.user_id)
|
||||||
|
|
||||||
|
@tree.command(name="fish", description=S.CMD["fish"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_fish(interaction: discord.Interaction):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
if interaction.user.id in active_games:
|
||||||
|
await interaction.response.send_message(S.ERR["game_in_progress"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await economy.do_fish_start(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["fish"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
user_data = await economy.get_user(interaction.user.id)
|
||||||
|
rarity_bump = "kalavork" in user_data.get("items", [])
|
||||||
|
has_echolood = "echolood" in user_data.get("items", [])
|
||||||
|
fish_id, weight = economy.roll_fish(rarity_bump=rarity_bump)
|
||||||
|
|
||||||
|
active_games.add(interaction.user.id)
|
||||||
|
view = FishingView(interaction.user.id, fish_id, weight)
|
||||||
|
if has_echolood:
|
||||||
|
view.BITE_WINDOW = 3.0
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fish_cast"],
|
||||||
|
description=S.FISH_UI["cast_desc"],
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, view=view)
|
||||||
|
msg = await interaction.original_response()
|
||||||
|
asyncio.create_task(view.start(msg))
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "fish"))
|
||||||
|
|
||||||
|
@tree.command(name="fishbook", description=S.CMD["fishbook"])
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["fishbook_kasutaja"])
|
||||||
|
async def cmd_fishbook(interaction: discord.Interaction, kasutaja: discord.Member | None = None):
|
||||||
|
target = kasutaja or interaction.user
|
||||||
|
res = await economy.do_fishbook(target.id)
|
||||||
|
book: dict = res["book"]
|
||||||
|
total = res["total_species"]
|
||||||
|
caught_count = res["unique_caught"]
|
||||||
|
|
||||||
|
if not book:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fishbook"],
|
||||||
|
description=S.FISH_UI["book_empty"],
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
inv_counts: dict = res.get("inv_counts", {})
|
||||||
|
all_fish = list(economy.FISH_CATALOGUE.items())
|
||||||
|
lines = [S.FISH_UI["book_caught"].format(caught=caught_count, total=total)]
|
||||||
|
for fish_id, fish_data in all_fish:
|
||||||
|
rarity = fish_data["rarity"]
|
||||||
|
emoji = S.FISH_RARITY_EMOJI[rarity]
|
||||||
|
rarity_name = S.FISH_RARITY_NAMES[rarity]
|
||||||
|
count = book.get(fish_id, 0)
|
||||||
|
if count > 0:
|
||||||
|
n_inv = inv_counts.get(fish_id, 0)
|
||||||
|
inv_str = S.FISH_UI["book_inv"].format(n=n_inv) if n_inv > 0 else ""
|
||||||
|
lines.append(
|
||||||
|
S.FISH_UI["book_yes"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=S.FISH_NAMES[fish_id],
|
||||||
|
rarity=rarity_name,
|
||||||
|
count=count,
|
||||||
|
inv=inv_str,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(S.FISH_UI["book_no"].format(rarity=rarity_name))
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fishbook"].replace("Kalakogu", f"{target.display_name} kalakogu"),
|
||||||
|
description="\n".join(lines),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.set_footer(
|
||||||
|
text=S.FISH_UI["book_footer"].format(
|
||||||
|
page=1,
|
||||||
|
total_pages=1,
|
||||||
|
caught=caught_count,
|
||||||
|
total=total,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="fishsell", description=S.CMD["fishsell"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_fishsell(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
user_data = await economy.get_user(interaction.user.id)
|
||||||
|
inv: list = user_data.get("fish_inventory") or []
|
||||||
|
if not inv:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fishbook"],
|
||||||
|
description=S.FISH_UI["inv_empty"],
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
total_value = sum(e["value"] for e in inv)
|
||||||
|
lines = [S.FISH_UI["inv_header"].format(count=len(inv), total_value=coin(total_value))]
|
||||||
|
for entry in inv:
|
||||||
|
fid = entry.get("fish_id", "")
|
||||||
|
rarity = economy.FISH_CATALOGUE.get(fid, {}).get("rarity", "common")
|
||||||
|
emoji = S.FISH_RARITY_EMOJI.get(rarity, "🐟")
|
||||||
|
name = S.FISH_NAMES.get(fid, fid)
|
||||||
|
lines.append(
|
||||||
|
S.FISH_UI["inv_entry"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=name,
|
||||||
|
weight=entry["weight"],
|
||||||
|
value=coin(entry["value"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["fishbook"],
|
||||||
|
description="\n".join(lines),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
|
||||||
|
sell_all_btn = discord.ui.Button(
|
||||||
|
label=S.FISH_UI["btn_sell"] + f" ({coin(total_value)})",
|
||||||
|
style=discord.ButtonStyle.success,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _sell_all(btn_interaction: discord.Interaction):
|
||||||
|
if btn_interaction.user.id != interaction.user.id:
|
||||||
|
await btn_interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
sell_view.stop()
|
||||||
|
for child in sell_view.children:
|
||||||
|
child.disabled = True
|
||||||
|
res = await economy.do_fish_sell(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
await btn_interaction.response.edit_message(
|
||||||
|
embed=discord.Embed(description=S.FISH_UI["inv_none"], color=0x99AAB5),
|
||||||
|
view=sell_view,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
sold_embed = discord.Embed(
|
||||||
|
title=S.TITLE["fishbook"],
|
||||||
|
description=S.FISH_UI["inv_sold_all"].format(
|
||||||
|
count=res["count"],
|
||||||
|
coins=coin(res["coins"]),
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await btn_interaction.response.edit_message(embed=sold_embed, view=sell_view)
|
||||||
|
|
||||||
|
sell_all_btn.callback = _sell_all
|
||||||
|
sell_view = discord.ui.View(timeout=60)
|
||||||
|
sell_view.add_item(sell_all_btn)
|
||||||
|
await interaction.followup.send(embed=embed, view=sell_view, ephemeral=True)
|
||||||
1132
economy_games_commands.py
Normal file
1132
economy_games_commands.py
Normal file
File diff suppressed because it is too large
Load Diff
257
economy_income_commands.py
Normal file
257
economy_income_commands.py
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_income_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
coin: Callable[[int], str],
|
||||||
|
cd_ts: Callable[[datetime.timedelta], str],
|
||||||
|
check_cmd_rate: Callable[[discord.Interaction], Awaitable[bool]],
|
||||||
|
maybe_remind: Callable[[int, str], Awaitable[None]],
|
||||||
|
award_exp: Callable[[discord.Interaction, int], Awaitable[None]],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="daily", description=S.CMD["daily"])
|
||||||
|
async def cmd_daily(interaction: discord.Interaction):
|
||||||
|
res = await economy.do_daily(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["daily"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
streak = res["streak"]
|
||||||
|
streak_str = f"🔥 {streak}p" + (
|
||||||
|
" (+200%)"
|
||||||
|
if res["streak_mult"] >= 3.0
|
||||||
|
else " (+100%)"
|
||||||
|
if res["streak_mult"] >= 2.0
|
||||||
|
else " (+50%)"
|
||||||
|
if res["streak_mult"] >= 1.5
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
lines = [S.DAILY_UI["earned"].format(earned=coin(res["earned"]))]
|
||||||
|
if res["interest"]:
|
||||||
|
lines.append(S.DAILY_UI["interest"].format(interest=coin(res["interest"])))
|
||||||
|
if res["vip"]:
|
||||||
|
lines.append(S.DAILY_UI["vip"])
|
||||||
|
lines.append(S.DAILY_UI["footer"].format(streak_str=streak_str, balance=coin(res["balance"])))
|
||||||
|
|
||||||
|
embed = discord.Embed(title=S.TITLE["daily"], description="\n".join(lines), color=0xF4C430)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "daily"))
|
||||||
|
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["daily"]))
|
||||||
|
|
||||||
|
@tree.command(name="work", description=S.CMD["work"])
|
||||||
|
async def cmd_work(interaction: discord.Interaction):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
res = await economy.do_work(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["work"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
desc = S.WORK_UI["desc"].format(job=res["job"], earned=coin(res["earned"]))
|
||||||
|
if res["lucky"]:
|
||||||
|
desc += S.WORK_UI["redbull"]
|
||||||
|
if res["hiir"]:
|
||||||
|
desc += S.WORK_UI["hiir"]
|
||||||
|
if res["laud"]:
|
||||||
|
desc += S.WORK_UI["laud"]
|
||||||
|
desc += S.WORK_UI["balance"].format(balance=coin(res["balance"]))
|
||||||
|
embed = discord.Embed(title=S.TITLE["work"], description=desc, color=0x57F287)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "work"))
|
||||||
|
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["work"]))
|
||||||
|
|
||||||
|
@tree.command(name="beg", description=S.CMD["beg"])
|
||||||
|
async def cmd_beg(interaction: discord.Interaction):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
res = await economy.do_beg(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["beg"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if res["jailed"]:
|
||||||
|
title = "🔒 " + S.TITLE["beg"]
|
||||||
|
color = 0xE67E22
|
||||||
|
else:
|
||||||
|
title = S.TITLE["beg"]
|
||||||
|
color = 0x99AAB5
|
||||||
|
beg_lines = [S.BEG_UI["desc"].format(text=res["text"], earned=coin(res["earned"]))]
|
||||||
|
if res["klaviatuur"]:
|
||||||
|
beg_lines.append(S.BEG_UI["klaviatuur"])
|
||||||
|
beg_lines.append(S.BEG_UI["balance"].format(balance=coin(res["balance"])))
|
||||||
|
embed = discord.Embed(title=title, description="\n".join(beg_lines), color=color)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "beg"))
|
||||||
|
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["beg"]))
|
||||||
|
|
||||||
|
@tree.command(name="crime", description=S.CMD["crime"])
|
||||||
|
async def cmd_crime(interaction: discord.Interaction):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
res = await economy.do_crime(interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["crime"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if res["success"]:
|
||||||
|
crime_lines = [S.CRIME_UI["win_desc"].format(text=res["text"], earned=coin(res["earned"]))]
|
||||||
|
if res["mikrofon"]:
|
||||||
|
crime_lines.append(S.CRIME_UI["mikrofon"].lstrip("\n"))
|
||||||
|
crime_lines.append(S.CRIME_UI["balance"].lstrip("\n").format(balance=coin(res["balance"])))
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["crime_win"],
|
||||||
|
description="\n".join(crime_lines),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
jail_part = (
|
||||||
|
S.CRIME_UI["fail_jailed"].format(ts=cd_ts(economy.JAIL_DURATION))
|
||||||
|
if res.get("jailed")
|
||||||
|
else S.CRIME_UI["fail_shield"]
|
||||||
|
)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["crime_fail"],
|
||||||
|
description=S.CRIME_UI["fail_base"].format(text=res["text"], fine=coin(res["fine"]))
|
||||||
|
+ jail_part
|
||||||
|
+ S.CRIME_UI["balance"].format(balance=coin(res["balance"])),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "crime"))
|
||||||
|
if res["success"]:
|
||||||
|
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["crime_win"]))
|
||||||
|
|
||||||
|
@tree.command(name="rob", description=S.CMD["rob"])
|
||||||
|
async def cmd_rob(interaction: discord.Interaction, sihtmärk: discord.Member):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
if sihtmärk.id == interaction.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["rob_self"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if sihtmärk.bot and (bot.user is None or sihtmärk.id != bot.user.id):
|
||||||
|
await interaction.response.send_message(S.ERR["rob_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if bot.user and sihtmärk.id == bot.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["rob_house_blocked"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await economy.do_rob(interaction.user.id, sihtmärk.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "cooldown":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["rob"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "broke":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["rob_too_poor"].format(name=sihtmärk.display_name),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
elif res["reason"] == "target_jailed":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["rob_target_jailed"].format(name=sihtmärk.display_name),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if res["success"]:
|
||||||
|
if res.get("jackpot"):
|
||||||
|
desc = S.ROB_UI["jackpot_desc"].format(stolen=coin(res["stolen"]), balance=coin(res["balance"]))
|
||||||
|
color = 0xF4C430
|
||||||
|
else:
|
||||||
|
desc = S.ROB_UI["win_desc"].format(
|
||||||
|
stolen=coin(res["stolen"]),
|
||||||
|
name=sihtmärk.display_name,
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
)
|
||||||
|
color = 0x57F287
|
||||||
|
embed = discord.Embed(title=S.TITLE["rob_win"], description=desc, color=color)
|
||||||
|
elif res["reason"] == "valvur":
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["rob_anticheat"],
|
||||||
|
description=S.ROB_UI["anticheat_desc"].format(
|
||||||
|
name=sihtmärk.display_name,
|
||||||
|
fine=coin(res["fine"]),
|
||||||
|
),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
target_data = await economy.get_user(sihtmärk.id)
|
||||||
|
if "anticheat" not in target_data.get("items", []):
|
||||||
|
try:
|
||||||
|
await sihtmärk.send(S.ROB_UI["anticheat_worn"])
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["rob_fail"],
|
||||||
|
description=S.ROB_UI["fail_desc"].format(
|
||||||
|
fine=coin(res["fine"]),
|
||||||
|
balance=coin(res["balance"]),
|
||||||
|
),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed)
|
||||||
|
asyncio.create_task(maybe_remind(interaction.user.id, "rob"))
|
||||||
|
if res["success"]:
|
||||||
|
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["rob_win"]))
|
||||||
|
try:
|
||||||
|
await sihtmärk.send(
|
||||||
|
S.ROB_UI["victim_dm"].format(
|
||||||
|
robber=interaction.user.display_name,
|
||||||
|
stolen=coin(res["stolen"]),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
249
economy_prestige_commands.py
Normal file
249
economy_prestige_commands.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_prestige_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
check_cmd_rate: Callable[[discord.Interaction], Awaitable[bool]],
|
||||||
|
ensure_level_role: Callable[[discord.Member, int], Awaitable[None]],
|
||||||
|
) -> None:
|
||||||
|
class PrestigeView(discord.ui.View):
|
||||||
|
def __init__(self, user_id: int, tab: str = "status"):
|
||||||
|
super().__init__(timeout=60)
|
||||||
|
self.user_id = user_id
|
||||||
|
self.tab = tab
|
||||||
|
|
||||||
|
async def _rebuild(self, data: dict):
|
||||||
|
self.clear_items()
|
||||||
|
pp = data.get("prestige_points", 0)
|
||||||
|
upgrades: dict = data.get("prestige_upgrades") or {}
|
||||||
|
exp = data.get("exp", 0)
|
||||||
|
level = economy.get_level(exp)
|
||||||
|
|
||||||
|
for tab_id, label in (("status", S.PRESTIGE_UI["btn_tab_status"]), ("shop", S.PRESTIGE_UI["btn_tab_shop"])):
|
||||||
|
btn = discord.ui.Button(
|
||||||
|
label=label,
|
||||||
|
style=discord.ButtonStyle.primary if tab_id == self.tab else discord.ButtonStyle.secondary,
|
||||||
|
disabled=(tab_id == self.tab),
|
||||||
|
row=0,
|
||||||
|
)
|
||||||
|
btn.callback = self._switch_tab(tab_id)
|
||||||
|
self.add_item(btn)
|
||||||
|
|
||||||
|
if self.tab == "status" and level >= economy.PRESTIGE_MIN_LEVEL:
|
||||||
|
confirm_btn = discord.ui.Button(
|
||||||
|
label=S.PRESTIGE_UI["btn_confirm"],
|
||||||
|
style=discord.ButtonStyle.danger,
|
||||||
|
row=1,
|
||||||
|
)
|
||||||
|
confirm_btn.callback = self._do_prestige()
|
||||||
|
cancel_btn = discord.ui.Button(
|
||||||
|
label=S.PRESTIGE_UI["btn_cancel"],
|
||||||
|
style=discord.ButtonStyle.secondary,
|
||||||
|
row=1,
|
||||||
|
)
|
||||||
|
cancel_btn.callback = self._do_cancel()
|
||||||
|
self.add_item(confirm_btn)
|
||||||
|
self.add_item(cancel_btn)
|
||||||
|
elif self.tab == "shop":
|
||||||
|
for uid, item in economy.PRESTIGE_SHOP.items():
|
||||||
|
cur_level = upgrades.get(uid, 0)
|
||||||
|
if cur_level >= item["max_level"]:
|
||||||
|
continue
|
||||||
|
cost = item["pp_cost"]
|
||||||
|
btn = discord.ui.Button(
|
||||||
|
label=S.PRESTIGE_UI["btn_buy_upgrade"].format(emoji=item["emoji"], name=S.PRESTIGE_SHOP_NAMES[uid], cost=cost),
|
||||||
|
style=discord.ButtonStyle.success if pp >= cost else discord.ButtonStyle.secondary,
|
||||||
|
disabled=(pp < cost),
|
||||||
|
row=1,
|
||||||
|
)
|
||||||
|
btn.callback = self._buy_upgrade(uid)
|
||||||
|
self.add_item(btn)
|
||||||
|
|
||||||
|
def _build_status_embed(self, data: dict) -> discord.Embed:
|
||||||
|
exp = data.get("exp", 0)
|
||||||
|
level = economy.get_level(exp)
|
||||||
|
pp = data.get("prestige_points", 0)
|
||||||
|
p_level = data.get("prestige_level", 0)
|
||||||
|
if level >= economy.PRESTIGE_MIN_LEVEL:
|
||||||
|
pp_preview = max(1, exp // 1000)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_confirm"],
|
||||||
|
description=S.PRESTIGE_UI["confirm_desc"].format(level=level, exp=exp, pp=pp_preview),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_too_low"],
|
||||||
|
description=S.PRESTIGE_UI["too_low_desc"].format(level=level, required=economy.PRESTIGE_MIN_LEVEL),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
if p_level > 0:
|
||||||
|
embed.set_footer(text=S.PRESTIGE_UI["status_footer"].format(level=p_level, pp=pp))
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _build_shop_embed(self, data: dict) -> discord.Embed:
|
||||||
|
pp = data.get("prestige_points", 0)
|
||||||
|
upgrades: dict = data.get("prestige_upgrades") or {}
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_shop"],
|
||||||
|
description=S.PRESTIGE_UI["shop_desc"].format(pp=pp),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
for uid, item in economy.PRESTIGE_SHOP.items():
|
||||||
|
cur_level = upgrades.get(uid, 0)
|
||||||
|
name = f"{item['emoji']} {S.PRESTIGE_SHOP_NAMES[uid]}"
|
||||||
|
cost_str = S.PRESTIGE_UI["shop_maxed"] if cur_level >= item["max_level"] else S.PRESTIGE_UI["shop_cost_fmt"].format(cost=item["pp_cost"])
|
||||||
|
level_str = S.PRESTIGE_UI["shop_level_fmt"].format(cur=cur_level, max=item["max_level"])
|
||||||
|
embed.add_field(name=f"{name} · {level_str} · {cost_str}", value=S.PRESTIGE_SHOP_DESCRIPTIONS[uid], inline=False)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _switch_tab(self, tab_id: str):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
self.tab = tab_id
|
||||||
|
await interaction.response.defer()
|
||||||
|
data = await economy.get_user(self.user_id)
|
||||||
|
await self._rebuild(data)
|
||||||
|
embed = self._build_status_embed(data) if tab_id == "status" else self._build_shop_embed(data)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
def _do_prestige(self):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await interaction.response.defer()
|
||||||
|
res = await economy.do_prestige(self.user_id)
|
||||||
|
self.clear_items()
|
||||||
|
if not res["ok"]:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_too_low"],
|
||||||
|
description=S.PRESTIGE_UI["too_low_desc"].format(level=res.get("level", 0), required=res.get("required", 30)),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_success"].format(level=res["prestige_level"]),
|
||||||
|
description=S.PRESTIGE_UI["success_desc"].format(pp=res["pp_earned"], level=res["prestige_level"], total_pp=res["prestige_points"]),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
if res.get("ok") and interaction.guild:
|
||||||
|
member = interaction.guild.get_member(self.user_id)
|
||||||
|
if member:
|
||||||
|
asyncio.create_task(ensure_level_role(member, 1))
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
def _do_cancel(self):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True)
|
||||||
|
return
|
||||||
|
self.clear_items()
|
||||||
|
await interaction.response.edit_message(view=self)
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
def _buy_upgrade(self, upgrade_id: str):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await interaction.response.defer()
|
||||||
|
res = await economy.do_prestige_buy(self.user_id, upgrade_id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "insufficient_pp":
|
||||||
|
err = S.PRESTIGE_UI["buy_no_pp"].format(have=res["have"], need=res["need"])
|
||||||
|
elif res["reason"] == "maxed":
|
||||||
|
err = S.PRESTIGE_UI["buy_maxed"]
|
||||||
|
else:
|
||||||
|
err = S.ERR["generic_error"].format(error=res["reason"])
|
||||||
|
await interaction.followup.send(err, ephemeral=True)
|
||||||
|
return
|
||||||
|
data = await economy.get_user(self.user_id)
|
||||||
|
await self._rebuild(data)
|
||||||
|
embed = self._build_shop_embed(data)
|
||||||
|
item = economy.PRESTIGE_SHOP[res["upgrade_id"]]
|
||||||
|
name = f"{item['emoji']} {S.PRESTIGE_SHOP_NAMES[res['upgrade_id']]}"
|
||||||
|
embed.description = S.PRESTIGE_UI["buy_success_desc"].format(
|
||||||
|
name=name,
|
||||||
|
new_level=res["new_level"],
|
||||||
|
max_level=item["max_level"],
|
||||||
|
pp=res["pp_remaining"],
|
||||||
|
) + "\n\n" + (embed.description or "")
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
self.clear_items()
|
||||||
|
|
||||||
|
@tree.command(name="prestige", description=S.CMD["prestige"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_prestige(interaction: discord.Interaction):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
if data.get("eco_banned"):
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
return
|
||||||
|
view = PrestigeView(interaction.user.id)
|
||||||
|
await view._rebuild(data)
|
||||||
|
embed = view._build_status_embed(data)
|
||||||
|
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="prestigeshop", description=S.CMD["prestigeshop"])
|
||||||
|
async def cmd_prestigeshop(interaction: discord.Interaction):
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
view = PrestigeView(interaction.user.id, tab="shop")
|
||||||
|
await view._rebuild(data)
|
||||||
|
embed = view._build_shop_embed(data)
|
||||||
|
await interaction.response.send_message(embed=embed, view=view, ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="prestigebuy", description=S.CMD["prestigebuy"])
|
||||||
|
@app_commands.describe(upgrade=S.OPT["prestigebuy_upgrade"])
|
||||||
|
async def cmd_prestigebuy(interaction: discord.Interaction, upgrade: str):
|
||||||
|
if await check_cmd_rate(interaction):
|
||||||
|
return
|
||||||
|
res = await economy.do_prestige_buy(interaction.user.id, upgrade.strip().lower())
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "banned":
|
||||||
|
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||||
|
elif res["reason"] == "not_found":
|
||||||
|
await interaction.response.send_message(S.PRESTIGE_UI["buy_not_found"], ephemeral=True)
|
||||||
|
elif res["reason"] == "maxed":
|
||||||
|
await interaction.response.send_message(S.PRESTIGE_UI["buy_maxed"], ephemeral=True)
|
||||||
|
elif res["reason"] == "insufficient_pp":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.PRESTIGE_UI["buy_no_pp"].format(have=res["have"], need=res["need"]),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
item = economy.PRESTIGE_SHOP[res["upgrade_id"]]
|
||||||
|
name = f"{item['emoji']} {S.PRESTIGE_SHOP_NAMES[res['upgrade_id']]}"
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["prestige_buy_ok"],
|
||||||
|
description=S.PRESTIGE_UI["buy_success_desc"].format(
|
||||||
|
name=name,
|
||||||
|
new_level=res["new_level"],
|
||||||
|
max_level=res["max_level"],
|
||||||
|
pp=res["pp_remaining"],
|
||||||
|
),
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
545
economy_profile_commands.py
Normal file
545
economy_profile_commands.py
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_profile_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
coin: Callable[[int], str],
|
||||||
|
cd_ts: Callable[[datetime.timedelta], str],
|
||||||
|
ensure_level_role: Callable[[discord.Member, int], Awaitable[None]],
|
||||||
|
) -> None:
|
||||||
|
def _profile_main_embed(target: discord.User | discord.Member, data: dict) -> discord.Embed:
|
||||||
|
exp = data.get("exp", 0)
|
||||||
|
level = economy.get_level(exp)
|
||||||
|
role_name = economy.level_role_name(level)
|
||||||
|
next_level = level + 1
|
||||||
|
exp_this = economy.exp_for_level(level)
|
||||||
|
exp_next = economy.exp_for_level(next_level)
|
||||||
|
progress = exp - exp_this
|
||||||
|
needed = exp_next - exp_this
|
||||||
|
pct = progress / needed if needed > 0 else 1.0
|
||||||
|
filled = int(pct * 12)
|
||||||
|
bar = "█" * filled + "░" * (12 - filled)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.PROFILE_UI["main_title"].format(name=target.display_name),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
embed.add_field(name=S.PROFILE_UI["f_balance"], value=coin(data.get("balance", 0)), inline=True)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.PROFILE_UI["f_level"],
|
||||||
|
value=S.PROFILE_UI["level_val"].format(level=level, role=role_name),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
streak = data.get("daily_streak", 0)
|
||||||
|
if streak:
|
||||||
|
embed.add_field(
|
||||||
|
name=S.PROFILE_UI["f_streak"],
|
||||||
|
value=S.BALANCE_UI["streak_val"].format(streak=streak),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
p_level = data.get("prestige_level", 0)
|
||||||
|
if p_level > 0:
|
||||||
|
p_pp = data.get("prestige_points", 0)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.PROFILE_UI["f_prestige"],
|
||||||
|
value=S.PROFILE_UI["prestige_val"].format(level=p_level, pp=p_pp),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
jail_remaining = economy._is_jailed(data)
|
||||||
|
if jail_remaining:
|
||||||
|
embed.add_field(name=S.PROFILE_UI["f_jail"], value=cd_ts(jail_remaining), inline=True)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.PROFILE_UI["f_progress"].format(next=next_level),
|
||||||
|
value=S.PROFILE_UI["progress_bar"].format(bar=bar, done=progress, needed=needed),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
if level < 10:
|
||||||
|
embed.set_footer(text=S.PROFILE_UI["footer_t1"])
|
||||||
|
elif level < 20:
|
||||||
|
embed.set_footer(text=S.PROFILE_UI["footer_t2"])
|
||||||
|
else:
|
||||||
|
embed.set_footer(text=S.PROFILE_UI["footer_t3"])
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _profile_items_embed(target: discord.User | discord.Member, data: dict) -> discord.Embed:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.PROFILE_UI["items_title"].format(name=target.display_name),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
uses_map = data.get("item_uses", {})
|
||||||
|
item_lines = []
|
||||||
|
for item_id in data.get("items", []):
|
||||||
|
if item_id not in economy.SHOP:
|
||||||
|
continue
|
||||||
|
line = f"{economy.SHOP[item_id]['emoji']} **{economy.SHOP[item_id]['name']}**"
|
||||||
|
if item_id in uses_map:
|
||||||
|
uses = uses_map[item_id]
|
||||||
|
line += (
|
||||||
|
S.BALANCE_UI["uses_one"].format(uses=uses)
|
||||||
|
if uses == 1
|
||||||
|
else S.BALANCE_UI["uses_many"].format(uses=uses)
|
||||||
|
)
|
||||||
|
item_lines.append(line)
|
||||||
|
embed.description = "\n".join(item_lines) if item_lines else S.PROFILE_UI["items_empty"]
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _profile_stats_embed(target: discord.User | discord.Member, data: dict) -> discord.Embed:
|
||||||
|
def _s(key: str) -> int:
|
||||||
|
return data.get(key, 0)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.PROFILE_UI["stats_title"].format(name=target.display_name),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["economy_field"],
|
||||||
|
value=S.STATS_UI["economy_val"].format(
|
||||||
|
peak=coin(_s("peak_balance")),
|
||||||
|
earned=coin(_s("lifetime_earned")),
|
||||||
|
lost=coin(_s("lifetime_lost")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["work_field"],
|
||||||
|
value=S.STATS_UI["work_val"].format(work=_s("work_count"), beg=_s("beg_count")),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(name="\u200b", value="\u200b", inline=False)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["gamble_field"],
|
||||||
|
value=S.STATS_UI["gamble_val"].format(
|
||||||
|
wagered=coin(_s("total_wagered")),
|
||||||
|
win=coin(_s("biggest_win")),
|
||||||
|
loss=coin(_s("biggest_loss")),
|
||||||
|
jackpots=_s("slots_jackpots"),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["crime_field"],
|
||||||
|
value=S.STATS_UI["crime_val"].format(
|
||||||
|
crimes=_s("crimes_attempted"),
|
||||||
|
succeeded=_s("crimes_succeeded"),
|
||||||
|
heists=_s("heists_joined"),
|
||||||
|
heists_won=_s("heists_won"),
|
||||||
|
jailed=_s("times_jailed"),
|
||||||
|
bail=coin(_s("total_bail_paid")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(name="\u200b", value="\u200b", inline=False)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["social_field"],
|
||||||
|
value=S.STATS_UI["social_val"].format(
|
||||||
|
given=coin(_s("total_given")),
|
||||||
|
received=coin(_s("total_received")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["records_field"],
|
||||||
|
value=S.STATS_UI["records_val"].format(streak=_s("best_daily_streak")),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
def _profile_fish_embed(target: discord.User | discord.Member, fish_res: dict) -> discord.Embed:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.PROFILE_UI["fish_title"].format(name=target.display_name),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
book: dict = fish_res["book"]
|
||||||
|
if not book:
|
||||||
|
embed.description = S.FISH_UI["book_empty"]
|
||||||
|
return embed
|
||||||
|
inv_counts: dict = fish_res.get("inv_counts", {})
|
||||||
|
caught_count = fish_res["unique_caught"]
|
||||||
|
total = fish_res["total_species"]
|
||||||
|
lines = [S.FISH_UI["book_caught"].format(caught=caught_count, total=total)]
|
||||||
|
for fish_id, fish_data in economy.FISH_CATALOGUE.items():
|
||||||
|
rarity = fish_data["rarity"]
|
||||||
|
emoji = S.FISH_RARITY_EMOJI[rarity]
|
||||||
|
rarity_name = S.FISH_RARITY_NAMES[rarity]
|
||||||
|
count = book.get(fish_id, 0)
|
||||||
|
if count > 0:
|
||||||
|
n_inv = inv_counts.get(fish_id, 0)
|
||||||
|
inv_str = S.FISH_UI["book_inv"].format(n=n_inv) if n_inv > 0 else ""
|
||||||
|
lines.append(
|
||||||
|
S.FISH_UI["book_yes"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=S.FISH_NAMES[fish_id],
|
||||||
|
rarity=rarity_name,
|
||||||
|
count=count,
|
||||||
|
inv=inv_str,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.append(S.FISH_UI["book_no"].format(rarity=rarity_name))
|
||||||
|
embed.description = "\n".join(lines)
|
||||||
|
embed.set_footer(
|
||||||
|
text=S.FISH_UI["book_footer"].format(
|
||||||
|
page=1,
|
||||||
|
total_pages=1,
|
||||||
|
caught=caught_count,
|
||||||
|
total=total,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
class ProfileView(discord.ui.View):
|
||||||
|
def __init__(self, target: discord.User | discord.Member, invoker_id: int, tab: str = "main"):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self.target = target
|
||||||
|
self.invoker_id = invoker_id
|
||||||
|
self.tab = tab
|
||||||
|
self._rebuild()
|
||||||
|
|
||||||
|
def _rebuild(self):
|
||||||
|
self.clear_items()
|
||||||
|
tabs = [
|
||||||
|
("main", S.PROFILE_UI["btn_profile"]),
|
||||||
|
("items", S.PROFILE_UI["btn_items"]),
|
||||||
|
("stats", S.PROFILE_UI["btn_stats"]),
|
||||||
|
("fish", S.PROFILE_UI["btn_fish"]),
|
||||||
|
]
|
||||||
|
for tab_id, label in tabs:
|
||||||
|
btn = discord.ui.Button(
|
||||||
|
label=label,
|
||||||
|
style=(
|
||||||
|
discord.ButtonStyle.primary
|
||||||
|
if tab_id == self.tab
|
||||||
|
else discord.ButtonStyle.secondary
|
||||||
|
),
|
||||||
|
disabled=(tab_id == self.tab),
|
||||||
|
)
|
||||||
|
btn.callback = self._make_cb(tab_id)
|
||||||
|
self.add_item(btn)
|
||||||
|
|
||||||
|
def _make_cb(self, tab_id: str):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.invoker_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
self.tab = tab_id
|
||||||
|
self._rebuild()
|
||||||
|
await interaction.response.defer()
|
||||||
|
data = await economy.get_user(self.target.id)
|
||||||
|
if tab_id == "fish":
|
||||||
|
fish_res = await economy.do_fishbook(self.target.id)
|
||||||
|
embed = _profile_fish_embed(self.target, fish_res)
|
||||||
|
inv: list = data.get("fish_inventory") or []
|
||||||
|
if inv and self.target.id == self.invoker_id:
|
||||||
|
total_value = sum(entry.get("value", 0) for entry in inv)
|
||||||
|
sell_btn = discord.ui.Button(
|
||||||
|
label=(
|
||||||
|
f"{S.FISH_UI['btn_sell']} "
|
||||||
|
f"({len(inv)} kala · {total_value:,} {economy.COIN})"
|
||||||
|
),
|
||||||
|
style=discord.ButtonStyle.success,
|
||||||
|
row=1,
|
||||||
|
)
|
||||||
|
sell_btn.callback = self._sell_fish_cb()
|
||||||
|
self.add_item(sell_btn)
|
||||||
|
elif tab_id == "items":
|
||||||
|
embed = _profile_items_embed(self.target, data)
|
||||||
|
elif tab_id == "stats":
|
||||||
|
embed = _profile_stats_embed(self.target, data)
|
||||||
|
else:
|
||||||
|
embed = _profile_main_embed(self.target, data)
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
def _sell_fish_cb(self):
|
||||||
|
async def _cb(interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.invoker_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await interaction.response.defer()
|
||||||
|
res = await economy.do_fish_sell(self.invoker_id)
|
||||||
|
self.tab = "fish"
|
||||||
|
self._rebuild()
|
||||||
|
fish_res = await economy.do_fishbook(self.target.id)
|
||||||
|
embed = _profile_fish_embed(self.target, fish_res)
|
||||||
|
sold_line = S.FISH_UI["inv_sold_all"].format(
|
||||||
|
count=res.get("count", 0),
|
||||||
|
coins=coin(res.get("coins", 0)),
|
||||||
|
balance=coin(res.get("balance", 0)),
|
||||||
|
)
|
||||||
|
embed.description = f"{sold_line}\n\n{embed.description or ''}"
|
||||||
|
await interaction.edit_original_response(embed=embed, view=self)
|
||||||
|
|
||||||
|
return _cb
|
||||||
|
|
||||||
|
def _balance_embed(user: discord.User | discord.Member, data: dict) -> discord.Embed:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{economy.COIN} {user.display_name}",
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
embed.add_field(name=S.BALANCE_UI["saldo"], value=coin(data["balance"]), inline=True)
|
||||||
|
streak = data.get("daily_streak", 0)
|
||||||
|
if streak:
|
||||||
|
embed.add_field(
|
||||||
|
name=S.BALANCE_UI["streak"],
|
||||||
|
value=S.BALANCE_UI["streak_val"].format(streak=streak),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
jail_remaining = economy._is_jailed(data)
|
||||||
|
if jail_remaining:
|
||||||
|
embed.add_field(name=S.BALANCE_UI["jailed_until"], value=cd_ts(jail_remaining), inline=True)
|
||||||
|
item_lines = []
|
||||||
|
uses_map = data.get("item_uses", {})
|
||||||
|
for item_id in data.get("items", []):
|
||||||
|
if item_id not in economy.SHOP:
|
||||||
|
continue
|
||||||
|
line = f"{economy.SHOP[item_id]['emoji']} {economy.SHOP[item_id]['name']}"
|
||||||
|
if item_id in uses_map:
|
||||||
|
uses = uses_map[item_id]
|
||||||
|
line += (
|
||||||
|
S.BALANCE_UI["uses_one"].format(uses=uses)
|
||||||
|
if uses == 1
|
||||||
|
else S.BALANCE_UI["uses_many"].format(uses=uses)
|
||||||
|
)
|
||||||
|
item_lines.append(line)
|
||||||
|
if item_lines:
|
||||||
|
embed.add_field(name=S.BALANCE_UI["items"], value="\n".join(item_lines), inline=False)
|
||||||
|
return embed
|
||||||
|
|
||||||
|
@tree.command(name="profile", description=S.CMD["profile"])
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["profile_kasutaja"])
|
||||||
|
async def cmd_profile(interaction: discord.Interaction, kasutaja: discord.Member | None = None):
|
||||||
|
target = kasutaja or interaction.user
|
||||||
|
data = await economy.get_user(target.id)
|
||||||
|
embed = _profile_main_embed(target, data)
|
||||||
|
invoker_id = interaction.user.id
|
||||||
|
await interaction.response.send_message(embed=embed, view=ProfileView(target, invoker_id))
|
||||||
|
if not kasutaja and interaction.guild:
|
||||||
|
member = interaction.guild.get_member(target.id)
|
||||||
|
if member:
|
||||||
|
asyncio.create_task(ensure_level_role(member, economy.get_level(data.get("exp", 0))))
|
||||||
|
|
||||||
|
@tree.command(name="balance", description=S.CMD["balance"])
|
||||||
|
async def cmd_balance(interaction: discord.Interaction, kasutaja: discord.Member | None = None):
|
||||||
|
target = kasutaja or interaction.user
|
||||||
|
data = await economy.get_user(target.id)
|
||||||
|
await interaction.response.send_message(embed=_balance_embed(target, data))
|
||||||
|
|
||||||
|
@tree.command(name="cooldowns", description=S.CMD["cooldowns"])
|
||||||
|
async def cmd_cooldowns(interaction: discord.Interaction):
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
items = set(data.get("items", []))
|
||||||
|
|
||||||
|
def _status(last_key: str, cooldown: datetime.timedelta) -> str:
|
||||||
|
raw = data.get(last_key)
|
||||||
|
if not raw:
|
||||||
|
return S.COOLDOWNS_UI["ready"]
|
||||||
|
last = economy._parse_dt(raw)
|
||||||
|
if last is None:
|
||||||
|
return S.COOLDOWNS_UI["ready"]
|
||||||
|
expires = last + cooldown
|
||||||
|
if expires <= now:
|
||||||
|
return S.COOLDOWNS_UI["ready"]
|
||||||
|
ts = int(expires.timestamp())
|
||||||
|
return f"⏳ <t:{ts}:R>"
|
||||||
|
|
||||||
|
work_cd = datetime.timedelta(minutes=40) if "monitor" in items else economy.COOLDOWNS["work"]
|
||||||
|
beg_cd = datetime.timedelta(minutes=3) if "hiirematt" in items else economy.COOLDOWNS["beg"]
|
||||||
|
daily_cd = datetime.timedelta(hours=18) if "korvaklapid" in items else economy.COOLDOWNS["daily"]
|
||||||
|
fish_cd = datetime.timedelta(seconds=90) if "ussipurk" in items else economy.COOLDOWNS["fish"]
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
S.COOLDOWNS_UI["daily_line"].format(
|
||||||
|
status=_status("last_daily", daily_cd),
|
||||||
|
note=S.COOLDOWNS_UI["note_korvak"] if "korvaklapid" in items else "",
|
||||||
|
),
|
||||||
|
S.COOLDOWNS_UI["work_line"].format(
|
||||||
|
status=_status("last_work", work_cd),
|
||||||
|
note=S.COOLDOWNS_UI["note_monitor"] if "monitor" in items else "",
|
||||||
|
),
|
||||||
|
S.COOLDOWNS_UI["beg_line"].format(
|
||||||
|
status=_status("last_beg", beg_cd),
|
||||||
|
note=S.COOLDOWNS_UI["note_hiirematt"] if "hiirematt" in items else "",
|
||||||
|
),
|
||||||
|
S.COOLDOWNS_UI["crime_line"].format(status=_status("last_crime", economy.COOLDOWNS["crime"])),
|
||||||
|
S.COOLDOWNS_UI["rob_line"].format(status=_status("last_rob", economy.COOLDOWNS["rob"])),
|
||||||
|
S.COOLDOWNS_UI["fish_line"].format(
|
||||||
|
status=_status("last_fish", fish_cd),
|
||||||
|
note=S.COOLDOWNS_UI["note_ussipurk"] if "ussipurk" in items else "",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
jailed = data.get("jailed_until")
|
||||||
|
if jailed:
|
||||||
|
jail_dt = datetime.datetime.fromisoformat(jailed)
|
||||||
|
if jail_dt > now:
|
||||||
|
ts = int(jail_dt.timestamp())
|
||||||
|
lines.append(S.COOLDOWNS_UI["jailed"].format(ts=ts))
|
||||||
|
else:
|
||||||
|
lines.append(S.COOLDOWNS_UI["jail_expired"])
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["cooldowns"],
|
||||||
|
description="\n".join(lines),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="jailed", description=S.CMD["jailed"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_jailed(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
jailed = await economy.do_get_jailed()
|
||||||
|
if not jailed:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.JAILED_UI["title"],
|
||||||
|
description=S.JAILED_UI["empty"],
|
||||||
|
color=0x57F287,
|
||||||
|
)
|
||||||
|
await interaction.followup.send(embed=embed)
|
||||||
|
return
|
||||||
|
|
||||||
|
now = datetime.datetime.now(datetime.timezone.utc)
|
||||||
|
lines = []
|
||||||
|
for uid, remaining in jailed:
|
||||||
|
ts = int((now + remaining).timestamp())
|
||||||
|
member = interaction.guild.get_member(uid) if interaction.guild else None
|
||||||
|
mention = member.mention if member else f"<@{uid}>"
|
||||||
|
lines.append(S.JAILED_UI["entry"].format(mention=mention, ts=ts))
|
||||||
|
|
||||||
|
plural = "" if len(jailed) == 1 else "i"
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.JAILED_UI["title"],
|
||||||
|
description="\n".join(lines),
|
||||||
|
color=0xED4245,
|
||||||
|
)
|
||||||
|
embed.set_footer(text=S.JAILED_UI["footer"].format(count=len(jailed), plural=plural))
|
||||||
|
await interaction.followup.send(embed=embed)
|
||||||
|
|
||||||
|
@tree.command(name="rank", description=S.CMD["rank"])
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["rank_kasutaja"])
|
||||||
|
async def cmd_rank(interaction: discord.Interaction, kasutaja: discord.Member | None = None):
|
||||||
|
target = kasutaja or interaction.user
|
||||||
|
data = await economy.get_user(target.id)
|
||||||
|
exp = data.get("exp", 0)
|
||||||
|
level = economy.get_level(exp)
|
||||||
|
role_name = economy.level_role_name(level)
|
||||||
|
next_level = level + 1
|
||||||
|
exp_this = economy.exp_for_level(level)
|
||||||
|
exp_next = economy.exp_for_level(next_level)
|
||||||
|
progress = exp - exp_this
|
||||||
|
needed = exp_next - exp_this
|
||||||
|
pct = progress / needed if needed > 0 else 1.0
|
||||||
|
filled = int(pct * 12)
|
||||||
|
bar = "█" * filled + "░" * (12 - filled)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.RANK_UI["title"].format(name=target.display_name, level=level),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.add_field(name=S.RANK_UI["field_title"], value=f"**{role_name}**", inline=True)
|
||||||
|
embed.add_field(name=S.RANK_UI["field_exp"], value=str(exp), inline=True)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.RANK_UI["field_progress"].format(next=next_level),
|
||||||
|
value=S.RANK_UI["progress_val"].format(bar=bar, progress=progress, needed=needed),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
p_level = data.get("prestige_level", 0)
|
||||||
|
p_pp = data.get("prestige_points", 0)
|
||||||
|
s_exp = data.get("season_total_exp", 0)
|
||||||
|
if p_level > 0 or s_exp > 0:
|
||||||
|
embed.add_field(
|
||||||
|
name="\u200b",
|
||||||
|
value=(
|
||||||
|
S.PRESTIGE_UI["rank_line"].format(level=p_level, pp=p_pp)
|
||||||
|
+ "\n"
|
||||||
|
+ S.PRESTIGE_UI["rank_season"].format(exp=s_exp)
|
||||||
|
),
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
if level < 10:
|
||||||
|
embed.set_footer(text=S.RANK_UI["footer_t1"])
|
||||||
|
elif level < 20:
|
||||||
|
embed.set_footer(text=S.RANK_UI["footer_t2"])
|
||||||
|
else:
|
||||||
|
embed.set_footer(text=S.RANK_UI["footer_t3"])
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
if not kasutaja and interaction.guild:
|
||||||
|
member = interaction.guild.get_member(target.id)
|
||||||
|
if member:
|
||||||
|
asyncio.create_task(ensure_level_role(member, level))
|
||||||
|
|
||||||
|
@tree.command(name="stats", description=S.CMD["stats"])
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["stats_kasutaja"])
|
||||||
|
async def cmd_stats(interaction: discord.Interaction, kasutaja: discord.Member | None = None):
|
||||||
|
target = kasutaja or interaction.user
|
||||||
|
data = await economy.get_user(target.id)
|
||||||
|
|
||||||
|
def _s(key: str) -> int:
|
||||||
|
return data.get(key, 0)
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"{S.TITLE['stats']} - {target.display_name}",
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["economy_field"],
|
||||||
|
value=S.STATS_UI["economy_val"].format(
|
||||||
|
peak=coin(_s("peak_balance")),
|
||||||
|
earned=coin(_s("lifetime_earned")),
|
||||||
|
lost=coin(_s("lifetime_lost")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["work_field"],
|
||||||
|
value=S.STATS_UI["work_val"].format(work=_s("work_count"), beg=_s("beg_count")),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(name="\u200b", value="\u200b", inline=False)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["gamble_field"],
|
||||||
|
value=S.STATS_UI["gamble_val"].format(
|
||||||
|
wagered=coin(_s("total_wagered")),
|
||||||
|
win=coin(_s("biggest_win")),
|
||||||
|
loss=coin(_s("biggest_loss")),
|
||||||
|
jackpots=_s("slots_jackpots"),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["crime_field"],
|
||||||
|
value=S.STATS_UI["crime_val"].format(
|
||||||
|
crimes=_s("crimes_attempted"),
|
||||||
|
succeeded=_s("crimes_succeeded"),
|
||||||
|
heists=_s("heists_joined"),
|
||||||
|
heists_won=_s("heists_won"),
|
||||||
|
jailed=_s("times_jailed"),
|
||||||
|
bail=coin(_s("total_bail_paid")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(name="\u200b", value="\u200b", inline=False)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["social_field"],
|
||||||
|
value=S.STATS_UI["social_val"].format(
|
||||||
|
given=coin(_s("total_given")),
|
||||||
|
received=coin(_s("total_received")),
|
||||||
|
),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATS_UI["records_field"],
|
||||||
|
value=S.STATS_UI["records_val"].format(streak=_s("best_daily_streak")),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
216
economy_support_commands.py
Normal file
216
economy_support_commands.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_support_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
parse_amount: Callable[[str, int], tuple[int | None, str | None]],
|
||||||
|
coin: Callable[[int], str],
|
||||||
|
cancel_reminder_task: Callable[[int, str], None],
|
||||||
|
) -> None:
|
||||||
|
class FundModal(discord.ui.Modal):
|
||||||
|
summa = discord.ui.TextInput(
|
||||||
|
label=S.REQUEST_UI["modal_label"],
|
||||||
|
min_length=1,
|
||||||
|
max_length=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, view: "RequestView"):
|
||||||
|
super().__init__(title=S.REQUEST_UI["modal_title"])
|
||||||
|
self._view = view
|
||||||
|
self.summa.placeholder = f"1 - {view.remaining}"
|
||||||
|
|
||||||
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
|
amount, err = parse_amount(self.summa.value, 0)
|
||||||
|
if err or amount is None:
|
||||||
|
await interaction.response.send_message(S.ERR["invalid_amount"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if amount <= 0 or amount > self._view.remaining:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["fund_range"].format(max=self._view.remaining), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
res = await economy.do_give(interaction.user.id, self._view.requester.id, amount)
|
||||||
|
if not res["ok"]:
|
||||||
|
data = await economy.get_user(interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["broke"].format(bal=coin(data["balance"])), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._view.remaining -= amount
|
||||||
|
funded_line = S.REQUEST_UI["funded_line"].format(
|
||||||
|
name=interaction.user.display_name,
|
||||||
|
amount=coin(amount),
|
||||||
|
)
|
||||||
|
if self._view.remaining <= 0:
|
||||||
|
self._view.fund_btn.disabled = True
|
||||||
|
self._view.fund_btn.label = S.REQUEST_UI["btn_funded"]
|
||||||
|
self._view.fund_btn.style = discord.ButtonStyle.secondary
|
||||||
|
self._view.stop()
|
||||||
|
funded_line += S.REQUEST_UI["funded_full"]
|
||||||
|
else:
|
||||||
|
self._view.fund_btn.label = S.REQUEST_UI["btn_fund_remaining"].format(
|
||||||
|
remaining=self._view.remaining
|
||||||
|
)
|
||||||
|
funded_line += S.REQUEST_UI["funded_partial"].format(
|
||||||
|
remaining=coin(self._view.remaining)
|
||||||
|
)
|
||||||
|
|
||||||
|
await interaction.response.send_message(funded_line)
|
||||||
|
if self._view.message:
|
||||||
|
await self._view.message.edit(view=self._view)
|
||||||
|
|
||||||
|
class RequestView(discord.ui.View):
|
||||||
|
def __init__(self, requester: discord.Member, amount: int, target: discord.Member | None):
|
||||||
|
super().__init__(timeout=300)
|
||||||
|
self.requester = requester
|
||||||
|
self.remaining = amount
|
||||||
|
self.target = target
|
||||||
|
self.message: discord.Message | None = None
|
||||||
|
self.fund_btn = discord.ui.Button(
|
||||||
|
label=S.REQUEST_UI["btn_fund"],
|
||||||
|
style=discord.ButtonStyle.success,
|
||||||
|
)
|
||||||
|
self.fund_btn.callback = self._fund
|
||||||
|
self.add_item(self.fund_btn)
|
||||||
|
|
||||||
|
async def _fund(self, interaction: discord.Interaction):
|
||||||
|
if interaction.user.id == self.requester.id:
|
||||||
|
await interaction.response.send_message(S.ERR["request_self_fund"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if self.target and interaction.user.id != self.target.id:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["request_targeted"].format(name=self.target.display_name),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await interaction.response.send_modal(FundModal(self))
|
||||||
|
|
||||||
|
async def on_timeout(self):
|
||||||
|
for item in self.children:
|
||||||
|
item.disabled = True
|
||||||
|
|
||||||
|
max_request = 1_000_000
|
||||||
|
|
||||||
|
@tree.command(name="request", description=S.CMD["request"])
|
||||||
|
@app_commands.describe(
|
||||||
|
summa=S.OPT["request_summa"],
|
||||||
|
põhjus=S.OPT["request_põhjus"],
|
||||||
|
sihtmärk=S.OPT["request_sihtmärk"],
|
||||||
|
)
|
||||||
|
async def cmd_request(
|
||||||
|
interaction: discord.Interaction,
|
||||||
|
summa: str,
|
||||||
|
põhjus: str,
|
||||||
|
sihtmärk: discord.Member | None = None,
|
||||||
|
):
|
||||||
|
summa_int, err = parse_amount(summa, 0)
|
||||||
|
if err or summa_int is None:
|
||||||
|
await interaction.response.send_message(err or S.ERR["invalid_amount"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if summa_int <= 0:
|
||||||
|
await interaction.response.send_message(S.ERR["positive_amount"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if summa_int > max_request:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["fund_range"].format(max=coin(max_request)),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
summa = summa_int
|
||||||
|
if sihtmärk and sihtmärk.id == interaction.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["request_self"], ephemeral=True)
|
||||||
|
return
|
||||||
|
if sihtmärk and sihtmärk.bot:
|
||||||
|
await interaction.response.send_message(S.ERR["request_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
audience = (
|
||||||
|
S.REQUEST_UI["audience_targeted"].format(name=sihtmärk.display_name)
|
||||||
|
if sihtmärk
|
||||||
|
else S.REQUEST_UI["audience_all"]
|
||||||
|
)
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["request"],
|
||||||
|
description=S.REQUEST_UI["desc"].format(
|
||||||
|
requester=interaction.user.display_name,
|
||||||
|
amount=coin(summa),
|
||||||
|
reason=põhjus,
|
||||||
|
audience=audience,
|
||||||
|
),
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
embed.set_footer(text=S.REQUEST_UI["footer"])
|
||||||
|
view = RequestView(interaction.user, summa, sihtmärk)
|
||||||
|
await interaction.response.send_message(embed=embed, view=view)
|
||||||
|
view.message = await interaction.original_response()
|
||||||
|
|
||||||
|
class RemindersSelect(discord.ui.Select):
|
||||||
|
def __init__(self, user_id: int, current: list[str]):
|
||||||
|
self.user_id = user_id
|
||||||
|
options = [
|
||||||
|
discord.SelectOption(
|
||||||
|
label=label,
|
||||||
|
description=desc,
|
||||||
|
value=cmd,
|
||||||
|
default=cmd in current,
|
||||||
|
)
|
||||||
|
for cmd, label, desc in S.REMINDER_OPTS
|
||||||
|
]
|
||||||
|
super().__init__(
|
||||||
|
placeholder=S.REMINDERS_UI["select_placeholder"],
|
||||||
|
options=options,
|
||||||
|
min_values=0,
|
||||||
|
max_values=len(S.REMINDER_OPTS),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def callback(self, interaction: discord.Interaction):
|
||||||
|
if interaction.user.id != self.user_id:
|
||||||
|
await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await economy.do_set_reminders(self.user_id, self.values)
|
||||||
|
enabled = set(self.values)
|
||||||
|
for cmd in [opt[0] for opt in S.REMINDER_OPTS]:
|
||||||
|
if cmd not in enabled:
|
||||||
|
cancel_reminder_task(self.user_id, cmd)
|
||||||
|
if self.values:
|
||||||
|
names = " ".join(f"`/{v}`" for v in self.values)
|
||||||
|
msg = S.REMINDERS_UI["saved_on"].format(names=names)
|
||||||
|
else:
|
||||||
|
msg = S.REMINDERS_UI["saved_off"]
|
||||||
|
await interaction.response.send_message(msg, ephemeral=True)
|
||||||
|
|
||||||
|
class RemindersView(discord.ui.View):
|
||||||
|
def __init__(self, user_id: int, current: list[str]):
|
||||||
|
super().__init__(timeout=60)
|
||||||
|
self.add_item(RemindersSelect(user_id, current))
|
||||||
|
|
||||||
|
@tree.command(name="reminders", description=S.CMD["reminders"])
|
||||||
|
async def cmd_reminders(interaction: discord.Interaction):
|
||||||
|
user_data = await economy.get_user(interaction.user.id)
|
||||||
|
current = user_data.get("reminders", [])
|
||||||
|
if current:
|
||||||
|
status = " ".join(f"`/{c}`" for c in current)
|
||||||
|
desc = S.REMINDERS_UI["desc_active"].format(status=status)
|
||||||
|
else:
|
||||||
|
desc = S.REMINDERS_UI["desc_none"]
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["reminders"],
|
||||||
|
description=desc,
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.set_footer(text=S.REMINDERS_UI["footer"])
|
||||||
|
await interaction.response.send_message(
|
||||||
|
embed=embed,
|
||||||
|
view=RemindersView(interaction.user.id, current),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
134
ops_admin_commands.py
Normal file
134
ops_admin_commands.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_ops_admin_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
process,
|
||||||
|
start_time: datetime.datetime,
|
||||||
|
log_dir: Path,
|
||||||
|
guild_obj: discord.Object,
|
||||||
|
restart_file: Path,
|
||||||
|
get_member_cache_size: Callable[[], int],
|
||||||
|
get_paused: Callable[[], bool],
|
||||||
|
set_paused: Callable[[bool], None],
|
||||||
|
count_economy_users: Callable[[], Awaitable[int]],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="status", description=S.CMD["status"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_status(interaction: discord.Interaction):
|
||||||
|
mem = process.memory_info()
|
||||||
|
cpu = process.cpu_percent(interval=0.1)
|
||||||
|
uptime = datetime.datetime.now() - start_time
|
||||||
|
hours, rem = divmod(int(uptime.total_seconds()), 3600)
|
||||||
|
minutes, seconds = divmod(rem, 60)
|
||||||
|
tasks_count = len(asyncio.all_tasks())
|
||||||
|
latency_ms = round(bot.latency * 1000, 1)
|
||||||
|
cache_size = get_member_cache_size()
|
||||||
|
user_count = await count_economy_users()
|
||||||
|
|
||||||
|
embed = discord.Embed(title=S.STATUS_UI["title"], color=0x57F287)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["uptime_field"],
|
||||||
|
value=S.STATUS_UI["uptime_val"].format(hours=hours, minutes=minutes, seconds=seconds),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["latency_field"],
|
||||||
|
value=S.STATUS_UI["latency_val"].format(ms=latency_ms),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["ram_field"],
|
||||||
|
value=S.STATUS_UI["ram_val"].format(mb=f"{mem.rss / 1024**2:.1f}"),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["cpu_field"],
|
||||||
|
value=S.STATUS_UI["cpu_val"].format(percent=f"{cpu:.1f}"),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["tasks_field"],
|
||||||
|
value=str(tasks_count),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["eco_players_field"],
|
||||||
|
value=str(user_count),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["members_cache_field"],
|
||||||
|
value=str(cache_size),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
log_lines = [
|
||||||
|
S.STATUS_UI["log_line"].format(name=p.name, size_kb=f"{p.stat().st_size / 1024:.1f}")
|
||||||
|
for p in sorted(log_dir.glob("*.log*"))
|
||||||
|
if p.is_file()
|
||||||
|
]
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["log_files_field"],
|
||||||
|
value="\n".join(log_lines) or S.STATUS_UI["none"],
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="sync", description=S.CMD["sync"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_sync(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
tree.copy_global_to(guild=guild_obj)
|
||||||
|
await tree.sync(guild=guild_obj)
|
||||||
|
tree.clear_commands(guild=None)
|
||||||
|
await tree.sync()
|
||||||
|
await interaction.followup.send(S.MSG_SYNC_DONE, ephemeral=True)
|
||||||
|
log.info("/sync triggered by %s", interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="restart", description=S.CMD["restart"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_restart(interaction: discord.Interaction):
|
||||||
|
restart_file.write_text(json.dumps({"channel_id": interaction.channel_id}), encoding="utf-8")
|
||||||
|
await interaction.response.send_message(S.MSG_RESTARTING, ephemeral=True)
|
||||||
|
log.info("/restart triggered by %s", interaction.user)
|
||||||
|
subprocess.Popen([sys.executable] + sys.argv, cwd=os.getcwd())
|
||||||
|
await bot.close()
|
||||||
|
|
||||||
|
@tree.command(name="shutdown", description=S.CMD["shutdown"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_shutdown(interaction: discord.Interaction):
|
||||||
|
await interaction.response.send_message(S.MSG_SHUTTING_DOWN, ephemeral=True)
|
||||||
|
log.info("/shutdown triggered by %s", interaction.user)
|
||||||
|
await bot.close()
|
||||||
|
|
||||||
|
@tree.command(name="pause", description=S.CMD["pause"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_pause(interaction: discord.Interaction):
|
||||||
|
paused = not get_paused()
|
||||||
|
set_paused(paused)
|
||||||
|
msg = S.MSG_PAUSED if paused else S.MSG_UNPAUSED
|
||||||
|
log.info("/pause toggled → %s by %s", "PAUSED" if paused else "UNPAUSED", interaction.user)
|
||||||
|
await interaction.response.send_message(msg, ephemeral=True)
|
||||||
135
ops_channel_commands.py
Normal file
135
ops_channel_commands.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_ops_channel_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
get_allowed_channels: Callable[[], list[int]],
|
||||||
|
set_allowed_channels: Callable[[list[int]], None],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="send", description=S.CMD["send"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kanal=S.OPT["send_kanal"],
|
||||||
|
sõnum=S.OPT["send_sõnum"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_send(interaction: discord.Interaction, kanal: discord.TextChannel, sõnum: str):
|
||||||
|
try:
|
||||||
|
await kanal.send(sõnum)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.SEND_UI["sent"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.SEND_UI["forbidden"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["send_failed"].format(error=e), ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="economysetup", description=S.CMD["economysetup"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_economysetup(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
guild = interaction.guild
|
||||||
|
bot_member = guild.get_member(bot.user.id)
|
||||||
|
bot_top_pos = max((r.position for r in bot_member.roles if r.managed), default=1)
|
||||||
|
|
||||||
|
all_role_names = [economy.ECONOMY_ROLE] + [name for _, name in economy.LEVEL_ROLES]
|
||||||
|
|
||||||
|
created, existing = [], []
|
||||||
|
for name in all_role_names:
|
||||||
|
role = discord.utils.find(lambda r, n=name: r.name == n, guild.roles)
|
||||||
|
if role is None:
|
||||||
|
await guild.create_role(name=name, reason="/economysetup")
|
||||||
|
created.append(name)
|
||||||
|
else:
|
||||||
|
existing.append(name)
|
||||||
|
|
||||||
|
positions: dict[discord.Role, int] = {}
|
||||||
|
base = max(bot_top_pos - 1, 1)
|
||||||
|
for i, name in enumerate(all_role_names):
|
||||||
|
role = discord.utils.find(lambda r, n=name: r.name == n, guild.roles)
|
||||||
|
if role:
|
||||||
|
positions[role] = max(base - i, 1)
|
||||||
|
if positions:
|
||||||
|
try:
|
||||||
|
await guild.edit_role_positions(positions=positions)
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
|
embed = discord.Embed(title=S.TITLE["economysetup"], color=0x57F287)
|
||||||
|
if created:
|
||||||
|
embed.add_field(name=S.ECONOMYSETUP_UI["created_field"], value="\n".join(created), inline=True)
|
||||||
|
if existing:
|
||||||
|
embed.add_field(name=S.ECONOMYSETUP_UI["existing_field"], value="\n".join(existing), inline=True)
|
||||||
|
embed.set_footer(text=S.ECONOMYSETUP_UI["footer"])
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
log.info("/economysetup triggered by %s", interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="allowchannel", description=S.CMD["allowchannel"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kanal=S.OPT["allowchannel_kanal"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_allowchannel(interaction: discord.Interaction, kanal: discord.TextChannel):
|
||||||
|
allowed = get_allowed_channels()
|
||||||
|
if kanal.id in allowed:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["already_allowed"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
allowed.append(kanal.id)
|
||||||
|
set_allowed_channels(allowed)
|
||||||
|
log.info("ALLOWCHANNEL +%s by %s", kanal, interaction.user)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["added"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="denychannel", description=S.CMD["denychannel"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kanal=S.OPT["denychannel_kanal"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_denychannel(interaction: discord.Interaction, kanal: discord.TextChannel):
|
||||||
|
allowed = get_allowed_channels()
|
||||||
|
if kanal.id not in allowed:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["not_in_list"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
allowed.remove(kanal.id)
|
||||||
|
set_allowed_channels(allowed)
|
||||||
|
log.info("DENYCHANNEL -%s by %s", kanal, interaction.user)
|
||||||
|
if allowed:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["removed"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["removed_last"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="channels", description=S.CMD["channels"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_channels(interaction: discord.Interaction):
|
||||||
|
allowed = get_allowed_channels()
|
||||||
|
if not allowed:
|
||||||
|
desc = S.CHANNEL_UI["list_empty"]
|
||||||
|
else:
|
||||||
|
lines = "\n".join(f"• <#{cid}>" for cid in allowed)
|
||||||
|
desc = S.CHANNEL_UI["list_filled"].format(lines=lines)
|
||||||
|
embed = discord.Embed(title=S.CHANNEL_UI["list_title"], description=desc, color=0x5865F2)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
12
pb_client.py
12
pb_client.py
@@ -7,24 +7,26 @@ Environment variables (set in .env):
|
|||||||
PB_URL Base URL of PocketBase (default: http://127.0.0.1:8090)
|
PB_URL Base URL of PocketBase (default: http://127.0.0.1:8090)
|
||||||
PB_ADMIN_EMAIL PocketBase admin e-mail
|
PB_ADMIN_EMAIL PocketBase admin e-mail
|
||||||
PB_ADMIN_PASSWORD PocketBase admin password
|
PB_ADMIN_PASSWORD PocketBase admin password
|
||||||
|
PB_ECONOMY_COLLECTION_DEV / PB_ECONOMY_COLLECTION_ECONOMY
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
_log = logging.getLogger("tipiCOIN.pb")
|
_log = logging.getLogger("tipiCOIN.pb")
|
||||||
|
|
||||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
PB_URL = config.PB_URL
|
||||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
||||||
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||||
ECONOMY_COLLECTION = "economy_users"
|
ECONOMY_COLLECTION = config.PB_ECONOMY_COLLECTION
|
||||||
|
|
||||||
_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ Requirements:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -21,10 +20,12 @@ from dotenv import load_dotenv
|
|||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
import config # noqa: E402
|
||||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
|
||||||
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
PB_URL = config.PB_URL
|
||||||
COLLECTION = "economy_users"
|
PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
||||||
|
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||||
|
COLLECTION = config.PB_ECONOMY_COLLECTION
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# New fields to add
|
# New fields to add
|
||||||
|
|||||||
180
scripts/reset_pb_collections.py
Normal file
180
scripts/reset_pb_collections.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
"""Destructively recreate economy PocketBase collections for dev + economy profiles.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/reset_pb_collections.py --confirm
|
||||||
|
|
||||||
|
This will DELETE and recreate the collections configured by:
|
||||||
|
- PB_ECONOMY_COLLECTION_DEV
|
||||||
|
- PB_ECONOMY_COLLECTION_ECONOMY
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
import config # noqa: E402
|
||||||
|
|
||||||
|
PB_URL = config.PB_URL
|
||||||
|
PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
||||||
|
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||||
|
|
||||||
|
|
||||||
|
def _text_field(name: str, required: bool = False) -> dict:
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"type": "text",
|
||||||
|
"required": required,
|
||||||
|
"options": {"min": None, "max": None, "pattern": ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _number_field(name: str) -> dict:
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"type": "number",
|
||||||
|
"required": False,
|
||||||
|
"options": {"min": None, "max": None, "noDecimal": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _bool_field(name: str) -> dict:
|
||||||
|
return {"name": name, "type": "bool", "required": False}
|
||||||
|
|
||||||
|
|
||||||
|
def _json_field(name: str) -> dict:
|
||||||
|
return {"name": name, "type": "json", "required": False}
|
||||||
|
|
||||||
|
|
||||||
|
def _collection_payload(name: str) -> dict:
|
||||||
|
fields = [
|
||||||
|
_text_field("user_id", required=True),
|
||||||
|
_number_field("balance"),
|
||||||
|
_number_field("exp"),
|
||||||
|
_number_field("daily_streak"),
|
||||||
|
_text_field("last_daily"),
|
||||||
|
_text_field("last_work"),
|
||||||
|
_text_field("last_beg"),
|
||||||
|
_text_field("last_crime"),
|
||||||
|
_text_field("last_rob"),
|
||||||
|
_text_field("last_heist"),
|
||||||
|
_text_field("last_streak_date"),
|
||||||
|
_text_field("jailed_until"),
|
||||||
|
_text_field("last_fish"),
|
||||||
|
_json_field("items"),
|
||||||
|
_json_field("item_uses"),
|
||||||
|
_json_field("reminders"),
|
||||||
|
_json_field("prestige_upgrades"),
|
||||||
|
_json_field("fish_book"),
|
||||||
|
_json_field("fish_inventory"),
|
||||||
|
_bool_field("eco_banned"),
|
||||||
|
_bool_field("jailbreak_used"),
|
||||||
|
_number_field("heist_global_cd_until"),
|
||||||
|
_number_field("peak_balance"),
|
||||||
|
_number_field("lifetime_earned"),
|
||||||
|
_number_field("lifetime_lost"),
|
||||||
|
_number_field("work_count"),
|
||||||
|
_number_field("beg_count"),
|
||||||
|
_number_field("total_wagered"),
|
||||||
|
_number_field("biggest_win"),
|
||||||
|
_number_field("biggest_loss"),
|
||||||
|
_number_field("slots_jackpots"),
|
||||||
|
_number_field("crimes_attempted"),
|
||||||
|
_number_field("crimes_succeeded"),
|
||||||
|
_number_field("times_jailed"),
|
||||||
|
_number_field("total_bail_paid"),
|
||||||
|
_number_field("heists_joined"),
|
||||||
|
_number_field("heists_won"),
|
||||||
|
_number_field("total_given"),
|
||||||
|
_number_field("total_received"),
|
||||||
|
_number_field("best_daily_streak"),
|
||||||
|
_number_field("prestige_level"),
|
||||||
|
_number_field("prestige_points"),
|
||||||
|
_number_field("season_total_exp"),
|
||||||
|
_number_field("total_fish_caught"),
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"type": "base",
|
||||||
|
"fields": fields,
|
||||||
|
"listRule": None,
|
||||||
|
"viewRule": None,
|
||||||
|
"createRule": None,
|
||||||
|
"updateRule": None,
|
||||||
|
"deleteRule": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _auth_token(session: aiohttp.ClientSession) -> str:
|
||||||
|
async with session.post(
|
||||||
|
f"{PB_URL}/api/collections/_superusers/auth-with-password",
|
||||||
|
json={"identity": PB_ADMIN_EMAIL, "password": PB_ADMIN_PASSWORD},
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise RuntimeError(f"Auth failed ({resp.status}): {await resp.text()}")
|
||||||
|
return (await resp.json())["token"]
|
||||||
|
|
||||||
|
|
||||||
|
async def _delete_if_exists(session: aiohttp.ClientSession, headers: dict[str, str], name: str) -> None:
|
||||||
|
async with session.get(f"{PB_URL}/api/collections/{name}", headers=headers) as resp:
|
||||||
|
if resp.status == 404:
|
||||||
|
print(f"[SKIP] {name} does not exist")
|
||||||
|
return
|
||||||
|
if resp.status != 200:
|
||||||
|
raise RuntimeError(f"Could not fetch {name} ({resp.status}): {await resp.text()}")
|
||||||
|
|
||||||
|
async with session.delete(f"{PB_URL}/api/collections/{name}", headers=headers) as resp:
|
||||||
|
if resp.status not in (200, 204):
|
||||||
|
raise RuntimeError(f"Delete failed for {name} ({resp.status}): {await resp.text()}")
|
||||||
|
print(f"[DELETE] {name}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_collection(session: aiohttp.ClientSession, headers: dict[str, str], name: str) -> None:
|
||||||
|
payload = _collection_payload(name)
|
||||||
|
async with session.post(f"{PB_URL}/api/collections", json=payload, headers=headers) as resp:
|
||||||
|
if resp.status not in (200, 201):
|
||||||
|
raise RuntimeError(f"Create failed for {name} ({resp.status}): {await resp.text()}")
|
||||||
|
print(f"[CREATE] {name}")
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--confirm", action="store_true", help="Required flag to run destructive reset")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.confirm:
|
||||||
|
raise SystemExit("Refusing to run without --confirm (this operation deletes collections).")
|
||||||
|
|
||||||
|
targets = []
|
||||||
|
for name in [config.PB_ECONOMY_COLLECTION_DEV, config.PB_ECONOMY_COLLECTION_ECONOMY]:
|
||||||
|
if name and name not in targets:
|
||||||
|
targets.append(name)
|
||||||
|
|
||||||
|
if not targets:
|
||||||
|
raise SystemExit("No target collections configured.")
|
||||||
|
|
||||||
|
timeout = aiohttp.ClientTimeout(total=20)
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
token = await _auth_token(session)
|
||||||
|
headers = {"Authorization": token}
|
||||||
|
|
||||||
|
for name in targets:
|
||||||
|
await _delete_if_exists(session, headers, name)
|
||||||
|
await _create_collection(session, headers, name)
|
||||||
|
|
||||||
|
print("\nDone. Collections recreated:")
|
||||||
|
for name in targets:
|
||||||
|
print(f" - {name}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
33
strings.py
33
strings.py
@@ -858,6 +858,11 @@ BIRTHDAY_UI: dict[str, str] = {
|
|||||||
"footer": "Leht {month}/12 · {month_name}",
|
"footer": "Leht {month}/12 · {month_name}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIRTHDAY_MONTHS: list[str] = [
|
||||||
|
"Jaanuar", "Veebruar", "Märts", "Aprill", "Mai", "Juuni",
|
||||||
|
"Juuli", "August", "September", "Oktoober", "November", "Detsember",
|
||||||
|
]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# /check summary strings
|
# /check summary strings
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -870,6 +875,7 @@ CHECK_UI: dict[str, str] = {
|
|||||||
"stat_uid": "Kasutaja ID",
|
"stat_uid": "Kasutaja ID",
|
||||||
"stat_discord": "Discordi kasutajanimi",
|
"stat_discord": "Discordi kasutajanimi",
|
||||||
"stat_bday": "Sünnipäev",
|
"stat_bday": "Sünnipäev",
|
||||||
|
"no_name": "(no name)",
|
||||||
"done": "**Kontroll lõpetatud!**",
|
"done": "**Kontroll lõpetatud!**",
|
||||||
"already_ok": "✅ Juba korras: {count}",
|
"already_ok": "✅ Juba korras: {count}",
|
||||||
"fixed": "🔧 Parandatud: {count}",
|
"fixed": "🔧 Parandatud: {count}",
|
||||||
@@ -878,9 +884,35 @@ CHECK_UI: dict[str, str] = {
|
|||||||
"errors": "⚠️ Vead: {count}",
|
"errors": "⚠️ Vead: {count}",
|
||||||
"details_header": "**Üksikasjad:**",
|
"details_header": "**Üksikasjad:**",
|
||||||
"details_more": "... ja {count} rohkem",
|
"details_more": "... ja {count} rohkem",
|
||||||
|
"detail_error": "⚠️ {error}",
|
||||||
|
"detail_nickname": "hüüdnimi",
|
||||||
|
"detail_roles_added": "+rollid: {roles}",
|
||||||
|
"detail_changed": "🔧 **{name}**: {parts}",
|
||||||
"ids_filled": "\n🔑 Täideti **{count}** puuduvat kasutaja ID-d.",
|
"ids_filled": "\n🔑 Täideti **{count}** puuduvat kasutaja ID-d.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# /status UI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
STATUS_UI: dict[str, str] = {
|
||||||
|
"title": "🖥️ Boti olek",
|
||||||
|
"uptime_field": "🕐 Uptime",
|
||||||
|
"uptime_val": "{hours}t {minutes}m {seconds}s",
|
||||||
|
"latency_field": "📡 Latency",
|
||||||
|
"latency_val": "{ms} ms",
|
||||||
|
"ram_field": "🧠 RAM (RSS)",
|
||||||
|
"ram_val": "{mb} MB",
|
||||||
|
"cpu_field": "⚙️ CPU",
|
||||||
|
"cpu_val": "{percent}%",
|
||||||
|
"tasks_field": "🔄 Async tasks",
|
||||||
|
"eco_players_field": "👤 Eco players",
|
||||||
|
"members_cache_field": "📋 Liikmed (cache)",
|
||||||
|
"log_files_field": "📂 Log files",
|
||||||
|
"log_line": "`{name}` - {size_kb} KB",
|
||||||
|
"none": "-",
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Admin command responses and DMs
|
# Admin command responses and DMs
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -1089,6 +1121,7 @@ JAILBREAK_UI: dict[str, str] = {
|
|||||||
|
|
||||||
LEADERBOARD_UI: dict[str, str] = {
|
LEADERBOARD_UI: dict[str, str] = {
|
||||||
"house_entry": "🤖 {name} *(maja)* - {balance}",
|
"house_entry": "🤖 {name} *(maja)* - {balance}",
|
||||||
|
"house_default_name": "TipiBOT",
|
||||||
"no_entries": "Keegi ei ole veel punkte teeninud.",
|
"no_entries": "Keegi ei ole veel punkte teeninud.",
|
||||||
"footer": "Lehekülg {page}/{total} · {count} mängijat",
|
"footer": "Lehekülg {page}/{total} · {count} mängijat",
|
||||||
"btn_coins": "🪙 Mündid",
|
"btn_coins": "🪙 Mündid",
|
||||||
|
|||||||
Reference in New Issue
Block a user