forked from sass/tipibot
Discomboluating
This commit is contained in:
36
.env.example
36
.env.example
@@ -1,5 +1,12 @@
|
|||||||
# Discord bot token (from https://discord.com/developers/applications)
|
# Bot runtime profile: dev (economy + member tools) or economy (economy-only)
|
||||||
DISCORD_TOKEN=your-bot-token-here
|
BOT_PROFILE=dev
|
||||||
|
|
||||||
|
# Profile-specific Discord bot tokens (from https://discord.com/developers/applications)
|
||||||
|
DISCORD_TOKEN_DEV=your-dev-bot-token-here
|
||||||
|
DISCORD_TOKEN_ECONOMY=your-economy-bot-token-here
|
||||||
|
|
||||||
|
# Legacy fallback token (optional, backward compatibility)
|
||||||
|
DISCORD_TOKEN=
|
||||||
|
|
||||||
# Google Sheets spreadsheet ID (the long string in the sheet URL)
|
# Google Sheets spreadsheet ID (the long string in the sheet URL)
|
||||||
SHEET_ID=your-google-sheet-id-here
|
SHEET_ID=your-google-sheet-id-here
|
||||||
@@ -7,11 +14,21 @@ SHEET_ID=your-google-sheet-id-here
|
|||||||
# Path to Google service account credentials JSON
|
# Path to Google service account credentials JSON
|
||||||
GOOGLE_CREDS_PATH=credentials.json
|
GOOGLE_CREDS_PATH=credentials.json
|
||||||
|
|
||||||
# Guild (server) ID - right-click your server with dev mode on
|
# Profile-specific guild (server) IDs - right-click your server with dev mode on
|
||||||
GUILD_ID=your-guild-id-here
|
GUILD_ID_DEV=your-dev-guild-id-here
|
||||||
|
GUILD_ID_ECONOMY=your-economy-guild-id-here
|
||||||
|
|
||||||
# Channel ID where birthday announcements are posted (bot sends @here on the birthday at 09:00 Tallinn time)
|
# Legacy fallback guild ID (optional, backward compatibility)
|
||||||
BIRTHDAY_CHANNEL_ID=your-channel-id-here
|
GUILD_ID=
|
||||||
|
|
||||||
|
# Channel ID where birthday announcements are posted (dev profile)
|
||||||
|
BIRTHDAY_CHANNEL_ID_DEV=your-dev-birthday-channel-id-here
|
||||||
|
|
||||||
|
# Optional birthday channel for economy profile (normally unset for economy-only bot)
|
||||||
|
BIRTHDAY_CHANNEL_ID_ECONOMY=
|
||||||
|
|
||||||
|
# Legacy fallback birthday channel ID (optional, backward compatibility)
|
||||||
|
BIRTHDAY_CHANNEL_ID=
|
||||||
|
|
||||||
# How many days before a birthday the on-join check counts as "coming up"
|
# How many days before a birthday the on-join check counts as "coming up"
|
||||||
BIRTHDAY_WINDOW_DAYS=7
|
BIRTHDAY_WINDOW_DAYS=7
|
||||||
@@ -20,3 +37,10 @@ BIRTHDAY_WINDOW_DAYS=7
|
|||||||
PB_URL=http://127.0.0.1:8090
|
PB_URL=http://127.0.0.1:8090
|
||||||
PB_ADMIN_EMAIL=admin@example.com
|
PB_ADMIN_EMAIL=admin@example.com
|
||||||
PB_ADMIN_PASSWORD=your-pb-admin-password
|
PB_ADMIN_PASSWORD=your-pb-admin-password
|
||||||
|
|
||||||
|
# Profile-specific PocketBase collections
|
||||||
|
PB_ECONOMY_COLLECTION_DEV=economy_users_dev
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY=economy_users_prod
|
||||||
|
|
||||||
|
# Legacy fallback collection name (optional, backward compatibility)
|
||||||
|
PB_ECONOMY_COLLECTION=
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -70,7 +70,7 @@ The economy system stores all player data in [PocketBase](https://pocketbase.io/
|
|||||||
1. Download `pocketbase.exe` (Windows) from https://pocketbase.io/docs/ and place it in the project root.
|
1. Download `pocketbase.exe` (Windows) from https://pocketbase.io/docs/ and place it in the project root.
|
||||||
2. Start PocketBase: `.\pocketbase.exe serve`
|
2. Start PocketBase: `.\pocketbase.exe serve`
|
||||||
3. Open the admin UI at http://127.0.0.1:8090/_/ and create a superuser account.
|
3. Open the admin UI at http://127.0.0.1:8090/_/ and create a superuser account.
|
||||||
4. Create a collection named `economy_users` - see `docs/POCKETBASE_SETUP.md` for the full schema.
|
4. Create two collections for profile separation: `economy_users_dev` and `economy_users_prod` - see `docs/POCKETBASE_SETUP.md` for schema notes.
|
||||||
5. Set `PB_URL`, `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD` in `.env`.
|
5. Set `PB_URL`, `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD` in `.env`.
|
||||||
6. **One-time data migration** (only if you have an existing `data/economy.json`): `python scripts/migrate_to_pb.py`
|
6. **One-time data migration** (only if you have an existing `data/economy.json`): `python scripts/migrate_to_pb.py`
|
||||||
|
|
||||||
@@ -88,15 +88,25 @@ cp .env.example .env
|
|||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `DISCORD_TOKEN` | Bot token from Discord Developer Portal |
|
| `BOT_PROFILE` | Runtime profile: `dev` or `economy` |
|
||||||
|
| `DISCORD_TOKEN_DEV` | Dev bot token from Discord Developer Portal |
|
||||||
|
| `DISCORD_TOKEN_ECONOMY` | Economy bot token from Discord Developer Portal |
|
||||||
|
| `DISCORD_TOKEN` | Legacy fallback token (optional) |
|
||||||
| `SHEET_ID` | ID from the Google Sheet URL |
|
| `SHEET_ID` | ID from the Google Sheet URL |
|
||||||
| `GOOGLE_CREDS_PATH` | Path to `credentials.json` (default: `credentials.json`) |
|
| `GOOGLE_CREDS_PATH` | Path to `credentials.json` (default: `credentials.json`) |
|
||||||
| `GUILD_ID` | Right-click server → Copy Server ID (Developer Mode on) |
|
| `GUILD_ID_DEV` | Dev bot guild ID |
|
||||||
| `BIRTHDAY_CHANNEL_ID` | Channel for birthday `@here` pings |
|
| `GUILD_ID_ECONOMY` | Economy bot guild ID |
|
||||||
|
| `GUILD_ID` | Legacy fallback guild ID (optional) |
|
||||||
|
| `BIRTHDAY_CHANNEL_ID_DEV` | Channel for birthday `@here` pings in dev profile |
|
||||||
|
| `BIRTHDAY_CHANNEL_ID_ECONOMY` | Optional birthday channel in economy profile |
|
||||||
|
| `BIRTHDAY_CHANNEL_ID` | Legacy fallback birthday channel ID (optional) |
|
||||||
| `BIRTHDAY_WINDOW_DAYS` | Days before birthday that on-join check flags it (default: 7) |
|
| `BIRTHDAY_WINDOW_DAYS` | Days before birthday that on-join check flags it (default: 7) |
|
||||||
| `PB_URL` | PocketBase base URL (default: `http://127.0.0.1:8090`) |
|
| `PB_URL` | PocketBase base URL (default: `http://127.0.0.1:8090`) |
|
||||||
| `PB_ADMIN_EMAIL` | PocketBase superuser e-mail |
|
| `PB_ADMIN_EMAIL` | PocketBase superuser e-mail |
|
||||||
| `PB_ADMIN_PASSWORD` | PocketBase superuser password |
|
| `PB_ADMIN_PASSWORD` | PocketBase superuser password |
|
||||||
|
| `PB_ECONOMY_COLLECTION_DEV` | PocketBase collection used by `BOT_PROFILE=dev` |
|
||||||
|
| `PB_ECONOMY_COLLECTION_ECONOMY` | PocketBase collection used by `BOT_PROFILE=economy` |
|
||||||
|
| `PB_ECONOMY_COLLECTION` | Legacy fallback collection (optional) |
|
||||||
|
|
||||||
### 6. Install & Run
|
### 6. Install & Run
|
||||||
|
|
||||||
@@ -110,7 +120,12 @@ pip install -r requirements.txt
|
|||||||
# Terminal 1 - keep running
|
# Terminal 1 - keep running
|
||||||
.\pocketbase.exe serve
|
.\pocketbase.exe serve
|
||||||
|
|
||||||
# Terminal 2
|
# Terminal 2 (dev bot)
|
||||||
|
set BOT_PROFILE=dev
|
||||||
|
python bot.py
|
||||||
|
|
||||||
|
# Terminal 3 (economy bot)
|
||||||
|
set BOT_PROFILE=economy
|
||||||
python bot.py
|
python bot.py
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -146,6 +161,8 @@ Admins (bot lacks permission to modify them) are silently skipped and still mark
|
|||||||
## Admin Commands
|
## Admin Commands
|
||||||
|
|
||||||
> These commands are hidden from users who don't have the required permission. Override per-role in **Server Settings → Integrations → Bot**.
|
> These commands are hidden from users who don't have the required permission. Override per-role in **Server Settings → Integrations → Bot**.
|
||||||
|
>
|
||||||
|
> Profile note: `/check`, `/member`, and `/birthdays` are available only when `BOT_PROFILE=dev`.
|
||||||
|
|
||||||
| Command | Permission | What it does |
|
| Command | Permission | What it does |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|||||||
50
config.py
50
config.py
@@ -3,14 +3,58 @@ from dotenv import load_dotenv
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
|
BOT_PROFILE = os.getenv("BOT_PROFILE", "dev").strip().lower() or "dev"
|
||||||
|
if BOT_PROFILE not in {"dev", "economy"}:
|
||||||
|
raise SystemExit("BOT_PROFILE must be either 'dev' or 'economy'.")
|
||||||
|
|
||||||
|
|
||||||
|
def _env_int(name: str, default: int) -> int:
|
||||||
|
raw = os.getenv(name)
|
||||||
|
if raw is None or not raw.strip():
|
||||||
|
return default
|
||||||
|
return int(raw)
|
||||||
|
|
||||||
|
|
||||||
|
_LEGACY_DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
|
||||||
|
DISCORD_TOKEN_DEV = os.getenv("DISCORD_TOKEN_DEV", "")
|
||||||
|
DISCORD_TOKEN_ECONOMY = os.getenv("DISCORD_TOKEN_ECONOMY", "")
|
||||||
|
DISCORD_TOKEN = (
|
||||||
|
DISCORD_TOKEN_ECONOMY if BOT_PROFILE == "economy" else DISCORD_TOKEN_DEV
|
||||||
|
) or _LEGACY_DISCORD_TOKEN
|
||||||
|
|
||||||
SHEET_ID = os.getenv("SHEET_ID")
|
SHEET_ID = os.getenv("SHEET_ID")
|
||||||
GOOGLE_CREDS_PATH = os.getenv("GOOGLE_CREDS_PATH", "credentials.json")
|
GOOGLE_CREDS_PATH = os.getenv("GOOGLE_CREDS_PATH", "credentials.json")
|
||||||
GUILD_ID = int(os.getenv("GUILD_ID", "0"))
|
|
||||||
BIRTHDAY_CHANNEL_ID = int(os.getenv("BIRTHDAY_CHANNEL_ID", "0"))
|
_LEGACY_GUILD_ID = _env_int("GUILD_ID", 0)
|
||||||
|
GUILD_ID_DEV = _env_int("GUILD_ID_DEV", _LEGACY_GUILD_ID)
|
||||||
|
GUILD_ID_ECONOMY = _env_int("GUILD_ID_ECONOMY", _LEGACY_GUILD_ID)
|
||||||
|
GUILD_ID = GUILD_ID_ECONOMY if BOT_PROFILE == "economy" else GUILD_ID_DEV
|
||||||
|
|
||||||
|
_LEGACY_BIRTHDAY_CHANNEL_ID = _env_int("BIRTHDAY_CHANNEL_ID", 0)
|
||||||
|
BIRTHDAY_CHANNEL_ID_DEV = _env_int("BIRTHDAY_CHANNEL_ID_DEV", _LEGACY_BIRTHDAY_CHANNEL_ID)
|
||||||
|
BIRTHDAY_CHANNEL_ID_ECONOMY = _env_int("BIRTHDAY_CHANNEL_ID_ECONOMY", 0)
|
||||||
|
BIRTHDAY_CHANNEL_ID = (
|
||||||
|
BIRTHDAY_CHANNEL_ID_ECONOMY
|
||||||
|
if BOT_PROFILE == "economy"
|
||||||
|
else BIRTHDAY_CHANNEL_ID_DEV
|
||||||
|
)
|
||||||
|
|
||||||
BIRTHDAY_WINDOW_DAYS = int(os.getenv("BIRTHDAY_WINDOW_DAYS", "7"))
|
BIRTHDAY_WINDOW_DAYS = int(os.getenv("BIRTHDAY_WINDOW_DAYS", "7"))
|
||||||
BASE_ROLE_IDS: list[int] = [1478304631930228779, 1478302278862766190]
|
BASE_ROLE_IDS: list[int] = [1478304631930228779, 1478302278862766190]
|
||||||
|
|
||||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
||||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
||||||
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
||||||
|
|
||||||
|
_LEGACY_PB_COLLECTION = os.getenv("PB_ECONOMY_COLLECTION", "").strip()
|
||||||
|
PB_ECONOMY_COLLECTION_DEV = (
|
||||||
|
os.getenv("PB_ECONOMY_COLLECTION_DEV", "").strip()
|
||||||
|
or (_LEGACY_PB_COLLECTION if _LEGACY_PB_COLLECTION else "economy_users_dev")
|
||||||
|
)
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY = (
|
||||||
|
os.getenv("PB_ECONOMY_COLLECTION_ECONOMY", "").strip()
|
||||||
|
or (_LEGACY_PB_COLLECTION if _LEGACY_PB_COLLECTION else "economy_users_prod")
|
||||||
|
)
|
||||||
|
PB_ECONOMY_COLLECTION = (
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY if BOT_PROFILE == "economy" else PB_ECONOMY_COLLECTION_DEV
|
||||||
|
)
|
||||||
|
|||||||
314
dev_member_commands.py
Normal file
314
dev_member_commands.py
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import sheets
|
||||||
|
import strings as S
|
||||||
|
from member_sync import announce_birthday, sync_member
|
||||||
|
|
||||||
|
|
||||||
|
class BirthdayPages(discord.ui.View):
|
||||||
|
def __init__(self, pages: list[discord.Embed], start: int = 0):
|
||||||
|
super().__init__(timeout=120)
|
||||||
|
self.pages = pages
|
||||||
|
self.current = start
|
||||||
|
self._update_buttons()
|
||||||
|
|
||||||
|
def _update_buttons(self):
|
||||||
|
self.prev_button.disabled = self.current == 0
|
||||||
|
self.next_button.disabled = self.current >= len(self.pages) - 1
|
||||||
|
|
||||||
|
@discord.ui.button(label="◀", style=discord.ButtonStyle.secondary)
|
||||||
|
async def prev_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.current -= 1
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self.pages[self.current], view=self)
|
||||||
|
|
||||||
|
@discord.ui.button(label="▶", style=discord.ButtonStyle.secondary)
|
||||||
|
async def next_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
self.current += 1
|
||||||
|
self._update_buttons()
|
||||||
|
await interaction.response.edit_message(embed=self.pages[self.current], view=self)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_birthday_pages(
|
||||||
|
guild: discord.Guild | None = None,
|
||||||
|
) -> tuple[list[discord.Embed], int]:
|
||||||
|
"""Build 12 monthly embeds (one per calendar month).
|
||||||
|
|
||||||
|
Returns (pages, start_index) where start_index is the current month.
|
||||||
|
"""
|
||||||
|
rows = sheets.get_cache()
|
||||||
|
today = datetime.date.today()
|
||||||
|
|
||||||
|
by_month: dict[int, list[tuple[int, str, int | None]]] = {m: [] for m in range(1, 13)}
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
name = str(row.get("Nimi", "")).strip()
|
||||||
|
bday_str = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
if not name or not bday_str or bday_str.strip().lower() in ("-", "x", "n/a", "none", "ei"):
|
||||||
|
continue
|
||||||
|
bday = None
|
||||||
|
for fmt in ["%d/%m/%Y", "%Y-%m-%d", "%m-%d"]:
|
||||||
|
try:
|
||||||
|
bday = datetime.datetime.strptime(bday_str, fmt).date()
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if bday is None:
|
||||||
|
continue
|
||||||
|
raw_uid = str(row.get("User ID", "")).strip()
|
||||||
|
try:
|
||||||
|
uid = int(raw_uid) if raw_uid and raw_uid not in ("0", "-") else None
|
||||||
|
except ValueError:
|
||||||
|
uid = None
|
||||||
|
by_month[bday.month].append((bday.day, name, uid))
|
||||||
|
|
||||||
|
pages: list[discord.Embed] = []
|
||||||
|
for month in range(1, 13):
|
||||||
|
entries = sorted(by_month[month], key=lambda x: x[0])
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=f"🎂 {S.BIRTHDAY_MONTHS[month - 1]}",
|
||||||
|
color=0xF4A261,
|
||||||
|
)
|
||||||
|
if not entries:
|
||||||
|
embed.description = S.BIRTHDAY_UI["no_entries"]
|
||||||
|
else:
|
||||||
|
lines = []
|
||||||
|
for day, name, uid in entries:
|
||||||
|
try:
|
||||||
|
this_year = datetime.date(today.year, month, day)
|
||||||
|
except ValueError:
|
||||||
|
this_year = datetime.date(today.year, month, day - 1)
|
||||||
|
next_bday = this_year if this_year >= today else this_year.replace(year=today.year + 1)
|
||||||
|
days_until = (next_bday - today).days
|
||||||
|
if days_until == 0:
|
||||||
|
when = S.BIRTHDAY_UI["today"]
|
||||||
|
elif days_until == 1:
|
||||||
|
when = S.BIRTHDAY_UI["tomorrow"]
|
||||||
|
else:
|
||||||
|
when = S.BIRTHDAY_UI["in_days"].format(days=days_until)
|
||||||
|
display = name
|
||||||
|
if guild and uid:
|
||||||
|
member = guild.get_member(uid)
|
||||||
|
if member:
|
||||||
|
display = member.mention
|
||||||
|
lines.append(f"{display} - {day:02d}/{month:02d} · {when}")
|
||||||
|
embed.description = "\n".join(lines)
|
||||||
|
embed.set_footer(text=S.BIRTHDAY_UI["footer"].format(month=month, month_name=S.BIRTHDAY_MONTHS[month - 1]))
|
||||||
|
pages.append(embed)
|
||||||
|
|
||||||
|
return pages, today.month - 1
|
||||||
|
|
||||||
|
|
||||||
|
def _sheet_stats(rows: list[dict]) -> str:
|
||||||
|
"""Return a formatted string with sheet completeness statistics."""
|
||||||
|
total = len(rows)
|
||||||
|
missing_uid = []
|
||||||
|
missing_discord = []
|
||||||
|
missing_birthday = []
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
name = str(row.get("Nimi", "")).strip() or S.CHECK_UI["no_name"]
|
||||||
|
uid = str(row.get("User ID", "")).strip()
|
||||||
|
discord_name = str(row.get("Discord", "")).strip()
|
||||||
|
bday = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
|
||||||
|
if not uid or uid == "0":
|
||||||
|
missing_uid.append(name)
|
||||||
|
if not discord_name:
|
||||||
|
missing_discord.append(name)
|
||||||
|
if not bday:
|
||||||
|
missing_birthday.append(name)
|
||||||
|
|
||||||
|
lines = [S.CHECK_UI["sheet_stats_header"].format(total=total)]
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
def stat_line(label: str, missing: list[str]) -> str:
|
||||||
|
count = len(missing)
|
||||||
|
if count == 0:
|
||||||
|
return S.CHECK_UI["stat_ok"].format(label=label)
|
||||||
|
names = ", ".join(missing[:5])
|
||||||
|
more = S.CHECK_UI["stat_more"].format(count=count - 5) if count > 5 else ""
|
||||||
|
return S.CHECK_UI["stat_warn"].format(label=label, count=count, names=names, more=more)
|
||||||
|
|
||||||
|
lines.append(stat_line(S.CHECK_UI["stat_uid"], missing_uid))
|
||||||
|
lines.append(stat_line(S.CHECK_UI["stat_discord"], missing_discord))
|
||||||
|
lines.append(stat_line(S.CHECK_UI["stat_bday"], missing_birthday))
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def register_dev_member_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
has_announced_today: Callable[[int], bool],
|
||||||
|
mark_announced_today: Callable[[int], None],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="birthdays", description=S.CMD["birthdays"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def cmd_birthdays(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer()
|
||||||
|
|
||||||
|
try:
|
||||||
|
sheets.refresh()
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(S.ERR["sheet_error"].format(error=e), ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
pages, start = _build_birthday_pages(guild=interaction.guild)
|
||||||
|
await interaction.followup.send(embed=pages[start], view=BirthdayPages(pages, start=start))
|
||||||
|
|
||||||
|
@tree.command(name="check", description=S.CMD["check"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_roles=True)
|
||||||
|
async def cmd_check(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
|
||||||
|
guild = interaction.guild
|
||||||
|
if guild is None:
|
||||||
|
await interaction.followup.send(S.ERR["guild_only"], ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = sheets.refresh()
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.followup.send(S.ERR["sheet_error"].format(error=e), ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
ids_filled = 0
|
||||||
|
for row in data:
|
||||||
|
uid = str(row.get("User ID", "")).strip()
|
||||||
|
if uid and uid not in ("0", "-"):
|
||||||
|
continue
|
||||||
|
discord_name = str(row.get("Discord", "")).strip()
|
||||||
|
if not discord_name:
|
||||||
|
continue
|
||||||
|
guild_member = discord.utils.find(
|
||||||
|
lambda m, n=discord_name: m.name.lower() == n.lower(),
|
||||||
|
guild.members,
|
||||||
|
)
|
||||||
|
if guild_member:
|
||||||
|
sheets.set_user_id(discord_name, guild_member.id)
|
||||||
|
ids_filled += 1
|
||||||
|
|
||||||
|
data = sheets.get_cache()
|
||||||
|
|
||||||
|
changed_count = 0
|
||||||
|
not_found = 0
|
||||||
|
already_ok = 0
|
||||||
|
errors_total = 0
|
||||||
|
birthday_pings = 0
|
||||||
|
details: list[str] = []
|
||||||
|
sync_updates: list[tuple[int, bool]] = []
|
||||||
|
|
||||||
|
for member in guild.members:
|
||||||
|
if member.bot:
|
||||||
|
continue
|
||||||
|
|
||||||
|
result = await sync_member(member, guild)
|
||||||
|
|
||||||
|
if result.not_found:
|
||||||
|
not_found += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
sync_updates.append((member.id, result.synced))
|
||||||
|
|
||||||
|
if result.errors:
|
||||||
|
errors_total += len(result.errors)
|
||||||
|
for err in result.errors:
|
||||||
|
details.append(S.CHECK_UI["detail_error"].format(error=err))
|
||||||
|
|
||||||
|
if result.changed:
|
||||||
|
changed_count += 1
|
||||||
|
parts = []
|
||||||
|
if result.nickname_changed:
|
||||||
|
parts.append(S.CHECK_UI["detail_nickname"])
|
||||||
|
if result.roles_added:
|
||||||
|
parts.append(S.CHECK_UI["detail_roles_added"].format(roles=", ".join(result.roles_added)))
|
||||||
|
details.append(S.CHECK_UI["detail_changed"].format(name=member.display_name, parts=", ".join(parts)))
|
||||||
|
else:
|
||||||
|
already_ok += 1
|
||||||
|
|
||||||
|
if result.birthday_soon and not has_announced_today(member.id):
|
||||||
|
birthday_pings += 1
|
||||||
|
await announce_birthday(member, bot)
|
||||||
|
mark_announced_today(member.id)
|
||||||
|
|
||||||
|
if sync_updates:
|
||||||
|
try:
|
||||||
|
sheets.batch_set_synced(sync_updates)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("/check batch_set_synced failed: %s", e)
|
||||||
|
|
||||||
|
summary_lines = [
|
||||||
|
S.CHECK_UI["done"],
|
||||||
|
S.CHECK_UI["already_ok"].format(count=already_ok),
|
||||||
|
S.CHECK_UI["fixed"].format(count=changed_count),
|
||||||
|
S.CHECK_UI["not_found"].format(count=not_found),
|
||||||
|
S.CHECK_UI["bday_pings"].format(count=birthday_pings),
|
||||||
|
]
|
||||||
|
if errors_total:
|
||||||
|
summary_lines.append(S.CHECK_UI["errors"].format(count=errors_total))
|
||||||
|
|
||||||
|
summary = "\n".join(summary_lines)
|
||||||
|
|
||||||
|
if details:
|
||||||
|
detail_text = "\n".join(details[:20])
|
||||||
|
summary += f"\n\n{S.CHECK_UI['details_header']}\n{detail_text}"
|
||||||
|
if len(details) > 20:
|
||||||
|
summary += "\n" + S.CHECK_UI["details_more"].format(count=len(details) - 20)
|
||||||
|
|
||||||
|
stats = _sheet_stats(data)
|
||||||
|
id_note = S.CHECK_UI["ids_filled"].format(count=ids_filled) if ids_filled else ""
|
||||||
|
summary = id_note + "\n" + summary + "\n\n" + stats
|
||||||
|
|
||||||
|
await interaction.followup.send(summary.strip(), ephemeral=True)
|
||||||
|
log.info(
|
||||||
|
"/check - OK=%d, Fixed=%d, NotFound=%d, IDs=%d, Errors=%d",
|
||||||
|
already_ok,
|
||||||
|
changed_count,
|
||||||
|
not_found,
|
||||||
|
ids_filled,
|
||||||
|
errors_total,
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="member", description=S.CMD["member"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_roles=True)
|
||||||
|
async def cmd_member(interaction: discord.Interaction, user: discord.Member):
|
||||||
|
row = sheets.find_member(user.id, user.name)
|
||||||
|
if row is None:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["member_not_found"].format(name=user.display_name),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
embed = discord.Embed(title=S.MEMBER_UI["title"].format(name=user.display_name), color=user.color)
|
||||||
|
|
||||||
|
bday_str = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
if bday_str and bday_str.lower() not in ("-", "x", "n/a", "none", "ei"):
|
||||||
|
for fmt in ["%d/%m/%Y", "%Y-%m-%d"]:
|
||||||
|
try:
|
||||||
|
bday = datetime.datetime.strptime(bday_str, fmt).date()
|
||||||
|
if 1920 <= bday.year <= datetime.date.today().year:
|
||||||
|
today = datetime.date.today()
|
||||||
|
age = today.year - bday.year - ((today.month, today.day) < (bday.month, bday.day))
|
||||||
|
embed.add_field(name=S.MEMBER_UI["age_field"], value=S.MEMBER_UI["age_val"].format(age=age), inline=True)
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for sheet_key, label in S.MEMBER_FIELDS:
|
||||||
|
val = str(row.get(sheet_key, "")).strip()
|
||||||
|
if val:
|
||||||
|
embed.add_field(name=label, value=val, inline=True)
|
||||||
|
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
93
dev_member_runtime.py
Normal file
93
dev_member_runtime.py
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
|
||||||
|
import config
|
||||||
|
import sheets
|
||||||
|
from member_sync import announce_birthday, is_birthday_today, sync_member
|
||||||
|
|
||||||
|
|
||||||
|
async def run_birthday_daily(
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
has_announced_today: Callable[[int], bool],
|
||||||
|
mark_announced_today: Callable[[int], None],
|
||||||
|
) -> None:
|
||||||
|
"""Announce birthdays in the configured guild for users whose birthday is today."""
|
||||||
|
guild = bot.get_guild(config.GUILD_ID)
|
||||||
|
if guild is None:
|
||||||
|
log.warning("Birthday task: guild %s not found", config.GUILD_ID)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = sheets.refresh()
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Birthday task: sheet refresh failed: %s", e)
|
||||||
|
data = sheets.get_cache()
|
||||||
|
|
||||||
|
announced = 0
|
||||||
|
for row in data:
|
||||||
|
bday_str = str(row.get("Sünnipäev", "")).strip()
|
||||||
|
if not is_birthday_today(bday_str):
|
||||||
|
continue
|
||||||
|
|
||||||
|
member = None
|
||||||
|
raw_id = str(row.get("User ID", "")).strip()
|
||||||
|
if raw_id:
|
||||||
|
try:
|
||||||
|
member = guild.get_member(int(raw_id))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if member is None:
|
||||||
|
discord_name = str(row.get("Discord", "")).strip()
|
||||||
|
if discord_name:
|
||||||
|
member = discord.utils.find(
|
||||||
|
lambda m, n=discord_name: m.name.lower() == n.lower(),
|
||||||
|
guild.members,
|
||||||
|
)
|
||||||
|
if member and not has_announced_today(member.id):
|
||||||
|
await announce_birthday(member, bot)
|
||||||
|
mark_announced_today(member.id)
|
||||||
|
announced += 1
|
||||||
|
|
||||||
|
log.info("Sünnipäeva ülesanne: teavitati %d liiget", announced)
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_member_join(
|
||||||
|
member: discord.Member,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
has_announced_today: Callable[[int], bool],
|
||||||
|
mark_announced_today: Callable[[int], None],
|
||||||
|
log_sync_result: Callable[[discord.Member, object], None],
|
||||||
|
) -> None:
|
||||||
|
"""Sync a newly joined member against sheet data and trigger birthday notice if needed."""
|
||||||
|
log.info("Member joined: %s (ID: %s)", member, member.id)
|
||||||
|
|
||||||
|
if not sheets.get_cache():
|
||||||
|
sheets.refresh()
|
||||||
|
|
||||||
|
result = await sync_member(member, member.guild)
|
||||||
|
|
||||||
|
if result.not_found:
|
||||||
|
try:
|
||||||
|
sheets.add_new_member_row(member.name, member.id)
|
||||||
|
log.info(
|
||||||
|
" → %s not in sheet, added new row (Discord=%s, ID=%s)",
|
||||||
|
member,
|
||||||
|
member.name,
|
||||||
|
member.id,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.error(" → Failed to add sheet row for %s: %s", member, e)
|
||||||
|
return
|
||||||
|
|
||||||
|
log_sync_result(member, result)
|
||||||
|
sheets.set_synced(member.id, result.synced)
|
||||||
|
|
||||||
|
if result.birthday_soon and not has_announced_today(member.id):
|
||||||
|
await announce_birthday(member, bot)
|
||||||
|
mark_announced_today(member.id)
|
||||||
@@ -13,9 +13,14 @@ Create your admin account on first launch via the Admin UI.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Create the `economy_users` collection
|
## 2. Create economy collections for both bot profiles
|
||||||
|
|
||||||
In the Admin UI → **Collections** → **New collection** → name it exactly `economy_users`.
|
In the Admin UI → **Collections** → **New collection** and create:
|
||||||
|
|
||||||
|
- `economy_users_dev`
|
||||||
|
- `economy_users_prod`
|
||||||
|
|
||||||
|
Use the same schema for both collections.
|
||||||
|
|
||||||
Add the following fields:
|
Add the following fields:
|
||||||
|
|
||||||
@@ -51,6 +56,8 @@ Add to your `.env`:
|
|||||||
PB_URL=http://127.0.0.1:8090
|
PB_URL=http://127.0.0.1:8090
|
||||||
PB_ADMIN_EMAIL=your-admin@email.com
|
PB_ADMIN_EMAIL=your-admin@email.com
|
||||||
PB_ADMIN_PASSWORD=your-admin-password
|
PB_ADMIN_PASSWORD=your-admin-password
|
||||||
|
PB_ECONOMY_COLLECTION_DEV=economy_users_dev
|
||||||
|
PB_ECONOMY_COLLECTION_ECONOMY=economy_users_prod
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
310
economy_admin_commands.py
Normal file
310
economy_admin_commands.py
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
async def _dm_user(bot: discord.Client, user_id: int, msg: str) -> None:
|
||||||
|
"""Best-effort DM to a user."""
|
||||||
|
try:
|
||||||
|
user = bot.get_user(user_id) or await bot.fetch_user(user_id)
|
||||||
|
await user.send(msg)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def register_economy_admin_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
cd_ts: Callable[[datetime.timedelta], str],
|
||||||
|
apply_level_role: Callable[[discord.Member, int, int], Awaitable[None]],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="adminseason", description=S.CMD["adminseason"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
@app_commands.describe(top_n=S.OPT["adminseason_top_n"])
|
||||||
|
async def cmd_adminseason(interaction: discord.Interaction, top_n: int = 10):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
top = await economy.do_season_reset(top_n)
|
||||||
|
guild = interaction.guild
|
||||||
|
|
||||||
|
if guild:
|
||||||
|
all_role_names = {name for _, name in economy.LEVEL_ROLES}
|
||||||
|
for role_name in all_role_names:
|
||||||
|
role = discord.utils.find(lambda r: r.name == role_name, guild.roles)
|
||||||
|
if not role:
|
||||||
|
continue
|
||||||
|
for member in list(role.members):
|
||||||
|
try:
|
||||||
|
await member.remove_roles(role, reason="Season reset")
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
|
medals = ["🥇", "🥈", "🥉"]
|
||||||
|
lines = []
|
||||||
|
for i, (uid, exp, lvl) in enumerate(top):
|
||||||
|
member = guild.get_member(int(uid)) if guild else None
|
||||||
|
name = member.display_name if member else S.LEADERBOARD_UI["unknown_user"].format(uid=uid)
|
||||||
|
prefix = medals[i] if i < 3 else f"**{i + 1}.**"
|
||||||
|
lines.append(S.SEASON["entry"].format(prefix=prefix, name=name, exp=exp, level=lvl))
|
||||||
|
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.TITLE["adminseason"],
|
||||||
|
description=(S.SEASON["top_header"] + "\n" + "\n".join(lines)) if lines else S.SEASON["no_players"],
|
||||||
|
color=0xF4C430,
|
||||||
|
)
|
||||||
|
embed.set_footer(text=S.SEASON["footer"])
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=False)
|
||||||
|
await interaction.followup.send(S.SEASON["done"], ephemeral=True)
|
||||||
|
log.info("/adminseason triggered by %s (top_n=%d)", interaction.user, top_n)
|
||||||
|
|
||||||
|
@tree.command(name="admincoins", description=S.CMD["admincoins"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
kogus=S.OPT["admincoins_kogus"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_admincoins(interaction: discord.Interaction, kasutaja: discord.Member, kogus: int, põhjus: str):
|
||||||
|
if kogus == 0:
|
||||||
|
await interaction.response.send_message(S.ERR["admincoins_zero"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_admin_coins(kasutaja.id, kogus, interaction.user.id, põhjus)
|
||||||
|
verb = f"+{kogus}" if kogus > 0 else str(kogus)
|
||||||
|
emoji = "💰" if kogus > 0 else "💸"
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["coins_done"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=kasutaja.display_name,
|
||||||
|
verb=verb,
|
||||||
|
coin=economy.COIN,
|
||||||
|
balance=f"{res['balance']:,}",
|
||||||
|
reason=põhjus,
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(
|
||||||
|
bot,
|
||||||
|
kasutaja.id,
|
||||||
|
S.ADMIN["coins_dm"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
verb=verb,
|
||||||
|
coin=economy.COIN,
|
||||||
|
reason=põhjus,
|
||||||
|
balance=f"{res['balance']:,}",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
log.info("ADMINCOINS %s → %s (%s) by %s", verb, kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminjail", description=S.CMD["adminjail"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
minutid=S.OPT["adminjail_minutid"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminjail(interaction: discord.Interaction, kasutaja: discord.Member, minutid: int, põhjus: str):
|
||||||
|
if minutid <= 0:
|
||||||
|
await interaction.response.send_message(S.ERR["positive_duration"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await economy.do_admin_jail(kasutaja.id, minutid, interaction.user.id, põhjus)
|
||||||
|
until_ts = cd_ts(datetime.timedelta(minutes=minutid))
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["jail_done"].format(name=kasutaja.display_name, minutes=minutid, ts=until_ts, reason=põhjus),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(
|
||||||
|
bot,
|
||||||
|
kasutaja.id,
|
||||||
|
S.ADMIN["jail_dm"].format(minutes=minutid, reason=põhjus, ts=until_ts),
|
||||||
|
)
|
||||||
|
log.info("ADMINJAIL %s %dmin (%s) by %s", kasutaja, minutid, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminunjail", description=S.CMD["adminunjail"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminunjail(interaction: discord.Interaction, kasutaja: discord.Member):
|
||||||
|
await economy.do_admin_unjail(kasutaja.id, interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["unjail_done"].format(name=kasutaja.display_name), ephemeral=True
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["unjail_dm"])
|
||||||
|
log.info("ADMINUNJAIL %s by %s", kasutaja, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminban", description=S.CMD["adminban"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminban(interaction: discord.Interaction, kasutaja: discord.Member, põhjus: str):
|
||||||
|
if bot.user and kasutaja.id == bot.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["admin_ban_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await economy.do_admin_ban(kasutaja.id, interaction.user.id, põhjus)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["ban_done"].format(name=kasutaja.display_name, reason=põhjus),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["ban_dm"].format(reason=põhjus))
|
||||||
|
log.info("ADMINBAN %s (%s) by %s", kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminunban", description=S.CMD["adminunban"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminunban(interaction: discord.Interaction, kasutaja: discord.Member):
|
||||||
|
await economy.do_admin_unban(kasutaja.id, interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["unban_done"].format(name=kasutaja.display_name), ephemeral=True
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["unban_dm"])
|
||||||
|
log.info("ADMINUNBAN %s by %s", kasutaja, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminreset", description=S.CMD["adminreset"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminreset(interaction: discord.Interaction, kasutaja: discord.Member, põhjus: str):
|
||||||
|
if bot.user and kasutaja.id == bot.user.id:
|
||||||
|
await interaction.response.send_message(S.ERR["admin_reset_bot"], ephemeral=True)
|
||||||
|
return
|
||||||
|
await economy.do_admin_reset(kasutaja.id, interaction.user.id)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["reset_done"].format(name=kasutaja.display_name, reason=põhjus),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["reset_dm"].format(reason=põhjus))
|
||||||
|
log.info("ADMINRESET %s (%s) by %s", kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminview", description=S.CMD["adminview"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminview(interaction: discord.Interaction, kasutaja: discord.Member):
|
||||||
|
res = await economy.do_admin_inspect(kasutaja.id)
|
||||||
|
data = res["data"]
|
||||||
|
items_str = ", ".join(data.get("items", [])) or "-"
|
||||||
|
uses = data.get("item_uses", {})
|
||||||
|
uses_str = ", ".join(f"{k}:{v}" for k, v in uses.items()) if uses else "-"
|
||||||
|
jailed = data.get("jailed_until") or "-"
|
||||||
|
banned = S.ADMINVIEW_UI["banned_yes"] if data.get("eco_banned") else S.ADMINVIEW_UI["banned_no"]
|
||||||
|
exp = data.get("exp", 0)
|
||||||
|
level = economy.get_level(exp)
|
||||||
|
prestige_lvl = data.get("prestige_level", 0)
|
||||||
|
prestige_pp = data.get("prestige_points", 0)
|
||||||
|
total_fish = data.get("total_fish_caught", 0)
|
||||||
|
inv_fish = len(data.get("fish_inventory") or [])
|
||||||
|
embed = discord.Embed(
|
||||||
|
title=S.ADMINVIEW_UI["title"].format(name=kasutaja.display_name),
|
||||||
|
color=0x5865F2,
|
||||||
|
)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_balance"], value=f"{data.get('balance', 0):,} {economy.COIN}", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_exp"], value=S.ADMINVIEW_UI["exp_val"].format(exp=f"{exp:,}", level=level), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_streak"], value=str(data.get("daily_streak", 0)), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_banned"], value=banned, inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_jailed"], value=jailed, inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_prestige"], value=S.ADMINVIEW_UI["prestige_val"].format(level=prestige_lvl, pp=prestige_pp), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_fish"], value=S.ADMINVIEW_UI["fish_val"].format(caught=total_fish, inv=inv_fish), inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_items"], value=items_str, inline=False)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_uses"], value=uses_str, inline=False)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_daily"], value=data.get("last_daily") or "-", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_work"], value=data.get("last_work") or "-", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_crime"], value=data.get("last_crime") or "-", inline=True)
|
||||||
|
embed.add_field(name=S.ADMINVIEW_UI["f_last_fish"], value=data.get("last_fish") or "-", inline=True)
|
||||||
|
embed.set_footer(text=S.ADMINVIEW_UI["footer"].format(uid=kasutaja.id))
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
log.info("ADMINVIEW %s by %s", kasutaja, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminexp", description=S.CMD["adminexp"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
kogus=S.OPT["adminexp_kogus"],
|
||||||
|
põhjus=S.OPT["admin_põhjus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminexp(interaction: discord.Interaction, kasutaja: discord.Member, kogus: int, põhjus: str):
|
||||||
|
if kogus == 0:
|
||||||
|
await interaction.response.send_message(S.ERR["admincoins_zero"], ephemeral=True)
|
||||||
|
return
|
||||||
|
res = await economy.do_admin_exp(kasutaja.id, kogus, interaction.user.id, põhjus)
|
||||||
|
verb = f"+{kogus}" if kogus > 0 else str(kogus)
|
||||||
|
emoji = "📈" if kogus > 0 else "📉"
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["exp_done"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
name=kasutaja.display_name,
|
||||||
|
verb=verb,
|
||||||
|
exp=f"{res['exp']:,}",
|
||||||
|
level=res["new_level"],
|
||||||
|
reason=põhjus,
|
||||||
|
),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
await _dm_user(
|
||||||
|
bot,
|
||||||
|
kasutaja.id,
|
||||||
|
S.ADMIN["exp_dm"].format(
|
||||||
|
emoji=emoji,
|
||||||
|
verb=verb,
|
||||||
|
reason=põhjus,
|
||||||
|
exp=f"{res['exp']:,}",
|
||||||
|
level=res["new_level"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if res["level_changed"]:
|
||||||
|
member = interaction.guild.get_member(kasutaja.id) if interaction.guild else None
|
||||||
|
if member:
|
||||||
|
await apply_level_role(member, res["new_level"], res["old_level"])
|
||||||
|
log.info("ADMINEXP %s → %s (%s) by %s", verb, kasutaja, põhjus, interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="adminitem", description=S.CMD["adminitem"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kasutaja=S.OPT["admin_kasutaja"],
|
||||||
|
ese=S.OPT["adminitem_ese"],
|
||||||
|
tegevus=S.OPT["adminitem_tegevus"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_adminitem(interaction: discord.Interaction, kasutaja: discord.Member, ese: str, tegevus: str):
|
||||||
|
action = tegevus.strip().lower()
|
||||||
|
if action not in ("anna", "eemalda"):
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_invalid"].format(item_id=tegevus), ephemeral=True)
|
||||||
|
return
|
||||||
|
action_key = "give" if action == "anna" else "remove"
|
||||||
|
res = await economy.do_admin_item(kasutaja.id, ese, action_key, interaction.user.id)
|
||||||
|
if not res["ok"]:
|
||||||
|
if res["reason"] == "invalid_item":
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_invalid"].format(item_id=ese), ephemeral=True)
|
||||||
|
elif res["reason"] == "not_owned":
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ADMIN["item_not_owned"].format(name=kasutaja.display_name, item_id=ese),
|
||||||
|
ephemeral=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(S.ERR["generic_error"].format(error=res["reason"]), ephemeral=True)
|
||||||
|
return
|
||||||
|
item_name = economy.SHOP[ese]["name"]
|
||||||
|
if action_key == "give":
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_given"].format(item=item_name, name=kasutaja.display_name), ephemeral=True)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["item_dm_given"].format(item=item_name))
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(S.ADMIN["item_removed"].format(item=item_name, name=kasutaja.display_name), ephemeral=True)
|
||||||
|
await _dm_user(bot, kasutaja.id, S.ADMIN["item_dm_removed"].format(item=item_name))
|
||||||
|
log.info("ADMINITEM %s %s %s by %s", action_key, ese, kasutaja, interaction.user)
|
||||||
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)
|
||||||
134
ops_admin_commands.py
Normal file
134
ops_admin_commands.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_ops_admin_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
process,
|
||||||
|
start_time: datetime.datetime,
|
||||||
|
log_dir: Path,
|
||||||
|
guild_obj: discord.Object,
|
||||||
|
restart_file: Path,
|
||||||
|
get_member_cache_size: Callable[[], int],
|
||||||
|
get_paused: Callable[[], bool],
|
||||||
|
set_paused: Callable[[bool], None],
|
||||||
|
count_economy_users: Callable[[], Awaitable[int]],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="status", description=S.CMD["status"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_status(interaction: discord.Interaction):
|
||||||
|
mem = process.memory_info()
|
||||||
|
cpu = process.cpu_percent(interval=0.1)
|
||||||
|
uptime = datetime.datetime.now() - start_time
|
||||||
|
hours, rem = divmod(int(uptime.total_seconds()), 3600)
|
||||||
|
minutes, seconds = divmod(rem, 60)
|
||||||
|
tasks_count = len(asyncio.all_tasks())
|
||||||
|
latency_ms = round(bot.latency * 1000, 1)
|
||||||
|
cache_size = get_member_cache_size()
|
||||||
|
user_count = await count_economy_users()
|
||||||
|
|
||||||
|
embed = discord.Embed(title=S.STATUS_UI["title"], color=0x57F287)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["uptime_field"],
|
||||||
|
value=S.STATUS_UI["uptime_val"].format(hours=hours, minutes=minutes, seconds=seconds),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["latency_field"],
|
||||||
|
value=S.STATUS_UI["latency_val"].format(ms=latency_ms),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["ram_field"],
|
||||||
|
value=S.STATUS_UI["ram_val"].format(mb=f"{mem.rss / 1024**2:.1f}"),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["cpu_field"],
|
||||||
|
value=S.STATUS_UI["cpu_val"].format(percent=f"{cpu:.1f}"),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["tasks_field"],
|
||||||
|
value=str(tasks_count),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["eco_players_field"],
|
||||||
|
value=str(user_count),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["members_cache_field"],
|
||||||
|
value=str(cache_size),
|
||||||
|
inline=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
log_lines = [
|
||||||
|
S.STATUS_UI["log_line"].format(name=p.name, size_kb=f"{p.stat().st_size / 1024:.1f}")
|
||||||
|
for p in sorted(log_dir.glob("*.log*"))
|
||||||
|
if p.is_file()
|
||||||
|
]
|
||||||
|
embed.add_field(
|
||||||
|
name=S.STATUS_UI["log_files_field"],
|
||||||
|
value="\n".join(log_lines) or S.STATUS_UI["none"],
|
||||||
|
inline=False,
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
|
|
||||||
|
@tree.command(name="sync", description=S.CMD["sync"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_sync(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
tree.copy_global_to(guild=guild_obj)
|
||||||
|
await tree.sync(guild=guild_obj)
|
||||||
|
tree.clear_commands(guild=None)
|
||||||
|
await tree.sync()
|
||||||
|
await interaction.followup.send(S.MSG_SYNC_DONE, ephemeral=True)
|
||||||
|
log.info("/sync triggered by %s", interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="restart", description=S.CMD["restart"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_restart(interaction: discord.Interaction):
|
||||||
|
restart_file.write_text(json.dumps({"channel_id": interaction.channel_id}), encoding="utf-8")
|
||||||
|
await interaction.response.send_message(S.MSG_RESTARTING, ephemeral=True)
|
||||||
|
log.info("/restart triggered by %s", interaction.user)
|
||||||
|
subprocess.Popen([sys.executable] + sys.argv, cwd=os.getcwd())
|
||||||
|
await bot.close()
|
||||||
|
|
||||||
|
@tree.command(name="shutdown", description=S.CMD["shutdown"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_shutdown(interaction: discord.Interaction):
|
||||||
|
await interaction.response.send_message(S.MSG_SHUTTING_DOWN, ephemeral=True)
|
||||||
|
log.info("/shutdown triggered by %s", interaction.user)
|
||||||
|
await bot.close()
|
||||||
|
|
||||||
|
@tree.command(name="pause", description=S.CMD["pause"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_pause(interaction: discord.Interaction):
|
||||||
|
paused = not get_paused()
|
||||||
|
set_paused(paused)
|
||||||
|
msg = S.MSG_PAUSED if paused else S.MSG_UNPAUSED
|
||||||
|
log.info("/pause toggled → %s by %s", "PAUSED" if paused else "UNPAUSED", interaction.user)
|
||||||
|
await interaction.response.send_message(msg, ephemeral=True)
|
||||||
135
ops_channel_commands.py
Normal file
135
ops_channel_commands.py
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord import app_commands
|
||||||
|
|
||||||
|
import economy
|
||||||
|
import strings as S
|
||||||
|
|
||||||
|
|
||||||
|
def register_ops_channel_commands(
|
||||||
|
tree: app_commands.CommandTree,
|
||||||
|
bot: discord.Client,
|
||||||
|
log: logging.Logger,
|
||||||
|
get_allowed_channels: Callable[[], list[int]],
|
||||||
|
set_allowed_channels: Callable[[list[int]], None],
|
||||||
|
) -> None:
|
||||||
|
@tree.command(name="send", description=S.CMD["send"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(
|
||||||
|
kanal=S.OPT["send_kanal"],
|
||||||
|
sõnum=S.OPT["send_sõnum"],
|
||||||
|
)
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_send(interaction: discord.Interaction, kanal: discord.TextChannel, sõnum: str):
|
||||||
|
try:
|
||||||
|
await kanal.send(sõnum)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.SEND_UI["sent"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
except discord.Forbidden:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.SEND_UI["forbidden"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.ERR["send_failed"].format(error=e), ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="economysetup", description=S.CMD["economysetup"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_economysetup(interaction: discord.Interaction):
|
||||||
|
await interaction.response.defer(ephemeral=True)
|
||||||
|
guild = interaction.guild
|
||||||
|
bot_member = guild.get_member(bot.user.id)
|
||||||
|
bot_top_pos = max((r.position for r in bot_member.roles if r.managed), default=1)
|
||||||
|
|
||||||
|
all_role_names = [economy.ECONOMY_ROLE] + [name for _, name in economy.LEVEL_ROLES]
|
||||||
|
|
||||||
|
created, existing = [], []
|
||||||
|
for name in all_role_names:
|
||||||
|
role = discord.utils.find(lambda r, n=name: r.name == n, guild.roles)
|
||||||
|
if role is None:
|
||||||
|
await guild.create_role(name=name, reason="/economysetup")
|
||||||
|
created.append(name)
|
||||||
|
else:
|
||||||
|
existing.append(name)
|
||||||
|
|
||||||
|
positions: dict[discord.Role, int] = {}
|
||||||
|
base = max(bot_top_pos - 1, 1)
|
||||||
|
for i, name in enumerate(all_role_names):
|
||||||
|
role = discord.utils.find(lambda r, n=name: r.name == n, guild.roles)
|
||||||
|
if role:
|
||||||
|
positions[role] = max(base - i, 1)
|
||||||
|
if positions:
|
||||||
|
try:
|
||||||
|
await guild.edit_role_positions(positions=positions)
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
|
embed = discord.Embed(title=S.TITLE["economysetup"], color=0x57F287)
|
||||||
|
if created:
|
||||||
|
embed.add_field(name=S.ECONOMYSETUP_UI["created_field"], value="\n".join(created), inline=True)
|
||||||
|
if existing:
|
||||||
|
embed.add_field(name=S.ECONOMYSETUP_UI["existing_field"], value="\n".join(existing), inline=True)
|
||||||
|
embed.set_footer(text=S.ECONOMYSETUP_UI["footer"])
|
||||||
|
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||||
|
log.info("/economysetup triggered by %s", interaction.user)
|
||||||
|
|
||||||
|
@tree.command(name="allowchannel", description=S.CMD["allowchannel"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kanal=S.OPT["allowchannel_kanal"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_allowchannel(interaction: discord.Interaction, kanal: discord.TextChannel):
|
||||||
|
allowed = get_allowed_channels()
|
||||||
|
if kanal.id in allowed:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["already_allowed"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
allowed.append(kanal.id)
|
||||||
|
set_allowed_channels(allowed)
|
||||||
|
log.info("ALLOWCHANNEL +%s by %s", kanal, interaction.user)
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["added"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="denychannel", description=S.CMD["denychannel"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.describe(kanal=S.OPT["denychannel_kanal"])
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_denychannel(interaction: discord.Interaction, kanal: discord.TextChannel):
|
||||||
|
allowed = get_allowed_channels()
|
||||||
|
if kanal.id not in allowed:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["not_in_list"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
allowed.remove(kanal.id)
|
||||||
|
set_allowed_channels(allowed)
|
||||||
|
log.info("DENYCHANNEL -%s by %s", kanal, interaction.user)
|
||||||
|
if allowed:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["removed"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message(
|
||||||
|
S.CHANNEL_UI["removed_last"].format(channel=kanal.mention), ephemeral=True
|
||||||
|
)
|
||||||
|
|
||||||
|
@tree.command(name="channels", description=S.CMD["channels"])
|
||||||
|
@app_commands.guild_only()
|
||||||
|
@app_commands.default_permissions(manage_guild=True)
|
||||||
|
async def cmd_channels(interaction: discord.Interaction):
|
||||||
|
allowed = get_allowed_channels()
|
||||||
|
if not allowed:
|
||||||
|
desc = S.CHANNEL_UI["list_empty"]
|
||||||
|
else:
|
||||||
|
lines = "\n".join(f"• <#{cid}>" for cid in allowed)
|
||||||
|
desc = S.CHANNEL_UI["list_filled"].format(lines=lines)
|
||||||
|
embed = discord.Embed(title=S.CHANNEL_UI["list_title"], description=desc, color=0x5865F2)
|
||||||
|
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||||
12
pb_client.py
12
pb_client.py
@@ -7,24 +7,26 @@ Environment variables (set in .env):
|
|||||||
PB_URL Base URL of PocketBase (default: http://127.0.0.1:8090)
|
PB_URL Base URL of PocketBase (default: http://127.0.0.1:8090)
|
||||||
PB_ADMIN_EMAIL PocketBase admin e-mail
|
PB_ADMIN_EMAIL PocketBase admin e-mail
|
||||||
PB_ADMIN_PASSWORD PocketBase admin password
|
PB_ADMIN_PASSWORD PocketBase admin password
|
||||||
|
PB_ECONOMY_COLLECTION_DEV / PB_ECONOMY_COLLECTION_ECONOMY
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
_log = logging.getLogger("tipiCOIN.pb")
|
_log = logging.getLogger("tipiCOIN.pb")
|
||||||
|
|
||||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
PB_URL = config.PB_URL
|
||||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
||||||
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||||
ECONOMY_COLLECTION = "economy_users"
|
ECONOMY_COLLECTION = config.PB_ECONOMY_COLLECTION
|
||||||
|
|
||||||
_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
_TIMEOUT = aiohttp.ClientTimeout(total=10)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ Requirements:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -21,10 +20,12 @@ from dotenv import load_dotenv
|
|||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
PB_URL = os.getenv("PB_URL", "http://127.0.0.1:8090")
|
import config # noqa: E402
|
||||||
PB_ADMIN_EMAIL = os.getenv("PB_ADMIN_EMAIL", "")
|
|
||||||
PB_ADMIN_PASSWORD = os.getenv("PB_ADMIN_PASSWORD", "")
|
PB_URL = config.PB_URL
|
||||||
COLLECTION = "economy_users"
|
PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
||||||
|
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||||
|
COLLECTION = config.PB_ECONOMY_COLLECTION
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# New fields to add
|
# New fields to add
|
||||||
|
|||||||
180
scripts/reset_pb_collections.py
Normal file
180
scripts/reset_pb_collections.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
"""Destructively recreate economy PocketBase collections for dev + economy profiles.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/reset_pb_collections.py --confirm
|
||||||
|
|
||||||
|
This will DELETE and recreate the collections configured by:
|
||||||
|
- PB_ECONOMY_COLLECTION_DEV
|
||||||
|
- PB_ECONOMY_COLLECTION_ECONOMY
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
import config # noqa: E402
|
||||||
|
|
||||||
|
PB_URL = config.PB_URL
|
||||||
|
PB_ADMIN_EMAIL = config.PB_ADMIN_EMAIL
|
||||||
|
PB_ADMIN_PASSWORD = config.PB_ADMIN_PASSWORD
|
||||||
|
|
||||||
|
|
||||||
|
def _text_field(name: str, required: bool = False) -> dict:
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"type": "text",
|
||||||
|
"required": required,
|
||||||
|
"options": {"min": None, "max": None, "pattern": ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _number_field(name: str) -> dict:
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"type": "number",
|
||||||
|
"required": False,
|
||||||
|
"options": {"min": None, "max": None, "noDecimal": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _bool_field(name: str) -> dict:
|
||||||
|
return {"name": name, "type": "bool", "required": False}
|
||||||
|
|
||||||
|
|
||||||
|
def _json_field(name: str) -> dict:
|
||||||
|
return {"name": name, "type": "json", "required": False}
|
||||||
|
|
||||||
|
|
||||||
|
def _collection_payload(name: str) -> dict:
|
||||||
|
fields = [
|
||||||
|
_text_field("user_id", required=True),
|
||||||
|
_number_field("balance"),
|
||||||
|
_number_field("exp"),
|
||||||
|
_number_field("daily_streak"),
|
||||||
|
_text_field("last_daily"),
|
||||||
|
_text_field("last_work"),
|
||||||
|
_text_field("last_beg"),
|
||||||
|
_text_field("last_crime"),
|
||||||
|
_text_field("last_rob"),
|
||||||
|
_text_field("last_heist"),
|
||||||
|
_text_field("last_streak_date"),
|
||||||
|
_text_field("jailed_until"),
|
||||||
|
_text_field("last_fish"),
|
||||||
|
_json_field("items"),
|
||||||
|
_json_field("item_uses"),
|
||||||
|
_json_field("reminders"),
|
||||||
|
_json_field("prestige_upgrades"),
|
||||||
|
_json_field("fish_book"),
|
||||||
|
_json_field("fish_inventory"),
|
||||||
|
_bool_field("eco_banned"),
|
||||||
|
_bool_field("jailbreak_used"),
|
||||||
|
_number_field("heist_global_cd_until"),
|
||||||
|
_number_field("peak_balance"),
|
||||||
|
_number_field("lifetime_earned"),
|
||||||
|
_number_field("lifetime_lost"),
|
||||||
|
_number_field("work_count"),
|
||||||
|
_number_field("beg_count"),
|
||||||
|
_number_field("total_wagered"),
|
||||||
|
_number_field("biggest_win"),
|
||||||
|
_number_field("biggest_loss"),
|
||||||
|
_number_field("slots_jackpots"),
|
||||||
|
_number_field("crimes_attempted"),
|
||||||
|
_number_field("crimes_succeeded"),
|
||||||
|
_number_field("times_jailed"),
|
||||||
|
_number_field("total_bail_paid"),
|
||||||
|
_number_field("heists_joined"),
|
||||||
|
_number_field("heists_won"),
|
||||||
|
_number_field("total_given"),
|
||||||
|
_number_field("total_received"),
|
||||||
|
_number_field("best_daily_streak"),
|
||||||
|
_number_field("prestige_level"),
|
||||||
|
_number_field("prestige_points"),
|
||||||
|
_number_field("season_total_exp"),
|
||||||
|
_number_field("total_fish_caught"),
|
||||||
|
]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"type": "base",
|
||||||
|
"fields": fields,
|
||||||
|
"listRule": None,
|
||||||
|
"viewRule": None,
|
||||||
|
"createRule": None,
|
||||||
|
"updateRule": None,
|
||||||
|
"deleteRule": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _auth_token(session: aiohttp.ClientSession) -> str:
|
||||||
|
async with session.post(
|
||||||
|
f"{PB_URL}/api/collections/_superusers/auth-with-password",
|
||||||
|
json={"identity": PB_ADMIN_EMAIL, "password": PB_ADMIN_PASSWORD},
|
||||||
|
) as resp:
|
||||||
|
if resp.status != 200:
|
||||||
|
raise RuntimeError(f"Auth failed ({resp.status}): {await resp.text()}")
|
||||||
|
return (await resp.json())["token"]
|
||||||
|
|
||||||
|
|
||||||
|
async def _delete_if_exists(session: aiohttp.ClientSession, headers: dict[str, str], name: str) -> None:
|
||||||
|
async with session.get(f"{PB_URL}/api/collections/{name}", headers=headers) as resp:
|
||||||
|
if resp.status == 404:
|
||||||
|
print(f"[SKIP] {name} does not exist")
|
||||||
|
return
|
||||||
|
if resp.status != 200:
|
||||||
|
raise RuntimeError(f"Could not fetch {name} ({resp.status}): {await resp.text()}")
|
||||||
|
|
||||||
|
async with session.delete(f"{PB_URL}/api/collections/{name}", headers=headers) as resp:
|
||||||
|
if resp.status not in (200, 204):
|
||||||
|
raise RuntimeError(f"Delete failed for {name} ({resp.status}): {await resp.text()}")
|
||||||
|
print(f"[DELETE] {name}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _create_collection(session: aiohttp.ClientSession, headers: dict[str, str], name: str) -> None:
|
||||||
|
payload = _collection_payload(name)
|
||||||
|
async with session.post(f"{PB_URL}/api/collections", json=payload, headers=headers) as resp:
|
||||||
|
if resp.status not in (200, 201):
|
||||||
|
raise RuntimeError(f"Create failed for {name} ({resp.status}): {await resp.text()}")
|
||||||
|
print(f"[CREATE] {name}")
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--confirm", action="store_true", help="Required flag to run destructive reset")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not args.confirm:
|
||||||
|
raise SystemExit("Refusing to run without --confirm (this operation deletes collections).")
|
||||||
|
|
||||||
|
targets = []
|
||||||
|
for name in [config.PB_ECONOMY_COLLECTION_DEV, config.PB_ECONOMY_COLLECTION_ECONOMY]:
|
||||||
|
if name and name not in targets:
|
||||||
|
targets.append(name)
|
||||||
|
|
||||||
|
if not targets:
|
||||||
|
raise SystemExit("No target collections configured.")
|
||||||
|
|
||||||
|
timeout = aiohttp.ClientTimeout(total=20)
|
||||||
|
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||||
|
token = await _auth_token(session)
|
||||||
|
headers = {"Authorization": token}
|
||||||
|
|
||||||
|
for name in targets:
|
||||||
|
await _delete_if_exists(session, headers, name)
|
||||||
|
await _create_collection(session, headers, name)
|
||||||
|
|
||||||
|
print("\nDone. Collections recreated:")
|
||||||
|
for name in targets:
|
||||||
|
print(f" - {name}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
32
strings.py
32
strings.py
@@ -858,6 +858,11 @@ BIRTHDAY_UI: dict[str, str] = {
|
|||||||
"footer": "Leht {month}/12 · {month_name}",
|
"footer": "Leht {month}/12 · {month_name}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BIRTHDAY_MONTHS: list[str] = [
|
||||||
|
"Jaanuar", "Veebruar", "Märts", "Aprill", "Mai", "Juuni",
|
||||||
|
"Juuli", "August", "September", "Oktoober", "November", "Detsember",
|
||||||
|
]
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# /check summary strings
|
# /check summary strings
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -870,6 +875,7 @@ CHECK_UI: dict[str, str] = {
|
|||||||
"stat_uid": "Kasutaja ID",
|
"stat_uid": "Kasutaja ID",
|
||||||
"stat_discord": "Discordi kasutajanimi",
|
"stat_discord": "Discordi kasutajanimi",
|
||||||
"stat_bday": "Sünnipäev",
|
"stat_bday": "Sünnipäev",
|
||||||
|
"no_name": "(no name)",
|
||||||
"done": "**Kontroll lõpetatud!**",
|
"done": "**Kontroll lõpetatud!**",
|
||||||
"already_ok": "✅ Juba korras: {count}",
|
"already_ok": "✅ Juba korras: {count}",
|
||||||
"fixed": "🔧 Parandatud: {count}",
|
"fixed": "🔧 Parandatud: {count}",
|
||||||
@@ -878,9 +884,35 @@ CHECK_UI: dict[str, str] = {
|
|||||||
"errors": "⚠️ Vead: {count}",
|
"errors": "⚠️ Vead: {count}",
|
||||||
"details_header": "**Üksikasjad:**",
|
"details_header": "**Üksikasjad:**",
|
||||||
"details_more": "... ja {count} rohkem",
|
"details_more": "... ja {count} rohkem",
|
||||||
|
"detail_error": "⚠️ {error}",
|
||||||
|
"detail_nickname": "hüüdnimi",
|
||||||
|
"detail_roles_added": "+rollid: {roles}",
|
||||||
|
"detail_changed": "🔧 **{name}**: {parts}",
|
||||||
"ids_filled": "\n🔑 Täideti **{count}** puuduvat kasutaja ID-d.",
|
"ids_filled": "\n🔑 Täideti **{count}** puuduvat kasutaja ID-d.",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# /status UI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
STATUS_UI: dict[str, str] = {
|
||||||
|
"title": "🖥️ Boti olek",
|
||||||
|
"uptime_field": "🕐 Uptime",
|
||||||
|
"uptime_val": "{hours}t {minutes}m {seconds}s",
|
||||||
|
"latency_field": "📡 Latency",
|
||||||
|
"latency_val": "{ms} ms",
|
||||||
|
"ram_field": "🧠 RAM (RSS)",
|
||||||
|
"ram_val": "{mb} MB",
|
||||||
|
"cpu_field": "⚙️ CPU",
|
||||||
|
"cpu_val": "{percent}%",
|
||||||
|
"tasks_field": "🔄 Async tasks",
|
||||||
|
"eco_players_field": "👤 Eco players",
|
||||||
|
"members_cache_field": "📋 Liikmed (cache)",
|
||||||
|
"log_files_field": "📂 Log files",
|
||||||
|
"log_line": "`{name}` - {size_kb} KB",
|
||||||
|
"none": "-",
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Admin command responses and DMs
|
# Admin command responses and DMs
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user