1
0
forked from sass/tipibot

14 Commits

Author SHA1 Message Date
Rene Arumetsa
0cdd8dac63 Update env variables 2026-06-21 22:49:46 +03:00
Rene Arumetsa
6101a278e7 Admin check now a dict 2026-06-21 22:40:57 +03:00
Rene Arumetsa
1e9ec56761 Update tipidice emoij to be animated 2026-06-03 18:50:06 +03:00
Rene Arumetsa
a96488fe9e Added eco server emoij ID 2026-06-03 18:45:46 +03:00
Rene Arumetsa
25cf60d2e1 Fix conflict 2026-06-03 18:38:40 +03:00
Rene Arumetsa
3939c879c9 Refacor emoijs to use application emoijs 2026-06-03 18:34:29 +03:00
Rene Arumetsa
42f7bae681 Edit discord role rights 2026-06-01 22:19:12 +03:00
Rene Arumetsa
b0e23c1a17 Change admin command permissions 2026-06-01 22:11:44 +03:00
Rene Arumetsa
24de79c503 Edit discord interaction, seems to cause a trouble 2026-05-25 17:51:33 +03:00
Rene Arumetsa
ee4e639c30 Remove custom_id from shop, which causes bug of tiers not working 2026-05-25 17:44:43 +03:00
Rene Arumetsa
48cc8398ec Rename logger to include bot profiles 2026-05-20 17:48:44 +03:00
Rene Arumetsa
b83b347d6a Fix shop interation 2026-05-19 18:37:42 +03:00
Rene Arumetsa
8d7ac504ca Change exp leveling system 2026-05-13 22:43:34 +03:00
Rene Arumetsa
15e3121d55 Fix birthday bug 2026-05-11 22:28:21 +03:00
14 changed files with 355 additions and 166 deletions

6
bot.py
View File

@@ -22,6 +22,7 @@ import psutil
import config
import strings as S
from core import economy, pb_client, sheets
from core.admin import is_bot_admin
from core.member_sync import SyncResult
from commands.dev_member_commands import register_dev_member_commands
from commands.dev_member_runtime import handle_member_join, run_birthday_daily
@@ -82,7 +83,7 @@ _txn_logger = logging.getLogger("tipiCOIN.txn")
_txn_logger.addHandler(_txn_h)
_txn_logger.propagate = False # don't double-log to console/bot.log
log = logging.getLogger("tipilan")
log = logging.getLogger(f"tipilan.{config.BOT_PROFILE}")
# ---------------------------------------------------------------------------
# Bot setup
@@ -593,8 +594,7 @@ class HelpSelect(discord.ui.Select):
@tree.command(name="help", description=S.CMD["help"])
async def cmd_help(interaction: discord.Interaction):
perms = interaction.user.guild_permissions if interaction.guild else None
is_admin = bool(perms and (perms.manage_roles or perms.manage_guild))
is_admin = is_bot_admin(interaction.user)
await interaction.response.send_message(
embed=_help_embed("üldine"), view=HelpView(is_admin), ephemeral=True
)

View File

@@ -8,8 +8,9 @@ import discord
from discord import app_commands
from core import sheets
from core.admin import bot_admin_check
import strings as S
from core.member_sync import announce_birthday, sync_member
from core.member_sync import announce_birthday, sync_member, today_local
class BirthdayPages(discord.ui.View):
@@ -44,7 +45,7 @@ def _build_birthday_pages(
Returns (pages, start_index) where start_index is the current month.
"""
rows = sheets.get_cache()
today = datetime.date.today()
today = today_local()
by_month: dict[int, list[tuple[int, str, int | None]]] = {m: [] for m in range(1, 13)}
@@ -167,7 +168,7 @@ def register_dev_member_commands(
@tree.command(name="check", description=S.CMD["check"])
@app_commands.guild_only()
@app_commands.default_permissions(manage_roles=True)
@bot_admin_check()
async def cmd_check(interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
@@ -236,7 +237,7 @@ def register_dev_member_commands(
else:
already_ok += 1
if result.birthday_soon and not has_announced_today(member.id):
if result.birthday_today and not has_announced_today(member.id):
birthday_pings += 1
await announce_birthday(member, bot)
mark_announced_today(member.id)
@@ -281,7 +282,7 @@ def register_dev_member_commands(
@tree.command(name="member", description=S.CMD["member"])
@app_commands.guild_only()
@app_commands.default_permissions(manage_roles=True)
@bot_admin_check()
async def cmd_member(interaction: discord.Interaction, user: discord.Member):
row = sheets.find_member(user.id, user.name)
if row is None:
@@ -298,8 +299,8 @@ def register_dev_member_commands(
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()
if 1920 <= bday.year <= today_local().year:
today = today_local()
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

View File

@@ -88,6 +88,6 @@ async def handle_member_join(
log_sync_result(member, result)
await sheets.set_synced(member.id, result.synced)
if result.birthday_soon and not has_announced_today(member.id):
if result.birthday_today and not has_announced_today(member.id):
await announce_birthday(member, bot)
mark_announced_today(member.id)

View File

@@ -8,6 +8,7 @@ import discord
from discord import app_commands
from core import economy
from core.admin import bot_admin_check
import strings as S
@@ -29,7 +30,7 @@ def register_economy_admin_commands(
) -> None:
@tree.command(name="adminseason", description=S.CMD["adminseason"])
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
@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)
@@ -73,7 +74,7 @@ def register_economy_admin_commands(
kogus=S.OPT["admincoins_kogus"],
põhjus=S.OPT["admin_põhjus"],
)
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
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)
@@ -112,7 +113,7 @@ def register_economy_admin_commands(
minutid=S.OPT["adminjail_minutid"],
põhjus=S.OPT["admin_põhjus"],
)
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
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)
@@ -133,7 +134,7 @@ def register_economy_admin_commands(
@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)
@bot_admin_check()
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(
@@ -148,7 +149,7 @@ def register_economy_admin_commands(
kasutaja=S.OPT["admin_kasutaja"],
põhjus=S.OPT["admin_põhjus"],
)
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
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)
@@ -164,7 +165,7 @@ def register_economy_admin_commands(
@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)
@bot_admin_check()
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(
@@ -179,7 +180,7 @@ def register_economy_admin_commands(
kasutaja=S.OPT["admin_kasutaja"],
põhjus=S.OPT["admin_põhjus"],
)
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
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)
@@ -195,7 +196,7 @@ def register_economy_admin_commands(
@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)
@bot_admin_check()
async def cmd_adminview(interaction: discord.Interaction, kasutaja: discord.Member):
res = await economy.do_admin_inspect(kasutaja.id)
data = res["data"]
@@ -238,7 +239,7 @@ def register_economy_admin_commands(
kogus=S.OPT["adminexp_kogus"],
põhjus=S.OPT["admin_põhjus"],
)
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
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)
@@ -281,7 +282,7 @@ def register_economy_admin_commands(
ese=S.OPT["adminitem_ese"],
tegevus=S.OPT["adminitem_tegevus"],
)
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
async def cmd_adminitem(interaction: discord.Interaction, kasutaja: discord.Member, ese: str, tegevus: str):
action = tegevus.strip().lower()
if action not in ("anna", "eemalda"):

View File

@@ -10,6 +10,7 @@ import discord
from discord import app_commands
from core import economy
from core.emoji import EMOJI as E
import strings as S
@@ -288,12 +289,12 @@ def register_economy_extra_commands(
# /jailbreak - Monopoly-style dice escape
# -----------------------------------------------------------------------
_DICE_EMOJI = [
"<:TipiYKS:1483103190491856916>",
"<:TipiKAKS:1483103215841972404>",
"<:TipiKOLM:1483103217846980781>",
"<:TipiNELI:1483103237585240114>",
"<:TipiVIIS:1483103239036469289>",
"<:TipiKUUS:1483103253163020348>",
E["TipiYKS"],
E["TipiKAKS"],
E["TipiKOLM"],
E["TipiNELI"],
E["TipiVIIS"],
E["TipiKUUS"],
]
class JailbreakView(discord.ui.View):
@@ -813,7 +814,6 @@ def register_economy_extra_commands(
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)
@@ -822,8 +822,9 @@ def register_economy_extra_commands(
async def callback(interaction: discord.Interaction):
self._tier = tier
self._update_buttons()
await interaction.response.defer()
self._user_data = await economy.get_user(interaction.user.id)
await interaction.response.edit_message(
await interaction.edit_original_response(
embed=_shop_embed(self._tier, self._user_data),
view=self,
)

View File

@@ -9,6 +9,7 @@ import discord
from discord import app_commands
from core import economy
from core.emoji import EMOJI as E
import strings as S
@@ -611,7 +612,7 @@ def register_economy_games_commands(
# -----------------------------------------------------------------------
# /slots
# -----------------------------------------------------------------------
_SLOTS_SPIN = "<a:TipiSLOTS:1483444233863037101>"
_SLOTS_SPIN = E["TipiSLOTS"]
_SLOTS_DELAY = 0.7
def _slots_embed(

View File

@@ -22,12 +22,13 @@ def register_economy_income_commands(
) -> None:
@tree.command(name="daily", description=S.CMD["daily"])
async def cmd_daily(interaction: discord.Interaction):
await interaction.response.defer()
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)
await interaction.followup.send(S.MSG_BANNED, ephemeral=True)
elif res["reason"] == "cooldown":
await interaction.response.send_message(
await interaction.followup.send(
S.CD_MSG["daily"].format(ts=cd_ts(res["remaining"])),
ephemeral=True,
)
@@ -51,7 +52,7 @@ def register_economy_income_commands(
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)
await interaction.followup.send(embed=embed)
asyncio.create_task(maybe_remind(interaction.user.id, "daily"))
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["daily"]))
@@ -59,17 +60,18 @@ def register_economy_income_commands(
async def cmd_work(interaction: discord.Interaction):
if await check_cmd_rate(interaction):
return
await interaction.response.defer()
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)
await interaction.followup.send(S.MSG_BANNED, ephemeral=True)
elif res["reason"] == "cooldown":
await interaction.response.send_message(
await interaction.followup.send(
S.CD_MSG["work"].format(ts=cd_ts(res["remaining"])),
ephemeral=True,
)
elif res["reason"] == "jailed":
await interaction.response.send_message(
await interaction.followup.send(
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
ephemeral=True,
)
@@ -84,7 +86,7 @@ def register_economy_income_commands(
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)
await interaction.followup.send(embed=embed)
asyncio.create_task(maybe_remind(interaction.user.id, "work"))
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["work"]))
@@ -92,12 +94,13 @@ def register_economy_income_commands(
async def cmd_beg(interaction: discord.Interaction):
if await check_cmd_rate(interaction):
return
await interaction.response.defer()
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)
await interaction.followup.send(S.MSG_BANNED, ephemeral=True)
elif res["reason"] == "cooldown":
await interaction.response.send_message(
await interaction.followup.send(
S.CD_MSG["beg"].format(ts=cd_ts(res["remaining"])),
ephemeral=True,
)
@@ -114,7 +117,7 @@ def register_economy_income_commands(
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)
await interaction.followup.send(embed=embed)
asyncio.create_task(maybe_remind(interaction.user.id, "beg"))
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["beg"]))
@@ -122,17 +125,18 @@ def register_economy_income_commands(
async def cmd_crime(interaction: discord.Interaction):
if await check_cmd_rate(interaction):
return
await interaction.response.defer()
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)
await interaction.followup.send(S.MSG_BANNED, ephemeral=True)
elif res["reason"] == "cooldown":
await interaction.response.send_message(
await interaction.followup.send(
S.CD_MSG["crime"].format(ts=cd_ts(res["remaining"])),
ephemeral=True,
)
elif res["reason"] == "jailed":
await interaction.response.send_message(
await interaction.followup.send(
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
ephemeral=True,
)
@@ -161,7 +165,7 @@ def register_economy_income_commands(
+ S.CRIME_UI["balance"].format(balance=coin(res["balance"])),
color=0xED4245,
)
await interaction.response.send_message(embed=embed)
await interaction.followup.send(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"]))
@@ -180,27 +184,28 @@ def register_economy_income_commands(
await interaction.response.send_message(S.ERR["rob_house_blocked"], ephemeral=True)
return
await interaction.response.defer()
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)
await interaction.followup.send(S.MSG_BANNED, ephemeral=True)
elif res["reason"] == "cooldown":
await interaction.response.send_message(
await interaction.followup.send(
S.CD_MSG["rob"].format(ts=cd_ts(res["remaining"])),
ephemeral=True,
)
elif res["reason"] == "jailed":
await interaction.response.send_message(
await interaction.followup.send(
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
ephemeral=True,
)
elif res["reason"] == "broke":
await interaction.response.send_message(
await interaction.followup.send(
S.ERR["rob_too_poor"].format(name=sihtmärk.display_name),
ephemeral=True,
)
elif res["reason"] == "target_jailed":
await interaction.response.send_message(
await interaction.followup.send(
S.ERR["rob_target_jailed"].format(name=sihtmärk.display_name),
ephemeral=True,
)
@@ -242,7 +247,7 @@ def register_economy_income_commands(
),
color=0xED4245,
)
await interaction.response.send_message(embed=embed)
await interaction.followup.send(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"]))

View File

@@ -13,7 +13,9 @@ from pathlib import Path
import discord
from discord import app_commands
from core.admin import bot_admin_check
import strings as S
from core.admin import bot_admin_check
def register_ops_admin_commands(
@@ -32,7 +34,7 @@ def register_ops_admin_commands(
) -> None:
@tree.command(name="status", description=S.CMD["status"])
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
async def cmd_status(interaction: discord.Interaction):
mem = process.memory_info()
cpu = process.cpu_percent(interval=0.1)
@@ -95,7 +97,7 @@ def register_ops_admin_commands(
@tree.command(name="sync", description=S.CMD["sync"])
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
async def cmd_sync(interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
tree.copy_global_to(guild=guild_obj)
@@ -107,7 +109,7 @@ def register_ops_admin_commands(
@tree.command(name="restart", description=S.CMD["restart"])
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
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)
@@ -117,7 +119,7 @@ def register_ops_admin_commands(
@tree.command(name="shutdown", description=S.CMD["shutdown"])
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
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)
@@ -125,7 +127,7 @@ def register_ops_admin_commands(
@tree.command(name="pause", description=S.CMD["pause"])
@app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True)
@bot_admin_check()
async def cmd_pause(interaction: discord.Interaction):
paused = not get_paused()
set_paused(paused)

View File

@@ -42,6 +42,30 @@ BIRTHDAY_CHANNEL_ID = (
BIRTHDAY_WINDOW_DAYS = int(os.getenv("BIRTHDAY_WINDOW_DAYS", "7"))
BASE_ROLE_IDS: list[int] = [1478304631930228779, 1478302278862766190]
def _parse_admin_roles(raw: str) -> dict[int, set[int]]:
"""Parse DISCORD_ADMIN_ROLES env var as "guild_id:role_id[:role_id...],guild_id:role_id...".
Multiple admin roles per guild are colon-separated; guild entries are comma-separated.
Repeating a guild_id across entries merges its roles.
"""
result: dict[int, set[int]] = {}
for entry in raw.split(","):
entry = entry.strip()
if not entry:
continue
parts = entry.split(":")
if len(parts) < 2 or not all(p.strip() for p in parts):
raise SystemExit(
f"DISCORD_ADMIN_ROLES: expected 'guild_id:role_id[:role_id...]', got {entry!r}"
)
guild_id = int(parts[0].strip())
result.setdefault(guild_id, set()).update(int(p.strip()) for p in parts[1:])
return result
BOT_ADMIN_ROLES: dict[int, set[int]] = _parse_admin_roles(os.getenv("DISCORD_ADMIN_ROLES", ""))
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", "")

27
core/admin.py Normal file
View File

@@ -0,0 +1,27 @@
from __future__ import annotations
import discord
from discord import app_commands
import config
def is_bot_admin(member: discord.abc.User | None) -> bool:
"""True when the member has any of the configured admin roles for their guild."""
if not isinstance(member, discord.Member) or member.guild is None:
return False
admin_role_ids = config.BOT_ADMIN_ROLES.get(member.guild.id)
if not admin_role_ids:
return False
return any(r.id in admin_role_ids for r in member.roles)
def bot_admin_check():
"""Slash-command decorator that gates execution behind ``is_bot_admin``."""
async def predicate(interaction: discord.Interaction) -> bool:
if is_bot_admin(interaction.user):
return True
raise app_commands.MissingPermissions(["bot_admin_role"])
return app_commands.check(predicate)

View File

@@ -17,6 +17,7 @@ import aiohttp
from . import pb_client
from .pb_client import DatabaseError
from .emoji import EMOJI as E
import strings
@@ -29,13 +30,9 @@ def _txn(event: str, **fields) -> None:
_txn_log.info("%-16s %s", event, body)
# ---------------------------------------------------------------------------
# Emoji config
# To use your custom Discord emoji replace COIN with the full tag, e.g.:
# COIN = "<:tipicoin:1234567890123456789>"
# ---------------------------------------------------------------------------
COIN = "<:TipiCOIN:1483000209188589628>"
PP_EMOJI = "<:TipiFIRE:1483431381668335687>"
# Per-profile emoji values live in core/emoji.py; add new IDs there.
COIN = E["TipiCOIN"]
PP_EMOJI = E["TipiFIRE"]
PRESTIGE_ROLE = "TipiPRESTIGE"
PRESTIGE_MIN_LEVEL = 30 # minimum level required to prestige
@@ -52,99 +49,99 @@ class ShopItem(TypedDict):
SHOP: dict[str, ShopItem] = {
"gaming_hiir": {
"name": "Mängurihiir",
"emoji": "<:TipiHIIR:1483004306012504128>",
"emoji": E["TipiHIIR"],
"cost": 500,
"description": strings.ITEM_DESCRIPTIONS["gaming_hiir"],
},
"hiirematt": {
"name": "Hiirematt",
"emoji": "<:TipiMATT:1483387697132208128>",
"emoji": E["TipiMATT"],
"cost": 600,
"description": strings.ITEM_DESCRIPTIONS["hiirematt"],
},
"korvaklapid": {
"name": "K\u00f5rvaklapid",
"emoji": "<:TipiKLAPID:1483387694083084349>",
"emoji": E["TipiKLAPID"],
"cost": 1200,
"description": strings.ITEM_DESCRIPTIONS["korvaklapid"],
},
"lan_pass": {
"name": "LAN pilet",
"emoji": "<:TipiPILET:1483004308353060904>",
"emoji": E["TipiPILET"],
"cost": 1200,
"description": strings.ITEM_DESCRIPTIONS["lan_pass"],
},
"energiajook": {
"name": "Red Bull",
"emoji": "<:TipiBULL:1483004310924300409>",
"emoji": E["TipiBULL"],
"cost": 800,
"description": strings.ITEM_DESCRIPTIONS["energiajook"],
},
"gaming_laptop": {
"name": "Bot Farm",
"emoji": "<:TipiLAP:1483004307161874566>",
"emoji": E["TipiLAP"],
"cost": 1500,
"description": strings.ITEM_DESCRIPTIONS["gaming_laptop"],
},
"anticheat": {
"name": "Anticheat",
"emoji": "<:TipiVAC:1483004309510819860>",
"emoji": E["TipiVAC"],
"cost": 1000,
"description": strings.ITEM_DESCRIPTIONS["anticheat"],
},
# ----- Tier 2 -----
"reguleeritav_laud": {
"name": "Reguleeritav laud",
"emoji": "<:TipiLAUD:1483387695576125440>",
"emoji": E["TipiLAUD"],
"cost": 3500,
"description": strings.ITEM_DESCRIPTIONS["reguleeritav_laud"],
},
"jellyfin": {
"name": "Jellyfin server",
"emoji": "<:TipiSERVER:1483387701032910969>",
"emoji": E["TipiSERVER"],
"cost": 4000,
"description": strings.ITEM_DESCRIPTIONS["jellyfin"],
},
"mikrofon": {
"name": "Eraldiseisev mikrofon",
"emoji": "<:TipiMIC:1483387698499551313>",
"emoji": E["TipiMIC"],
"cost": 2800,
"description": strings.ITEM_DESCRIPTIONS["mikrofon"],
},
"klaviatuur": {
"name": "Mehaaniline klaviatuur",
"emoji": "<:TipiKLAVA:1483014339228078140>",
"emoji": E["TipiKLAVA"],
"cost": 1800,
"description": strings.ITEM_DESCRIPTIONS["klaviatuur"],
},
"monitor": {
"name": "Ultralai monitor",
"emoji": "<:TipiMONITOR:1483014340327243908>",
"emoji": E["TipiMONITOR"],
"cost": 2500,
"description": strings.ITEM_DESCRIPTIONS["monitor"],
},
"cat6": {
"name": "Cat6 kaabel",
"emoji": "<:TipiCAT:1483014337663602718>",
"emoji": E["TipiCAT"],
"cost": 3500,
"description": strings.ITEM_DESCRIPTIONS["cat6"],
},
# ----- Tier 3 -----
"monitor_360": {
"name": "360Hz monitor",
"emoji": "<:TipiMONITOR2:1483387699514839162>",
"emoji": E["TipiMONITOR2"],
"cost": 7500,
"description": strings.ITEM_DESCRIPTIONS["monitor_360"],
},
"karikas": {
"name": "TipiLAN karikas",
"emoji": "<:TipiKARIKAS:1483014841148112977>",
"emoji": E["TipiKARIKAS"],
"cost": 6000,
"description": strings.ITEM_DESCRIPTIONS["karikas"],
},
"gaming_tool": {
"name": "Gaming tool",
"emoji": "<:TipiTOOL:1483014341648187613>",
"emoji": E["TipiTOOL"],
"cost": 9000,
"description": strings.ITEM_DESCRIPTIONS["gaming_tool"],
},
@@ -204,7 +201,7 @@ class PrestigeItem(TypedDict):
PRESTIGE_SHOP: dict[str, PrestigeItem] = {
"coin_mult": {
"emoji": "<:TipiCOIN:1483000209188589628>",
"emoji": E["TipiCOIN"],
"max_level": 5,
"pp_cost": 5,
"effect": 0.08,
@@ -317,16 +314,18 @@ LEVEL_ROLES: list[tuple[int, str]] = [
def get_level(exp: int) -> int:
"""Level = max(1, floor(sqrt(exp/6))).
Level 5 @ 150 EXP, 10 @ 600, 20 @ 2400, 30 @ 5400."""
return max(1, int(math.sqrt(max(0, exp) / 6)))
"""Level = max(1, floor(sqrt(exp/10))).
Level 5 @ 250 EXP, 10 @ 1000, 20 @ 4000, 30 @ 9000."""
return max(1, int(math.sqrt(max(0, exp) / 10)))
def exp_for_level(level: int) -> int:
"""Minimum cumulative EXP to reach this level. level^2 * 6."""
"""Minimum cumulative EXP to reach this level.
Recurrence: exp_for_level(L) = L*20 - 10 + exp_for_level(L-1), base 0.
Closed form: 10*level^2."""
if level <= 1:
return 0
return level * level * 6
return 10 * level * level
def level_role_name(level: int) -> str:
@@ -1480,21 +1479,21 @@ async def do_rps_pvp_refund(user_id: int, bet: int) -> dict:
# /slots
# ---------------------------------------------------------------------------
_SLOTS_SYMBOLS: list[tuple[str, int]] = [
("<:TipiHEART:1483431377561976853>", 27),
("<:TipiFIRE:1483431381668335687>", 22),
("<:TipiTROLL:1483431380166774895>", 18),
("<:TipICRY:1483431288852709387>", 15),
("<:TipiSKULL:1483431378929451028>", 10),
("<:TipiKARIKAS:1483014841148112977>", 8),
(E["TipiHEART"], 27),
(E["TipiFIRE"], 22),
(E["TipiTROLL"], 18),
(E["TipICRY"], 15),
(E["TipiSKULL"], 10),
(E["TipiKARIKAS"], 8),
]
_SLOTS_JACKPOT = "<:TipiKARIKAS:1483014841148112977>"
_SLOTS_JACKPOT = E["TipiKARIKAS"]
_SLOTS_TRIPLE_MULT: dict[str, int] = {
"<:TipiHEART:1483431377561976853>": 4,
"<:TipiFIRE:1483431381668335687>": 5,
"<:TipiTROLL:1483431380166774895>": 7,
"<:TipICRY:1483431288852709387>": 10,
"<:TipiSKULL:1483431378929451028>": 15,
"<:TipiKARIKAS:1483014841148112977>": 25, # jackpot
E["TipiHEART"]: 4,
E["TipiFIRE"]: 5,
E["TipiTROLL"]: 7,
E["TipICRY"]: 10,
E["TipiSKULL"]: 15,
E["TipiKARIKAS"]: 25, # jackpot
}

97
core/emoji.py Normal file
View File

@@ -0,0 +1,97 @@
"""Custom Discord emoji registry, keyed by symbolic name and resolved per BOT_PROFILE.
Emojis are uploaded as application emojis via the Discord Developer Portal
and are scoped to a single bot application. Dev and production are separate
applications, so the same logical emoji has a different ID in each — hence
two dicts below, selected by BOT_PROFILE.
To add a new emoji: upload it to both applications in the dev portal, grab
the two IDs, and add one line to each dict.
"""
from __future__ import annotations
from config import BOT_PROFILE
_DEV: dict[str, str] = {
"TipiCOIN": "<:TipiCOIN:1483000209188589628>",
"TipiFIRE": "<:TipiFIRE:1483431381668335687>",
"TipiHIIR": "<:TipiHIIR:1483004306012504128>",
"TipiMATT": "<:TipiMATT:1483387697132208128>",
"TipiKLAPID": "<:TipiKLAPID:1483387694083084349>",
"TipiPILET": "<:TipiPILET:1483004308353060904>",
"TipiBULL": "<:TipiBULL:1483004310924300409>",
"TipiLAP": "<:TipiLAP:1483004307161874566>",
"TipiVAC": "<:TipiVAC:1483004309510819860>",
"TipiLAUD": "<:TipiLAUD:1483387695576125440>",
"TipiSERVER": "<:TipiSERVER:1483387701032910969>",
"TipiMIC": "<:TipiMIC:1483387698499551313>",
"TipiKLAVA": "<:TipiKLAVA:1483014339228078140>",
"TipiMONITOR": "<:TipiMONITOR:1483014340327243908>",
"TipiCAT": "<:TipiCAT:1483014337663602718>",
"TipiMONITOR2": "<:TipiMONITOR2:1483387699514839162>",
"TipiKARIKAS": "<:TipiKARIKAS:1483014841148112977>",
"TipiTOOL": "<:TipiTOOL:1483014341648187613>",
"TipiHEART": "<:TipiHEART:1483431377561976853>",
"TipiTROLL": "<:TipiTROLL:1483431380166774895>",
"TipICRY": "<:TipICRY:1483431288852709387>",
"TipiSKULL": "<:TipiSKULL:1483431378929451028>",
"TipiDICE": "<a:TipiDICE:1485923107108556950>",
"TipiYKS": "<:TipiYKS:1483103190491856916>",
"TipiKAKS": "<:TipiKAKS:1483103215841972404>",
"TipiKOLM": "<:TipiKOLM:1483103217846980781>",
"TipiNELI": "<:TipiNELI:1483103237585240114>",
"TipiVIIS": "<:TipiVIIS:1483103239036469289>",
"TipiKUUS": "<:TipiKUUS:1483103253163020348>",
"TipiSLOTS": "<a:TipiSLOTS:1483444233863037101>",
}
# Production application emoji IDs (from the TipiBOT application's dev-portal
# Emojis tab). The display name in <:name:id> is cosmetic — Discord resolves
# by ID — so we keep the same logical names as _DEV even when the prod portal
# uses a different upload name (e.g. prod's "TipiFIVE" → key "TipiVIIS").
_ECONOMY: dict[str, str] = {
"TipiCOIN": "<:TipiCOIN:1511754551747940485>",
"TipiFIRE": "<:TipiFIRE:1511754615761272862>",
"TipiHIIR": "<:TipiHIIR:1511754556105822218>",
"TipiMATT": "<:TipiMATT:1511754595448131665>",
"TipiKLAPID": "<:TipiKLAPID:1511754589798666433>",
"TipiPILET": "<:TipiPILET:1511754560593727652>",
"TipiBULL": "<:TipiBULL:1511754564179595264>",
"TipiLAP": "<:TipiLAP:1511754558970269907>",
"TipiVAC": "<:TipiVAC:1511754562413924442>",
"TipiLAUD": "<:TipiLAUD:1511754592759713985>",
"TipiSERVER": "<:TipiSERVER:1511754601186066534>",
"TipiMIC": "<:TipiMIC:1511754597016801310>",
"TipiKLAVA": "<:TipiKLAVA:1511754567648542780>",
"TipiMONITOR": "<:TipiMONITOR:1511754570722971868>",
"TipiCAT": "<:TipiCAT:1511754566092193853>",
"TipiMONITOR2": "<:TipiMONITOR2:1511754598732402689>",
"TipiKARIKAS": "<:TipiKARIKAS:1511754574405435502>",
"TipiTOOL": "<:TipiTOOL:1511754572522061874>",
"TipiHEART": "<:TipiHEART:1511754608299737139>",
"TipiTROLL": "<:TipiTROLL:1511754612775063832>",
"TipICRY": "<:TipICRY:1511754603308515368>",
"TipiSKULL": "<:TipiSKULL:1511754610195566622>",
"TipiDICE": "<a:TipiDICE:1511753607119376504>",
"TipiYKS": "<:TipiYKS:1511754576368373951>",
"TipiKAKS": "<:TipiKAKS:1511754577928523997>",
"TipiKOLM": "<:TipiKOLM:1511754581078442005>",
"TipiNELI": "<:TipiNELI:1511754582571880509>",
"TipiVIIS": "<:TipiVIIS:1511754584182227005>",
"TipiKUUS": "<:TipiKUUS:1511754586262736977>",
"TipiSLOTS": "<a:TipiSLOTS:1511754521188106431>",
}
_EMOJI_SETS = {
"dev": _DEV,
"economy": _ECONOMY,
}
EMOJI: dict[str, str] = _EMOJI_SETS[BOT_PROFILE]
_missing = set(_DEV) - set(EMOJI)
if _missing:
raise RuntimeError(f"Emoji set {BOT_PROFILE!r} missing keys: {sorted(_missing)}")

View File

@@ -2,9 +2,11 @@
from __future__ import annotations
import calendar
import logging
from datetime import datetime, date
from dataclasses import dataclass, field
from zoneinfo import ZoneInfo
import discord
@@ -13,6 +15,20 @@ from . import sheets
log = logging.getLogger(__name__)
_PLACEHOLDER = {"-", "x", "n/a", "none", "ei"}
_TZ = ZoneInfo("Europe/Tallinn")
def today_local() -> date:
"""Today's date in Europe/Tallinn — the bot's operational timezone, independent of host TZ."""
return datetime.now(_TZ).date()
def _shift_year_safe(d: date, year: int) -> date:
"""Move `d` to `year`; Feb 29 falls back to Feb 28 when the target year is non-leap."""
try:
return d.replace(year=year)
except ValueError:
return d.replace(year=year, day=28)
def _is_placeholder(val: str) -> bool:
@@ -34,6 +50,7 @@ class SyncResult:
roles_added: list[str] = field(default_factory=list)
roles_removed: list[str] = field(default_factory=list)
birthday_soon: bool = False
birthday_today: bool = False
not_found: bool = False
errors: list[str] = field(default_factory=list)
synced: bool = False # True when no errors; caller writes this to the sheet
@@ -71,7 +88,7 @@ def _parse_birthday(raw: str) -> date | None:
raw = str(raw).strip()
if _is_placeholder(raw):
return None
today = date.today()
today = today_local()
for fmt, has_year in [("%d/%m/%Y", True), ("%Y-%m-%d", True), ("%m-%d", False)]:
try:
parsed = datetime.strptime(raw, fmt).date()
@@ -89,21 +106,32 @@ def _is_birthday_soon(birthday_str: str, window_days: int | None = None) -> bool
if bday is None:
return False
window = window_days or config.BIRTHDAY_WINDOW_DAYS
today = date.today()
this_year_bday = bday.replace(year=today.year)
today = today_local()
this_year_bday = _shift_year_safe(bday, today.year)
if this_year_bday < today:
this_year_bday = bday.replace(year=today.year + 1)
this_year_bday = _shift_year_safe(bday, today.year + 1)
delta = (this_year_bday - today).days
return 0 <= delta <= window
def is_birthday_today(birthday_str: str) -> bool:
"""Return True if today is the member's birthday (any supported date format)."""
"""Return True if today is the member's birthday (any supported date format).
Feb 29 babies are observed on Feb 28 in non-leap years.
"""
bday = _parse_birthday(birthday_str)
if bday is None:
return False
today = date.today()
return bday.month == today.month and bday.day == today.day
today = today_local()
if bday.month == today.month and bday.day == today.day:
return True
if (
bday.month == 2 and bday.day == 29
and today.month == 2 and today.day == 28
and not calendar.isleap(today.year)
):
return True
return False
async def sync_member(
@@ -186,8 +214,9 @@ async def sync_member(
# --- Birthday check ---
birthday_str = str(row.get("Sünnipäev", "")).strip()
if not _is_placeholder(birthday_str) and _is_birthday_soon(birthday_str):
result.birthday_soon = True
if not _is_placeholder(birthday_str):
result.birthday_today = is_birthday_today(birthday_str)
result.birthday_soon = _is_birthday_soon(birthday_str)
# --- Mark synced (caller is responsible for writing to sheet) ---
result.synced = not bool(result.errors)

View File

@@ -4,6 +4,8 @@ Edit this file to change any message, description, or flavour text
without touching any logic code.
"""
from core.emoji import EMOJI as E
# ---------------------------------------------------------------------------
# Flavour text
# ---------------------------------------------------------------------------
@@ -256,22 +258,22 @@ HELP_CATEGORIES: dict[str, dict] = {
"description": "TipiBOTi poe esemed ja nende efektid",
"color": 0xF4C430,
"fields": [
("<:TipiHIIR:1483004306012504128> Mängurihiir - 500 ⬡", "Teeni töötades 50% rohkem TipiCOINe."),
("<:TipiMATT:1483387697132208128> XL hiirematt - 600 ⬡", "Kerjamise ooteaeg 5min → 3min."),
("<:TipiKLAPID:1483387694083084349> Kõrvaklapid - 1200 ⬡", "Päevase boonuse ooteaeg 20h → 18h."),
("<:TipiPILET:1483004308353060904> LAN pilet (2025) - 1200 ⬡", "Päevane boonus on duubeldatud."),
("<:TipiVAC:1483004309510819860> Anticheat - 750 ⬡", "Röövimine sinu vastu ebaõnnestub. Pärast 2 kasutust pead ostma uue."),
("<:TipiBULL:1483004310924300409> Red Bull - 800 ⬡", "30% tõenäosus, et teenid töötades 3x rohkem."),
("<:TipiLAP:1483004307161874566> Botikoobas - 1500 ⬡", "RTX 5090 jooksutab botte 24/7. Päevane boonus genereerib 5% intressi sinu saldo pealt."),
("<:TipiLAUD:1483387695576125440> Reguleeritav laud - 3500 ⬡ *(T2)*", "/work teenib 25% rohkem (stackib mängurihiirega)."),
("<:TipiSERVER:1483387701032910969> Jellyfin server - 4000 ⬡ *(T2)*", "Röövimise edu tõenäosus 45% → 60%."),
("<:TipiMIC:1483387698499551313> Mikrofon - 2800 ⬡ *(T2)*", "Teeni 30% rohkem eduka /crime puhul."),
("<:TipiKLAVA:1483014339228078140> Mehhaaniline klaviatuur - 1800 ⬡ *(T2)*", "/beg teenib 2x rohkem."),
("<:TipiMONITOR:1483014340327243908> Ultralai monitor - 2500 ⬡ *(T2)*", "/work ooteaeg: 1h → 40min."),
("<:TipiCAT:1483014337663602718> CAT6 netikaabel - 3500 ⬡ *(T2)*", "/crime edu tõenäosus tõuseb 60% → 75%."),
("<:TipiMONITOR2:1483387699514839162> 360hz monitor - 7500 ⬡ *(T3)*", "Mänguautomaadi jackpot 10x → 15x, kolmik 4x → 6x."),
("<:TipiKARIKAS:1483014841148112977> TipiLANi trofee - 6000 ⬡ *(T3)*", "Streak ei nulli, kui sa mõne päeva vahele jätad."),
("<:TipiTOOL:1483014341648187613> Mänguritool - 9000 ⬡ *(T3)*", "/crime ebaõnnestumine ei saada sind vanglasse."),
(f"{E['TipiHIIR']} Mängurihiir - 500 ⬡", "Teeni töötades 50% rohkem TipiCOINe."),
(f"{E['TipiMATT']} XL hiirematt - 600 ⬡", "Kerjamise ooteaeg 5min → 3min."),
(f"{E['TipiKLAPID']} Kõrvaklapid - 1200 ⬡", "Päevase boonuse ooteaeg 20h → 18h."),
(f"{E['TipiPILET']} LAN pilet (2025) - 1200 ⬡", "Päevane boonus on duubeldatud."),
(f"{E['TipiVAC']} Anticheat - 750 ⬡", "Röövimine sinu vastu ebaõnnestub. Pärast 2 kasutust pead ostma uue."),
(f"{E['TipiBULL']} Red Bull - 800 ⬡", "30% tõenäosus, et teenid töötades 3x rohkem."),
(f"{E['TipiLAP']} Botikoobas - 1500 ⬡", "RTX 5090 jooksutab botte 24/7. Päevane boonus genereerib 5% intressi sinu saldo pealt."),
(f"{E['TipiLAUD']} Reguleeritav laud - 3500 ⬡ *(T2)*", "/work teenib 25% rohkem (stackib mängurihiirega)."),
(f"{E['TipiSERVER']} Jellyfin server - 4000 ⬡ *(T2)*", "Röövimise edu tõenäosus 45% → 60%."),
(f"{E['TipiMIC']} Mikrofon - 2800 ⬡ *(T2)*", "Teeni 30% rohkem eduka /crime puhul."),
(f"{E['TipiKLAVA']} Mehhaaniline klaviatuur - 1800 ⬡ *(T2)*", "/beg teenib 2x rohkem."),
(f"{E['TipiMONITOR']} Ultralai monitor - 2500 ⬡ *(T2)*", "/work ooteaeg: 1h → 40min."),
(f"{E['TipiCAT']} CAT6 netikaabel - 3500 ⬡ *(T2)*", "/crime edu tõenäosus tõuseb 60% → 75%."),
(f"{E['TipiMONITOR2']} 360hz monitor - 7500 ⬡ *(T3)*", "Mänguautomaadi jackpot 10x → 15x, kolmik 4x → 6x."),
(f"{E['TipiKARIKAS']} TipiLANi trofee - 6000 ⬡ *(T3)*", "Streak ei nulli, kui sa mõne päeva vahele jätad."),
(f"{E['TipiTOOL']} Mänguritool - 9000 ⬡ *(T3)*", "/crime ebaõnnestumine ei saada sind vanglasse."),
],
},
"games": {
@@ -347,10 +349,10 @@ REMINDER_OPTS: list[tuple[str, str, str]] = [
# ---------------------------------------------------------------------------
SLOTS_TIERS: dict[str, tuple[str, int]] = {
"jackpot": ("<:TipiFIRE:1483431381668335687> JACKPOT!!!", 0xF4C430),
"jackpot": (f"{E['TipiFIRE']} JACKPOT!!!", 0xF4C430),
"triple": ("🎰 Kolmik!", 0x57F287),
"pair": ("🎰 Paar", 0x99AAB5),
"miss": ("<:TipICRY:1483431288852709387> Ei õnnestunud", 0xED4245),
"miss": (f"{E['TipICRY']} Ei õnnestunud", 0xED4245),
}
# ---------------------------------------------------------------------------
@@ -534,22 +536,22 @@ TITLE: dict[str, str] = {
"daily": "📅 Päevane boonus",
"work": "💼 Töö",
"beg": "🙏 Kerjamine",
"crime_win": "<:TipiFIRE:1483431381668335687> Kuritegu õnnestus!",
"crime_fail": "<:TipiTROLL:1483431380166774895> Vahele jäid!",
"rob_win": "<:TipiFIRE:1483431381668335687> Rööv õnnestus!",
"rob_fail": "<:TipiTROLL:1483431380166774895> Rööv ebaõnnestus!",
"rob_anticheat": "<:TipiVAC:1483004309510819860> Anticheat peatas sind!",
"crime_win": f"{E['TipiFIRE']} Kuritegu õnnestus!",
"crime_fail": f"{E['TipiTROLL']} Vahele jäid!",
"rob_win": f"{E['TipiFIRE']} Rööv õnnestus!",
"rob_fail": f"{E['TipiTROLL']} Rööv ebaõnnestus!",
"rob_anticheat": f"{E['TipiVAC']} Anticheat peatas sind!",
"jailbreak": "🎲 Vanglast põgenemine",
"jailbreak_free": "🎲 <:TipiFIRE:1483431381668335687> DUUBEL! Oled vaba!",
"jailbreak_fail": "<:TipICRY:1483431288852709387> Kolm katset läbi!",
"jailbreak_miss": "🎲 <:TipICRY:1483431288852709387> Ei saanud duublit ({tries}/{max})",
"jailbreak_free": f"🎲 {E['TipiFIRE']} DUUBEL! Oled vaba!",
"jailbreak_fail": f"{E['TipICRY']} Kolm katset läbi!",
"jailbreak_miss": "🎲 " + E["TipICRY"] + " Ei saanud duublit ({tries}/{max})",
"jailbreak_bail": "💸 Kautsjon",
"give": "<:TipiHEART:1483431377561976853> TipiCOINi ülekanne",
"give": f"{E['TipiHEART']} TipiCOINi ülekanne",
"stats": "📊 Mängustatistika",
"leaderboard_coins": "🪙 TipiBOTi edetabel - Mündid",
"leaderboard_exp": "📊 TipiBOTi edetabel - EXP / Tase",
"leaderboard_season": "🏆 TipiBOTi edetabel - Hooaja EXP",
"leaderboard_prestige": "<:TipiFIRE:1483431381668335687> TipiBOTi edetabel - Prestiiž",
"leaderboard_prestige": f"{E['TipiFIRE']} TipiBOTi edetabel - Prestiiž",
"leaderboard_wagered": "🎲 TipiBOTi edetabel - Hasartmängud",
"leaderboard_fish": "🎣 TipiBOTi edetabel - Kalapüük",
"rps": "⚔️ Kivi, Paber, Käärid",
@@ -560,26 +562,26 @@ TITLE: dict[str, str] = {
"rps_duel_expire": "⚔️ KPK duell - aegus",
"rps_duel_decline": "⚔️ KPK duell - keelduti",
"heist_lobby": "🔫 Grupirööv - kogunemine",
"heist_win": "<:TipiFIRE:1483431381668335687> Grupirööv õnnestus!",
"heist_fail": "<:TipiSKULL:1483431378929451028> Grupirööv ebaõnnestus!",
"heist_win": f"{E['TipiFIRE']} Grupirööv õnnestus!",
"heist_fail": f"{E['TipiSKULL']} Grupirööv ebaõnnestus!",
"heist_cancel": "🔫 Grupirööv tühistatud",
"request": "<:TipiHEART:1483431377561976853> Rahataotlus",
"request": f"{E['TipiHEART']} Rahataotlus",
"reminders": "⏰ Meeldetuletused",
"cooldowns": "⏱️ Sinu ooteajad",
"adminseason": "🏆 Hooaeg lõppes!",
"economysetup": "⚙️ Majanduse seadistamine",
"blackjack": "🃏 Blackjack",
"blackjack_bj": "🃏 <:TipiFIRE:1483431381668335687> BLACKJACK!",
"blackjack_win": "<:TipiFIRE:1483431381668335687> Võitsid!",
"blackjack_lose": "<:TipiSKULL:1483431378929451028> Kaotasid!",
"blackjack_bust": "<:TipiSKULL:1483431378929451028> Üle 21 - kaotasid!",
"blackjack_bj": f"🃏 {E['TipiFIRE']} BLACKJACK!",
"blackjack_win": f"{E['TipiFIRE']} Võitsid!",
"blackjack_lose": f"{E['TipiSKULL']} Kaotasid!",
"blackjack_bust": f"{E['TipiSKULL']} Üle 21 - kaotasid!",
"blackjack_push": "🤝 Viik!",
"blackjack_dbust": "<:TipiSKULL:1483431378929451028> Üle 21 - mõlemad kaotasid!",
"blackjack_dwin": "<:TipiFIRE:1483431381668335687> Topeltpanus võitis!",
"blackjack_dbust": f"{E['TipiSKULL']} Üle 21 - mõlemad kaotasid!",
"blackjack_dwin": f"{E['TipiFIRE']} Topeltpanus võitis!",
"prestige_confirm": "🔥 Prestiiž - kinnita",
"prestige_success": "<:TipiFIRE:1483431381668335687> Prestiiž {level} saavutatud!",
"prestige_success": E["TipiFIRE"] + " Prestiiž {level} saavutatud!",
"prestige_too_low": "❌ Prestiiž pole saadaval",
"prestige_shop": "<:TipiFIRE:1483431381668335687> Prestiižipood",
"prestige_shop": f"{E['TipiFIRE']} Prestiižipood",
"prestige_buy_ok": "✅ Uuendus ostetud!",
"fish_cast": "🎣 Otsid kala...",
"fish_bite": "🐟 KALA NÄKKAB!",
@@ -609,8 +611,8 @@ ERR: dict[str, str] = {
"heist_active": "❌ Serveris on juba aktiivne grupirööv käimas! Oota, kuni see lõpeb.",
"heist_full": "❌ Grupirööv on täis!",
"heist_min_players": "❌ Grupiröövi alustamiseks on vaja vähemalt **{min}** osalejat.",
"broke": "<:TipICRY:1483431288852709387> Sul pole piisavalt TipiCOINe. Saldo: {bal}",
"broke_need": "<:TipICRY:1483431288852709387> Sul pole piisavalt TipiCOINe. Vajad veel {need}.",
"broke": E["TipICRY"] + " Sul pole piisavalt TipiCOINe. Saldo: {bal}",
"broke_need": E["TipICRY"] + " Sul pole piisavalt TipiCOINe. Vajad veel {need}.",
"item_owned": "❌ Sul on see ese juba olemas.",
"item_not_found": "❌ Eset ei leitud.",
"item_level_req": "🔒 Selle eseme ostmiseks vajad **taset {min_level}** (sul on tase {user_level}). Teeni EXP-id kõiki käske kasutades.",
@@ -653,7 +655,7 @@ CD_MSG: dict[str, str] = {
"rob": "⏳ Saad uuesti röövida {ts}.",
"heist": "⏳ Saad uuesti heisti teha {ts}.",
"heist_global": "⏳ Pangahoidla alles kosub eelmisest röövist. Järgmine heist võimalik {ts}.",
"jailed": "<:TipiTROLL:1483431380166774895> Oled vangis! Pääsed välja {ts}. Kasuta `/jailbreak`, et varem välja pääseda.",
"jailed": E["TipiTROLL"] + " Oled vangis! Pääsed välja {ts}. Kasuta `/jailbreak`, et varem välja pääseda.",
"fish": "🎣 Saad uuesti kalastada {ts}.",
}
@@ -837,8 +839,8 @@ CHANNEL_UI: dict[str, str] = {
DAILY_UI: dict[str, str] = {
"earned": "✅ Said {earned}!",
"interest": "<:TipiLAP:1483004307161874566> Bot Farm tootis: +{interest}",
"vip": "<:TipiPILET:1483004308353060904> LAN pileti boonus rakendus!",
"interest": E["TipiLAP"] + " Bot Farm tootis: +{interest}",
"vip": f"{E['TipiPILET']} LAN pileti boonus rakendus!",
"footer": "Streak: {streak_str} · Saldo: {balance}",
}
@@ -1050,9 +1052,9 @@ RANK_UI: dict[str, str] = {
WORK_UI: dict[str, str] = {
"desc": "Sa {job} ja teenisid {earned}!",
"redbull": "\n<:TipiBULL:1483004310924300409> Red Bull aktiveerus - 3x boonus!",
"hiir": "\n<:TipiHIIR:1483004306012504128> Mängurihiir: +50% palk",
"laud": "\n<:TipiLAUD:1483387695576125440> Reguleeritav laud: +25% palk",
"redbull": f"\n{E['TipiBULL']} Red Bull aktiveerus - 3x boonus!",
"hiir": f"\n{E['TipiHIIR']} Mängurihiir: +50% palk",
"laud": f"\n{E['TipiLAUD']} Reguleeritav laud: +25% palk",
"balance": "\nSaldo: {balance}",
}
@@ -1062,7 +1064,7 @@ WORK_UI: dict[str, str] = {
BEG_UI: dict[str, str] = {
"desc": "Sa {text} ja said {earned}.",
"klaviatuur": "<:TipiKLAVA:1483014339228078140> Mehhaaniline klaviatuur: 2x tulu",
"klaviatuur": f"{E['TipiKLAVA']} Mehhaaniline klaviatuur: 2x tulu",
"balance": "Saldo: {balance}",
}
@@ -1075,8 +1077,8 @@ CRIME_UI: dict[str, str] = {
"fail_base": "Sa {text} ja said trahvi {fine}.",
"fail_jailed": "\n\ud83d\udd12 Oled vangis! P\u00e4\u00e4sed {ts}.",
"fail_shield": "\n\ud83d\udee1\ufe0f Gaming Tool hoidis sind vanglast!",
"mikrofon": "\n<:TipiMIC:1483387698499551313> Mikrofon: +30% saak",
"cat6": "\n<:TipiCAT:1483014337663602718> CAT6: 75% edu t\u00f5en\u00e4osus",
"mikrofon": f"\n{E['TipiMIC']} Mikrofon: +30% saak",
"cat6": f"\n{E['TipiCAT']} CAT6: 75% edu t\u00f5en\u00e4osus",
"balance": "\nSaldo: {balance}",
}
@@ -1116,7 +1118,7 @@ BUY_UI: dict[str, str] = {
JAILBREAK_UI: dict[str, str] = {
"btn_roll": "🎲 Viska täringud ({try_}/{max})",
"rolling_desc": "<:TipiDICE:1485923107108556950> *Täringud lendavad...*",
"rolling_desc": f"{E['TipiDICE']} *Täringud lendavad...*",
"free_desc": "{d1} {d2}\n\n✅ Viskasid duubli - pääsesid vanglast!",
"miss_desc": "{d1} {d2}\n\n{left} katset jäänud. Proovi uuesti!",
"intro_desc": "Oled vangis kuni {ts}.\n\nViska täringuid ja proovi **duublit** saada - siis pääsed tasuta vabaks!\nSul on **{tries} katset**. Ebaõnnestumisel saad valida: maksa kautsjon **(2030% saldost, min 350 ⬡)** või jää vanglasse kuni aja lõpuni.",
@@ -1160,7 +1162,7 @@ LEADERBOARD_UI: dict[str, str] = {
SLOTS_UI: dict[str, str] = {
"playing": "🎰 Mängimas...",
"jackpot_footer": "<:TipiKARIKAS:1483014841148112977> Kolm karikat! +{change}",
"jackpot_footer": E["TipiKARIKAS"] + " Kolm karikat! +{change}",
"triple_footer": "✅ Kolm ühesugust! +{change}",
"pair_footer": "Kaks ühesugust! +{change}",
"miss_footer": "-{amount}",
@@ -1264,7 +1266,7 @@ PRESTIGE_SHOP_DESCRIPTIONS: dict[str, str] = {
PRESTIGE_UI: dict[str, str] = {
"confirm_desc": (
"Oled tasemel **{level}** ({exp} EXP).\n\n"
"Prestiiži korral saad **{pp}** <:TipiFIRE:1483431381668335687> ja kõik lähtestub:\n"
"Prestiiži korral saad **{pp}** " + E["TipiFIRE"] + " ja kõik lähtestub:\n"
"• Saldo, EXP, esemed, ooteajad\n\n"
"**Kalakogu jääb alles!**\n\nKas oled kindel?"
),
@@ -1273,21 +1275,21 @@ PRESTIGE_UI: dict[str, str] = {
"btn_tab_status": "⭐ Prestiiz",
"btn_tab_shop": "🛍️ Uuendused",
"success_desc": (
"Said **{pp}** <:TipiFIRE:1483431381668335687>\n"
"Said **{pp}** " + E["TipiFIRE"] + "\n"
"Prestiiži tase: **{level}**\n"
"Kogutud PP: **{total_pp}** <:TipiFIRE:1483431381668335687>\n\n"
"Kogutud PP: **{total_pp}** " + E["TipiFIRE"] + "\n\n"
"*Kõik lähtestati. Alusta otsast!*"
),
"too_low_desc": "Prestiiži jaoks vajad taset **{required}** (sul on tase {level}).",
"shop_desc": "Sul on **{pp}** <:TipiFIRE:1483431381668335687> · Vajuta nuppu uuenduse ostmiseks",
"shop_desc": "Sul on **{pp}** " + E["TipiFIRE"] + " · Vajuta nuppu uuenduse ostmiseks",
"shop_maxed": "✅ Max",
"shop_level_fmt": "Tase {cur}/{max}",
"shop_cost_fmt": "{cost} <:TipiFIRE:1483431381668335687>",
"buy_success_desc":"**{name}** uuendatud tasemele **{new_level}/{max_level}**!\nPP alles: **{pp}** <:TipiFIRE:1483431381668335687>",
"buy_no_pp": "<:TipICRY:1483431288852709387> Sul pole piisavalt PP. Sul on **{have}**, vajad **{need}** <:TipiFIRE:1483431381668335687>.",
"shop_cost_fmt": "{cost} " + E["TipiFIRE"],
"buy_success_desc":"**{name}** uuendatud tasemele **{new_level}/{max_level}**!\nPP alles: **{pp}** " + E["TipiFIRE"],
"buy_no_pp": E["TipICRY"] + " Sul pole piisavalt PP. Sul on **{have}**, vajad **{need}** " + E["TipiFIRE"] + ".",
"buy_maxed": "❌ See uuendus on juba maksimumtasemel.",
"buy_not_found": "❌ Sellist uuendust ei leitud. Vaata `/prestigeshop`.",
"rank_line": "<:TipiFIRE:1483431381668335687> Prestiiž **{level}** · {pp} PP",
"rank_line": E["TipiFIRE"] + " Prestiiž **{level}** · {pp} PP",
"rank_season": "🏆 Hooaja EXP: **{exp}**",
"btn_buy_upgrade": "{emoji} {name} +1 ({cost} PP)",
"status_footer": "⭐ Prestiiž {level} · {pp} PP",