Compare commits
4 Commits
rewrite
...
07360d3f11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07360d3f11 | ||
| 8f28832432 | |||
|
|
4d1981420d | ||
|
|
14927b610d |
36
.env.example
36
.env.example
@@ -1,5 +1,12 @@
|
||||
# Discord bot token (from https://discord.com/developers/applications)
|
||||
DISCORD_TOKEN=your-bot-token-here
|
||||
# Bot runtime profile: dev (economy + member tools) or economy (economy-only)
|
||||
BOT_PROFILE=dev
|
||||
|
||||
# Profile-specific Discord bot tokens (from https://discord.com/developers/applications)
|
||||
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)
|
||||
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
|
||||
GOOGLE_CREDS_PATH=credentials.json
|
||||
|
||||
# Guild (server) ID - right-click your server with dev mode on
|
||||
GUILD_ID=your-guild-id-here
|
||||
# Profile-specific guild (server) IDs - right-click your server with dev mode on
|
||||
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)
|
||||
BIRTHDAY_CHANNEL_ID=your-channel-id-here
|
||||
# Legacy fallback guild ID (optional, backward compatibility)
|
||||
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"
|
||||
BIRTHDAY_WINDOW_DAYS=7
|
||||
@@ -20,3 +37,10 @@ BIRTHDAY_WINDOW_DAYS=7
|
||||
PB_URL=http://127.0.0.1:8090
|
||||
PB_ADMIN_EMAIL=admin@example.com
|
||||
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.
|
||||
2. Start PocketBase: `.\pocketbase.exe serve`
|
||||
3. Open the admin UI at http://127.0.0.1:8090/_/ and create a superuser account.
|
||||
4. Create a collection named `economy_users` - see `docs/POCKETBASE_SETUP.md` for the full schema.
|
||||
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`.
|
||||
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 |
|
||||
|---|---|
|
||||
| `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 |
|
||||
| `GOOGLE_CREDS_PATH` | Path to `credentials.json` (default: `credentials.json`) |
|
||||
| `GUILD_ID` | Right-click server → Copy Server ID (Developer Mode on) |
|
||||
| `BIRTHDAY_CHANNEL_ID` | Channel for birthday `@here` pings |
|
||||
| `GUILD_ID_DEV` | Dev bot guild ID |
|
||||
| `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) |
|
||||
| `PB_URL` | PocketBase base URL (default: `http://127.0.0.1:8090`) |
|
||||
| `PB_ADMIN_EMAIL` | PocketBase superuser e-mail |
|
||||
| `PB_ADMIN_PASSWORD` | PocketBase superuser password |
|
||||
| `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
|
||||
|
||||
@@ -110,7 +120,12 @@ pip install -r requirements.txt
|
||||
# Terminal 1 - keep running
|
||||
.\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
|
||||
```
|
||||
|
||||
@@ -146,6 +161,8 @@ Admins (bot lacks permission to modify them) are silently skipped and still mark
|
||||
## Admin Commands
|
||||
|
||||
> 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 |
|
||||
|---|---|---|
|
||||
|
||||
50
config.py
50
config.py
@@ -3,14 +3,58 @@ from dotenv import 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")
|
||||
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"))
|
||||
BASE_ROLE_IDS: list[int] = [1478304631930228779, 1478302278862766190]
|
||||
|
||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
||||
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:
|
||||
|
||||
@@ -51,6 +56,8 @@ Add to your `.env`:
|
||||
PB_URL=http://127.0.0.1:8090
|
||||
PB_ADMIN_EMAIL=your-admin@email.com
|
||||
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,
|
||||
)
|
||||
11386
logs/bot.log
11386
logs/bot.log
File diff suppressed because it is too large
Load Diff
@@ -1,108 +0,0 @@
|
||||
2026-04-01 18:44:29 | WORK user=272518654715887618 earned=+54 lucky=False bal=24077
|
||||
2026-04-01 20:02:11 | BEG user=272518654715887618 earned=+23 jailed=False bal=24100
|
||||
2026-04-01 20:02:53 | ROULETTE_WIN user=178852380018868224 bet=15214708 colour=punane result=punane mult=1 bal=30429416
|
||||
2026-04-01 20:03:42 | ROULETTE_WIN user=178852380018868224 bet=30429416 colour=must result=must mult=1 bal=60858832
|
||||
2026-04-01 20:04:16 | ROULETTE_LOSE user=178852380018868224 bet=60858832 colour=punane result=must mult=1 bal=0
|
||||
2026-04-01 20:06:37 | BEG user=401373976431165449 earned=+52 jailed=False bal=8446
|
||||
2026-04-01 20:06:39 | WORK user=401373976431165449 earned=+60 lucky=False bal=8506
|
||||
2026-04-01 20:06:52 | DAILY user=401373976431165449 earned=+750 streak=1 bal=9256
|
||||
2026-04-01 20:07:07 | DAILY user=272518654715887618 earned=+825 streak=1 bal=24925
|
||||
2026-04-01 20:07:12 | CRIME_WIN user=272518654715887618 earned=+331 bal=25256
|
||||
2026-04-01 20:07:38 | CRIME_WIN user=401373976431165449 earned=+391 bal=9647
|
||||
2026-04-01 20:07:49 | BUY user=401373976431165449 item=echolood cost=-8000 bal=1647
|
||||
2026-04-01 20:09:16 | ROB_BLOCKED robber=824516445382901800 victim=340451525799182357 fine=-118 robber_bal=891 ac_uses_left=1
|
||||
2026-04-01 20:09:23 | ROB_BLOCKED robber=401373976431165449 victim=340451525799182357 fine=-175 robber_bal=1472 ac_uses_left=0
|
||||
2026-04-01 20:09:52 | ROB_WIN robber=178852380018868224 victim=340451525799182357 stolen=+34868 jackpot=False robber_bal=34868 victim_bal=140238
|
||||
2026-04-01 20:10:48 | DAILY user=367347301322326016 earned=+712 streak=1 bal=8462
|
||||
2026-04-01 20:10:55 | WORK user=367347301322326016 earned=+25 lucky=False bal=8487
|
||||
2026-04-01 20:11:00 | BEG user=367347301322326016 earned=+15 jailed=False bal=8502
|
||||
2026-04-01 20:11:37 | ROB_FAIL robber=272518654715887618 victim=340451525799182357 fine=-140 robber_bal=25116
|
||||
2026-04-01 20:15:19 | HEIST_FAIL user=178852380018868224 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=33868
|
||||
2026-04-01 20:15:19 | HEIST_FAIL user=340451525799182357 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=139238
|
||||
2026-04-01 20:15:19 | HEIST_FAIL user=272518654715887618 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=24116
|
||||
2026-04-01 20:15:19 | HEIST_FAIL user=209554152584380420 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=20112
|
||||
2026-04-01 20:15:19 | HEIST_FAIL user=401373976431165449 fine=-220 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=1252
|
||||
2026-04-01 20:15:19 | HEIST_FAIL user=344531774518591498 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=112701
|
||||
2026-04-01 20:15:19 | HEIST_FAIL user=367347301322326016 fine=-1000 jailed_until=2026-04-01T18:45:19.705842+00:00 bal=7502
|
||||
2026-04-01 20:15:45 | JAIL_FREE user=272518654715887618 method=doubles
|
||||
2026-04-01 20:20:07 | JAIL_FREE user=344531774518591498 method=doubles
|
||||
2026-04-01 20:20:14 | DAILY user=344531774518591498 earned=+825 streak=1 bal=113526
|
||||
2026-04-01 20:20:16 | WORK user=344531774518591498 earned=+45 lucky=False bal=113571
|
||||
2026-04-01 20:20:19 | WORK user=272518654715887618 earned=+55 lucky=False bal=24171
|
||||
2026-04-01 20:20:19 | BEG user=344531774518591498 earned=+22 jailed=False bal=113593
|
||||
2026-04-01 20:20:36 | BLACKJACK user=272518654715887618 payout=+0 net=-24171 bal=0
|
||||
2026-04-01 20:21:03 | CRIME_FAIL user=344531774518591498 fine=-90 jailed=True bal=113503
|
||||
2026-04-01 20:21:11 | FISH user=272518654715887618 fish=koger weight=590 value=15
|
||||
2026-04-01 20:21:45 | ROB_WIN robber=344531774518591498 victim=340451525799182357 stolen=+15566 jackpot=False robber_bal=129069 victim_bal=123672
|
||||
2026-04-01 20:25:40 | BAIL_PAID user=178852380018868224 fine=-8760 pct=26% bal=25108
|
||||
2026-04-01 20:28:28 | BEG user=178852380018868224 earned=+28 jailed=False bal=25136
|
||||
2026-04-01 20:28:30 | WORK user=178852380018868224 earned=+92 lucky=False bal=25228
|
||||
2026-04-01 20:28:33 | DAILY user=178852380018868224 earned=+825 streak=1 bal=26053
|
||||
2026-04-01 20:28:38 | CRIME_WIN user=178852380018868224 earned=+640 bal=26693
|
||||
2026-04-01 20:35:35 | BEG user=401373976431165449 earned=+56 jailed=True bal=1308
|
||||
2026-04-01 20:36:20 | JAIL_FREE user=401373976431165449 method=doubles
|
||||
2026-04-01 20:37:47 | FISH user=401373976431165449 fish=angerjas weight=989 value=79
|
||||
2026-04-01 20:46:02 | DAILY user=338622999127261185 earned=+300 streak=1 bal=300
|
||||
2026-04-01 20:56:03 | BEG user=344531774518591498 earned=+60 jailed=False bal=129129
|
||||
2026-04-01 20:56:29 | ROULETTE_LOSE user=344531774518591498 bet=1000 colour=punane result=must mult=1 bal=128129
|
||||
2026-04-01 21:01:23 | BEG user=272518654715887618 earned=+33 jailed=False bal=33
|
||||
2026-04-01 21:01:42 | FISH user=272518654715887618 fish=sarj weight=151 value=6
|
||||
2026-04-01 21:02:38 | FISH user=344531774518591498 fish=viidikas weight=98 value=6
|
||||
2026-04-01 21:02:57 | FISH_SELL user=344531774518591498 count=2 coins=+13 bal=128142
|
||||
2026-04-01 21:03:10 | BEG user=401373976431165449 earned=+54 jailed=False bal=1362
|
||||
2026-04-01 21:03:31 | FISH user=401373976431165449 fish=siig weight=584 value=63
|
||||
2026-04-01 21:05:19 | WORK user=401373976431165449 earned=+131 lucky=False bal=1493
|
||||
2026-04-01 21:05:38 | FISH user=401373976431165449 fish=siig weight=1624 value=112
|
||||
2026-04-01 21:05:48 | FISH_SELL user=401373976431165449 count=3 coins=+254 bal=1747
|
||||
2026-04-01 21:08:31 | BEG user=401373976431165449 earned=+32 jailed=False bal=1779
|
||||
2026-04-01 21:08:45 | FISH user=401373976431165449 fish=tougjas weight=3051 value=207
|
||||
2026-04-01 21:09:01 | ROULETTE_WIN user=401373976431165449 bet=1000 colour=punane result=punane mult=1 bal=2779
|
||||
2026-04-01 21:09:35 | BLACKJACK user=401373976431165449 payout=+3000 net=+1500 bal=4279
|
||||
2026-04-01 21:09:35 | ROULETTE_LOSE user=344531774518591498 bet=1000 colour=punane result=must mult=1 bal=127142
|
||||
2026-04-01 21:10:05 | ROULETTE_WIN user=401373976431165449 bet=1000 colour=punane result=punane mult=1 bal=5279
|
||||
2026-04-01 21:10:46 | ROULETTE_WIN user=344531774518591498 bet=1000 colour=punane result=punane mult=1 bal=128142
|
||||
2026-04-01 21:11:03 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=must result=punane mult=1 bal=4279
|
||||
2026-04-01 21:11:24 | ROULETTE_LOSE user=344531774518591498 bet=1000 colour=punane result=roheline mult=1 bal=127142
|
||||
2026-04-01 21:15:50 | WORK user=338622999127261185 earned=+15 lucky=False bal=315
|
||||
2026-04-01 21:15:54 | CRIME_WIN user=338622999127261185 earned=+453 bal=768
|
||||
2026-04-01 21:16:00 | BEG user=338622999127261185 earned=+20 jailed=False bal=788
|
||||
2026-04-01 21:16:13 | FISH user=338622999127261185 fish=ahven weight=422 value=14
|
||||
2026-04-01 21:18:36 | BEG user=401373976431165449 earned=+20 jailed=False bal=4299
|
||||
2026-04-01 21:18:52 | FISH user=401373976431165449 fish=karpkala weight=1920 value=47
|
||||
2026-04-01 21:20:58 | SLOTS_TRIPLE user=344531774518591498 bet=1000 change=4000 bal=131142
|
||||
2026-04-01 21:21:39 | SLOTS_MISS user=344531774518591498 bet=1000 change=-1000 bal=130142
|
||||
2026-04-01 21:28:25 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=3299
|
||||
2026-04-01 21:29:10 | SLOTS_PAIR user=344531774518591498 bet=1000 change=500 bal=130642
|
||||
2026-04-01 21:29:28 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=2299
|
||||
2026-04-01 21:30:49 | SLOTS_PAIR user=344531774518591498 bet=1000 change=500 bal=131142
|
||||
2026-04-01 21:31:30 | SLOTS_PAIR user=344531774518591498 bet=1000 change=500 bal=131642
|
||||
2026-04-01 21:31:33 | ROULETTE_WIN user=401373976431165449 bet=1000 colour=punane result=punane mult=1 bal=3299
|
||||
2026-04-01 21:31:37 | BEG user=401373976431165449 earned=+68 jailed=False bal=3367
|
||||
2026-04-01 21:32:05 | FISH user=401373976431165449 fish=latikas weight=2351 value=66
|
||||
2026-04-01 21:33:14 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=2367
|
||||
2026-04-01 21:35:09 | SLOTS_MISS user=344531774518591498 bet=1000 change=-1000 bal=130642
|
||||
2026-04-01 21:35:14 | ROULETTE_LOSE user=401373976431165449 bet=1000 colour=punane result=must mult=1 bal=1367
|
||||
2026-04-01 21:48:35 | ROB_FAIL robber=338622999127261185 victim=340451525799182357 fine=-237 robber_bal=551
|
||||
2026-04-01 21:49:59 | BEG user=401373976431165449 earned=+56 jailed=False bal=1423
|
||||
2026-04-01 21:50:15 | FISH user=401373976431165449 fish=vimb weight=856 value=462
|
||||
2026-04-01 22:11:02 | BEG user=401373976431165449 earned=+52 jailed=False bal=1475
|
||||
2026-04-01 22:11:13 | WORK user=401373976431165449 earned=+73 lucky=False bal=1548
|
||||
2026-04-01 22:11:27 | ROB_WIN robber=401373976431165449 victim=367347301322326016 stolen=+1818 jackpot=False robber_bal=3366 victim_bal=5684
|
||||
2026-04-01 22:11:44 | FISH user=401373976431165449 fish=lohe weight=2973 value=313
|
||||
2026-04-01 22:13:30 | ROULETTE_LOSE user=401373976431165449 bet=2000 colour=punane result=roheline mult=1 bal=1366
|
||||
2026-04-01 22:14:33 | ROULETTE_LOSE user=401373976431165449 bet=1366 colour=punane result=must mult=1 bal=0
|
||||
2026-04-01 22:40:19 | WORK user=367347301322326016 earned=+82 lucky=False bal=5766
|
||||
2026-04-01 22:40:23 | CRIME_FAIL user=367347301322326016 fine=-100 jailed=True bal=5666
|
||||
2026-04-01 22:46:27 | WORK user=344531774518591498 earned=+116 lucky=False bal=130758
|
||||
2026-04-01 22:46:30 | BEG user=344531774518591498 earned=+58 jailed=False bal=130816
|
||||
2026-04-01 22:46:35 | CRIME_WIN user=344531774518591498 earned=+419 bal=131235
|
||||
2026-04-01 22:46:44 | ROB_FAIL robber=344531774518591498 victim=340451525799182357 fine=-246 robber_bal=130989
|
||||
2026-04-01 22:47:01 | FISH user=344531774518591498 fish=viidikas weight=80 value=5
|
||||
2026-04-01 22:48:58 | WORK user=178852380018868224 earned=+106 lucky=True bal=26799
|
||||
2026-04-01 22:49:03 | CRIME_WIN user=178852380018868224 earned=+486 bal=27285
|
||||
2026-04-01 22:49:05 | BEG user=178852380018868224 earned=+76 jailed=False bal=27361
|
||||
2026-04-01 22:52:25 | BEG user=401373976431165449 earned=+44 jailed=False bal=44
|
||||
2026-04-01 22:52:29 | WORK user=401373976431165449 earned=+103 lucky=False bal=147
|
||||
2026-04-01 22:52:39 | CRIME_FAIL user=401373976431165449 fine=-125 jailed=False bal=22
|
||||
2026-04-01 22:53:01 | FISH user=401373976431165449 fish=latikas weight=1217 value=40
|
||||
2026-04-01 22:57:11 | SLOTS_PAIR user=344531774518591498 bet=10000 change=5000 bal=135989
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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_ADMIN_EMAIL PocketBase admin e-mail
|
||||
PB_ADMIN_PASSWORD PocketBase admin password
|
||||
PB_ECONOMY_COLLECTION_DEV / PB_ECONOMY_COLLECTION_ECONOMY
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
import config
|
||||
|
||||
_log = logging.getLogger("tipiCOIN.pb")
|
||||
|
||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
||||
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
||||
ECONOMY_COLLECTION = "economy_users"
|
||||
PB_URL = config.PB_URL
|
||||
PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
||||
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||
ECONOMY_COLLECTION = config.PB_ECONOMY_COLLECTION
|
||||
|
||||
_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ Requirements:
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
@@ -21,10 +20,12 @@ from dotenv import load_dotenv
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
load_dotenv()
|
||||
|
||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
||||
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
||||
COLLECTION = "economy_users"
|
||||
import config # noqa: E402
|
||||
|
||||
PB_URL = config.PB_URL
|
||||
PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
||||
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||
COLLECTION = config.PB_ECONOMY_COLLECTION
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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}",
|
||||
}
|
||||
|
||||
BIRTHDAY_MONTHS: list[str] = [
|
||||
"Jaanuar", "Veebruar", "Märts", "Aprill", "Mai", "Juuni",
|
||||
"Juuli", "August", "September", "Oktoober", "November", "Detsember",
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /check summary strings
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -870,6 +875,7 @@ CHECK_UI: dict[str, str] = {
|
||||
"stat_uid": "Kasutaja ID",
|
||||
"stat_discord": "Discordi kasutajanimi",
|
||||
"stat_bday": "Sünnipäev",
|
||||
"no_name": "(no name)",
|
||||
"done": "**Kontroll lõpetatud!**",
|
||||
"already_ok": "✅ Juba korras: {count}",
|
||||
"fixed": "🔧 Parandatud: {count}",
|
||||
@@ -878,9 +884,35 @@ CHECK_UI: dict[str, str] = {
|
||||
"errors": "⚠️ Vead: {count}",
|
||||
"details_header": "**Üksikasjad:**",
|
||||
"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.",
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1089,6 +1121,7 @@ JAILBREAK_UI: dict[str, str] = {
|
||||
|
||||
LEADERBOARD_UI: dict[str, str] = {
|
||||
"house_entry": "🤖 {name} *(maja)* - {balance}",
|
||||
"house_default_name": "TipiBOT",
|
||||
"no_entries": "Keegi ei ole veel punkte teeninud.",
|
||||
"footer": "Lehekülg {page}/{total} · {count} mängijat",
|
||||
"btn_coins": "🪙 Mündid",
|
||||
|
||||
Reference in New Issue
Block a user