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

View File

@@ -8,8 +8,9 @@ import discord
from discord import app_commands from discord import app_commands
from core import sheets from core import sheets
from core.admin import bot_admin_check
import strings as S 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): class BirthdayPages(discord.ui.View):
@@ -44,7 +45,7 @@ def _build_birthday_pages(
Returns (pages, start_index) where start_index is the current month. Returns (pages, start_index) where start_index is the current month.
""" """
rows = sheets.get_cache() 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)} 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"]) @tree.command(name="check", description=S.CMD["check"])
@app_commands.guild_only() @app_commands.guild_only()
@app_commands.default_permissions(manage_roles=True) @bot_admin_check()
async def cmd_check(interaction: discord.Interaction): async def cmd_check(interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
@@ -236,7 +237,7 @@ def register_dev_member_commands(
else: else:
already_ok += 1 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 birthday_pings += 1
await announce_birthday(member, bot) await announce_birthday(member, bot)
mark_announced_today(member.id) mark_announced_today(member.id)
@@ -281,7 +282,7 @@ def register_dev_member_commands(
@tree.command(name="member", description=S.CMD["member"]) @tree.command(name="member", description=S.CMD["member"])
@app_commands.guild_only() @app_commands.guild_only()
@app_commands.default_permissions(manage_roles=True) @bot_admin_check()
async def cmd_member(interaction: discord.Interaction, user: discord.Member): async def cmd_member(interaction: discord.Interaction, user: discord.Member):
row = sheets.find_member(user.id, user.name) row = sheets.find_member(user.id, user.name)
if row is None: if row is None:
@@ -298,8 +299,8 @@ def register_dev_member_commands(
for fmt in ["%d/%m/%Y", "%Y-%m-%d"]: for fmt in ["%d/%m/%Y", "%Y-%m-%d"]:
try: try:
bday = datetime.datetime.strptime(bday_str, fmt).date() bday = datetime.datetime.strptime(bday_str, fmt).date()
if 1920 <= bday.year <= datetime.date.today().year: if 1920 <= bday.year <= today_local().year:
today = datetime.date.today() today = today_local()
age = today.year - bday.year - ((today.month, today.day) < (bday.month, bday.day)) 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) embed.add_field(name=S.MEMBER_UI["age_field"], value=S.MEMBER_UI["age_val"].format(age=age), inline=True)
break break

View File

@@ -88,6 +88,6 @@ async def handle_member_join(
log_sync_result(member, result) log_sync_result(member, result)
await sheets.set_synced(member.id, result.synced) 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) await announce_birthday(member, bot)
mark_announced_today(member.id) mark_announced_today(member.id)

View File

@@ -8,6 +8,7 @@ import discord
from discord import app_commands from discord import app_commands
from core import economy from core import economy
from core.admin import bot_admin_check
import strings as S import strings as S
@@ -29,7 +30,7 @@ def register_economy_admin_commands(
) -> None: ) -> None:
@tree.command(name="adminseason", description=S.CMD["adminseason"]) @tree.command(name="adminseason", description=S.CMD["adminseason"])
@app_commands.guild_only() @app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True) @bot_admin_check()
@app_commands.describe(top_n=S.OPT["adminseason_top_n"]) @app_commands.describe(top_n=S.OPT["adminseason_top_n"])
async def cmd_adminseason(interaction: discord.Interaction, top_n: int = 10): async def cmd_adminseason(interaction: discord.Interaction, top_n: int = 10):
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
@@ -73,7 +74,7 @@ def register_economy_admin_commands(
kogus=S.OPT["admincoins_kogus"], kogus=S.OPT["admincoins_kogus"],
põhjus=S.OPT["admin_põhjus"], 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): async def cmd_admincoins(interaction: discord.Interaction, kasutaja: discord.Member, kogus: int, põhjus: str):
if kogus == 0: if kogus == 0:
await interaction.response.send_message(S.ERR["admincoins_zero"], ephemeral=True) 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"], minutid=S.OPT["adminjail_minutid"],
põhjus=S.OPT["admin_põhjus"], 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): async def cmd_adminjail(interaction: discord.Interaction, kasutaja: discord.Member, minutid: int, põhjus: str):
if minutid <= 0: if minutid <= 0:
await interaction.response.send_message(S.ERR["positive_duration"], ephemeral=True) 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"]) @tree.command(name="adminunjail", description=S.CMD["adminunjail"])
@app_commands.guild_only() @app_commands.guild_only()
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"]) @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): async def cmd_adminunjail(interaction: discord.Interaction, kasutaja: discord.Member):
await economy.do_admin_unjail(kasutaja.id, interaction.user.id) await economy.do_admin_unjail(kasutaja.id, interaction.user.id)
await interaction.response.send_message( await interaction.response.send_message(
@@ -148,7 +149,7 @@ def register_economy_admin_commands(
kasutaja=S.OPT["admin_kasutaja"], kasutaja=S.OPT["admin_kasutaja"],
põhjus=S.OPT["admin_põhjus"], 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): async def cmd_adminban(interaction: discord.Interaction, kasutaja: discord.Member, põhjus: str):
if bot.user and kasutaja.id == bot.user.id: if bot.user and kasutaja.id == bot.user.id:
await interaction.response.send_message(S.ERR["admin_ban_bot"], ephemeral=True) 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"]) @tree.command(name="adminunban", description=S.CMD["adminunban"])
@app_commands.guild_only() @app_commands.guild_only()
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"]) @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): async def cmd_adminunban(interaction: discord.Interaction, kasutaja: discord.Member):
await economy.do_admin_unban(kasutaja.id, interaction.user.id) await economy.do_admin_unban(kasutaja.id, interaction.user.id)
await interaction.response.send_message( await interaction.response.send_message(
@@ -179,7 +180,7 @@ def register_economy_admin_commands(
kasutaja=S.OPT["admin_kasutaja"], kasutaja=S.OPT["admin_kasutaja"],
põhjus=S.OPT["admin_põhjus"], 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): async def cmd_adminreset(interaction: discord.Interaction, kasutaja: discord.Member, põhjus: str):
if bot.user and kasutaja.id == bot.user.id: if bot.user and kasutaja.id == bot.user.id:
await interaction.response.send_message(S.ERR["admin_reset_bot"], ephemeral=True) 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"]) @tree.command(name="adminview", description=S.CMD["adminview"])
@app_commands.guild_only() @app_commands.guild_only()
@app_commands.describe(kasutaja=S.OPT["admin_kasutaja"]) @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): async def cmd_adminview(interaction: discord.Interaction, kasutaja: discord.Member):
res = await economy.do_admin_inspect(kasutaja.id) res = await economy.do_admin_inspect(kasutaja.id)
data = res["data"] data = res["data"]
@@ -238,7 +239,7 @@ def register_economy_admin_commands(
kogus=S.OPT["adminexp_kogus"], kogus=S.OPT["adminexp_kogus"],
põhjus=S.OPT["admin_põhjus"], 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): async def cmd_adminexp(interaction: discord.Interaction, kasutaja: discord.Member, kogus: int, põhjus: str):
if kogus == 0: if kogus == 0:
await interaction.response.send_message(S.ERR["admincoins_zero"], ephemeral=True) 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"], ese=S.OPT["adminitem_ese"],
tegevus=S.OPT["adminitem_tegevus"], 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): async def cmd_adminitem(interaction: discord.Interaction, kasutaja: discord.Member, ese: str, tegevus: str):
action = tegevus.strip().lower() action = tegevus.strip().lower()
if action not in ("anna", "eemalda"): if action not in ("anna", "eemalda"):

View File

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

View File

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

View File

@@ -22,12 +22,13 @@ def register_economy_income_commands(
) -> None: ) -> None:
@tree.command(name="daily", description=S.CMD["daily"]) @tree.command(name="daily", description=S.CMD["daily"])
async def cmd_daily(interaction: discord.Interaction): async def cmd_daily(interaction: discord.Interaction):
await interaction.response.defer()
res = await economy.do_daily(interaction.user.id) res = await economy.do_daily(interaction.user.id)
if not res["ok"]: if not res["ok"]:
if res["reason"] == "banned": 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": elif res["reason"] == "cooldown":
await interaction.response.send_message( await interaction.followup.send(
S.CD_MSG["daily"].format(ts=cd_ts(res["remaining"])), S.CD_MSG["daily"].format(ts=cd_ts(res["remaining"])),
ephemeral=True, 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"]))) 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) 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(maybe_remind(interaction.user.id, "daily"))
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["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): async def cmd_work(interaction: discord.Interaction):
if await check_cmd_rate(interaction): if await check_cmd_rate(interaction):
return return
await interaction.response.defer()
res = await economy.do_work(interaction.user.id) res = await economy.do_work(interaction.user.id)
if not res["ok"]: if not res["ok"]:
if res["reason"] == "banned": 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": elif res["reason"] == "cooldown":
await interaction.response.send_message( await interaction.followup.send(
S.CD_MSG["work"].format(ts=cd_ts(res["remaining"])), S.CD_MSG["work"].format(ts=cd_ts(res["remaining"])),
ephemeral=True, ephemeral=True,
) )
elif res["reason"] == "jailed": elif res["reason"] == "jailed":
await interaction.response.send_message( await interaction.followup.send(
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])), S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
ephemeral=True, ephemeral=True,
) )
@@ -84,7 +86,7 @@ def register_economy_income_commands(
desc += S.WORK_UI["laud"] desc += S.WORK_UI["laud"]
desc += S.WORK_UI["balance"].format(balance=coin(res["balance"])) desc += S.WORK_UI["balance"].format(balance=coin(res["balance"]))
embed = discord.Embed(title=S.TITLE["work"], description=desc, color=0x57F287) 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(maybe_remind(interaction.user.id, "work"))
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["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): async def cmd_beg(interaction: discord.Interaction):
if await check_cmd_rate(interaction): if await check_cmd_rate(interaction):
return return
await interaction.response.defer()
res = await economy.do_beg(interaction.user.id) res = await economy.do_beg(interaction.user.id)
if not res["ok"]: if not res["ok"]:
if res["reason"] == "banned": 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": elif res["reason"] == "cooldown":
await interaction.response.send_message( await interaction.followup.send(
S.CD_MSG["beg"].format(ts=cd_ts(res["remaining"])), S.CD_MSG["beg"].format(ts=cd_ts(res["remaining"])),
ephemeral=True, ephemeral=True,
) )
@@ -114,7 +117,7 @@ def register_economy_income_commands(
beg_lines.append(S.BEG_UI["klaviatuur"]) beg_lines.append(S.BEG_UI["klaviatuur"])
beg_lines.append(S.BEG_UI["balance"].format(balance=coin(res["balance"]))) beg_lines.append(S.BEG_UI["balance"].format(balance=coin(res["balance"])))
embed = discord.Embed(title=title, description="\n".join(beg_lines), color=color) 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(maybe_remind(interaction.user.id, "beg"))
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["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): async def cmd_crime(interaction: discord.Interaction):
if await check_cmd_rate(interaction): if await check_cmd_rate(interaction):
return return
await interaction.response.defer()
res = await economy.do_crime(interaction.user.id) res = await economy.do_crime(interaction.user.id)
if not res["ok"]: if not res["ok"]:
if res["reason"] == "banned": 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": elif res["reason"] == "cooldown":
await interaction.response.send_message( await interaction.followup.send(
S.CD_MSG["crime"].format(ts=cd_ts(res["remaining"])), S.CD_MSG["crime"].format(ts=cd_ts(res["remaining"])),
ephemeral=True, ephemeral=True,
) )
elif res["reason"] == "jailed": elif res["reason"] == "jailed":
await interaction.response.send_message( await interaction.followup.send(
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])), S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
ephemeral=True, ephemeral=True,
) )
@@ -161,7 +165,7 @@ def register_economy_income_commands(
+ S.CRIME_UI["balance"].format(balance=coin(res["balance"])), + S.CRIME_UI["balance"].format(balance=coin(res["balance"])),
color=0xED4245, color=0xED4245,
) )
await interaction.response.send_message(embed=embed) await interaction.followup.send(embed=embed)
asyncio.create_task(maybe_remind(interaction.user.id, "crime")) asyncio.create_task(maybe_remind(interaction.user.id, "crime"))
if res["success"]: if res["success"]:
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["crime_win"])) 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) await interaction.response.send_message(S.ERR["rob_house_blocked"], ephemeral=True)
return return
await interaction.response.defer()
res = await economy.do_rob(interaction.user.id, sihtmärk.id) res = await economy.do_rob(interaction.user.id, sihtmärk.id)
if not res["ok"]: if not res["ok"]:
if res["reason"] == "banned": 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": elif res["reason"] == "cooldown":
await interaction.response.send_message( await interaction.followup.send(
S.CD_MSG["rob"].format(ts=cd_ts(res["remaining"])), S.CD_MSG["rob"].format(ts=cd_ts(res["remaining"])),
ephemeral=True, ephemeral=True,
) )
elif res["reason"] == "jailed": elif res["reason"] == "jailed":
await interaction.response.send_message( await interaction.followup.send(
S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])), S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])),
ephemeral=True, ephemeral=True,
) )
elif res["reason"] == "broke": elif res["reason"] == "broke":
await interaction.response.send_message( await interaction.followup.send(
S.ERR["rob_too_poor"].format(name=sihtmärk.display_name), S.ERR["rob_too_poor"].format(name=sihtmärk.display_name),
ephemeral=True, ephemeral=True,
) )
elif res["reason"] == "target_jailed": 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), S.ERR["rob_target_jailed"].format(name=sihtmärk.display_name),
ephemeral=True, ephemeral=True,
) )
@@ -242,7 +247,7 @@ def register_economy_income_commands(
), ),
color=0xED4245, color=0xED4245,
) )
await interaction.response.send_message(embed=embed) await interaction.followup.send(embed=embed)
asyncio.create_task(maybe_remind(interaction.user.id, "rob")) asyncio.create_task(maybe_remind(interaction.user.id, "rob"))
if res["success"]: if res["success"]:
asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["rob_win"])) asyncio.create_task(award_exp(interaction, economy.EXP_REWARDS["rob_win"]))

View File

@@ -13,7 +13,9 @@ from pathlib import Path
import discord import discord
from discord import app_commands from discord import app_commands
from core.admin import bot_admin_check
import strings as S import strings as S
from core.admin import bot_admin_check
def register_ops_admin_commands( def register_ops_admin_commands(
@@ -32,7 +34,7 @@ def register_ops_admin_commands(
) -> None: ) -> None:
@tree.command(name="status", description=S.CMD["status"]) @tree.command(name="status", description=S.CMD["status"])
@app_commands.guild_only() @app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True) @bot_admin_check()
async def cmd_status(interaction: discord.Interaction): async def cmd_status(interaction: discord.Interaction):
mem = process.memory_info() mem = process.memory_info()
cpu = process.cpu_percent(interval=0.1) cpu = process.cpu_percent(interval=0.1)
@@ -95,7 +97,7 @@ def register_ops_admin_commands(
@tree.command(name="sync", description=S.CMD["sync"]) @tree.command(name="sync", description=S.CMD["sync"])
@app_commands.guild_only() @app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True) @bot_admin_check()
async def cmd_sync(interaction: discord.Interaction): async def cmd_sync(interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True) await interaction.response.defer(ephemeral=True)
tree.copy_global_to(guild=guild_obj) tree.copy_global_to(guild=guild_obj)
@@ -107,7 +109,7 @@ def register_ops_admin_commands(
@tree.command(name="restart", description=S.CMD["restart"]) @tree.command(name="restart", description=S.CMD["restart"])
@app_commands.guild_only() @app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True) @bot_admin_check()
async def cmd_restart(interaction: discord.Interaction): async def cmd_restart(interaction: discord.Interaction):
restart_file.write_text(json.dumps({"channel_id": interaction.channel_id}), encoding="utf-8") restart_file.write_text(json.dumps({"channel_id": interaction.channel_id}), encoding="utf-8")
await interaction.response.send_message(S.MSG_RESTARTING, ephemeral=True) 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"]) @tree.command(name="shutdown", description=S.CMD["shutdown"])
@app_commands.guild_only() @app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True) @bot_admin_check()
async def cmd_shutdown(interaction: discord.Interaction): async def cmd_shutdown(interaction: discord.Interaction):
await interaction.response.send_message(S.MSG_SHUTTING_DOWN, ephemeral=True) await interaction.response.send_message(S.MSG_SHUTTING_DOWN, ephemeral=True)
log.info("/shutdown triggered by %s", interaction.user) 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"]) @tree.command(name="pause", description=S.CMD["pause"])
@app_commands.guild_only() @app_commands.guild_only()
@app_commands.default_permissions(manage_guild=True) @bot_admin_check()
async def cmd_pause(interaction: discord.Interaction): async def cmd_pause(interaction: discord.Interaction):
paused = not get_paused() paused = not get_paused()
set_paused(paused) set_paused(paused)

View File

@@ -42,6 +42,30 @@ BIRTHDAY_CHANNEL_ID = (
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]
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_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", "")

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 . import pb_client
from .pb_client import DatabaseError from .pb_client import DatabaseError
from .emoji import EMOJI as E
import strings import strings
@@ -29,13 +30,9 @@ def _txn(event: str, **fields) -> None:
_txn_log.info("%-16s %s", event, body) _txn_log.info("%-16s %s", event, body)
# --------------------------------------------------------------------------- # Per-profile emoji values live in core/emoji.py; add new IDs there.
# Emoji config COIN = E["TipiCOIN"]
# To use your custom Discord emoji replace COIN with the full tag, e.g.: PP_EMOJI = E["TipiFIRE"]
# COIN = "<:tipicoin:1234567890123456789>"
# ---------------------------------------------------------------------------
COIN = "<:TipiCOIN:1483000209188589628>"
PP_EMOJI = "<:TipiFIRE:1483431381668335687>"
PRESTIGE_ROLE = "TipiPRESTIGE" PRESTIGE_ROLE = "TipiPRESTIGE"
PRESTIGE_MIN_LEVEL = 30 # minimum level required to prestige PRESTIGE_MIN_LEVEL = 30 # minimum level required to prestige
@@ -52,99 +49,99 @@ class ShopItem(TypedDict):
SHOP: dict[str, ShopItem] = { SHOP: dict[str, ShopItem] = {
"gaming_hiir": { "gaming_hiir": {
"name": "Mängurihiir", "name": "Mängurihiir",
"emoji": "<:TipiHIIR:1483004306012504128>", "emoji": E["TipiHIIR"],
"cost": 500, "cost": 500,
"description": strings.ITEM_DESCRIPTIONS["gaming_hiir"], "description": strings.ITEM_DESCRIPTIONS["gaming_hiir"],
}, },
"hiirematt": { "hiirematt": {
"name": "Hiirematt", "name": "Hiirematt",
"emoji": "<:TipiMATT:1483387697132208128>", "emoji": E["TipiMATT"],
"cost": 600, "cost": 600,
"description": strings.ITEM_DESCRIPTIONS["hiirematt"], "description": strings.ITEM_DESCRIPTIONS["hiirematt"],
}, },
"korvaklapid": { "korvaklapid": {
"name": "K\u00f5rvaklapid", "name": "K\u00f5rvaklapid",
"emoji": "<:TipiKLAPID:1483387694083084349>", "emoji": E["TipiKLAPID"],
"cost": 1200, "cost": 1200,
"description": strings.ITEM_DESCRIPTIONS["korvaklapid"], "description": strings.ITEM_DESCRIPTIONS["korvaklapid"],
}, },
"lan_pass": { "lan_pass": {
"name": "LAN pilet", "name": "LAN pilet",
"emoji": "<:TipiPILET:1483004308353060904>", "emoji": E["TipiPILET"],
"cost": 1200, "cost": 1200,
"description": strings.ITEM_DESCRIPTIONS["lan_pass"], "description": strings.ITEM_DESCRIPTIONS["lan_pass"],
}, },
"energiajook": { "energiajook": {
"name": "Red Bull", "name": "Red Bull",
"emoji": "<:TipiBULL:1483004310924300409>", "emoji": E["TipiBULL"],
"cost": 800, "cost": 800,
"description": strings.ITEM_DESCRIPTIONS["energiajook"], "description": strings.ITEM_DESCRIPTIONS["energiajook"],
}, },
"gaming_laptop": { "gaming_laptop": {
"name": "Bot Farm", "name": "Bot Farm",
"emoji": "<:TipiLAP:1483004307161874566>", "emoji": E["TipiLAP"],
"cost": 1500, "cost": 1500,
"description": strings.ITEM_DESCRIPTIONS["gaming_laptop"], "description": strings.ITEM_DESCRIPTIONS["gaming_laptop"],
}, },
"anticheat": { "anticheat": {
"name": "Anticheat", "name": "Anticheat",
"emoji": "<:TipiVAC:1483004309510819860>", "emoji": E["TipiVAC"],
"cost": 1000, "cost": 1000,
"description": strings.ITEM_DESCRIPTIONS["anticheat"], "description": strings.ITEM_DESCRIPTIONS["anticheat"],
}, },
# ----- Tier 2 ----- # ----- Tier 2 -----
"reguleeritav_laud": { "reguleeritav_laud": {
"name": "Reguleeritav laud", "name": "Reguleeritav laud",
"emoji": "<:TipiLAUD:1483387695576125440>", "emoji": E["TipiLAUD"],
"cost": 3500, "cost": 3500,
"description": strings.ITEM_DESCRIPTIONS["reguleeritav_laud"], "description": strings.ITEM_DESCRIPTIONS["reguleeritav_laud"],
}, },
"jellyfin": { "jellyfin": {
"name": "Jellyfin server", "name": "Jellyfin server",
"emoji": "<:TipiSERVER:1483387701032910969>", "emoji": E["TipiSERVER"],
"cost": 4000, "cost": 4000,
"description": strings.ITEM_DESCRIPTIONS["jellyfin"], "description": strings.ITEM_DESCRIPTIONS["jellyfin"],
}, },
"mikrofon": { "mikrofon": {
"name": "Eraldiseisev mikrofon", "name": "Eraldiseisev mikrofon",
"emoji": "<:TipiMIC:1483387698499551313>", "emoji": E["TipiMIC"],
"cost": 2800, "cost": 2800,
"description": strings.ITEM_DESCRIPTIONS["mikrofon"], "description": strings.ITEM_DESCRIPTIONS["mikrofon"],
}, },
"klaviatuur": { "klaviatuur": {
"name": "Mehaaniline klaviatuur", "name": "Mehaaniline klaviatuur",
"emoji": "<:TipiKLAVA:1483014339228078140>", "emoji": E["TipiKLAVA"],
"cost": 1800, "cost": 1800,
"description": strings.ITEM_DESCRIPTIONS["klaviatuur"], "description": strings.ITEM_DESCRIPTIONS["klaviatuur"],
}, },
"monitor": { "monitor": {
"name": "Ultralai monitor", "name": "Ultralai monitor",
"emoji": "<:TipiMONITOR:1483014340327243908>", "emoji": E["TipiMONITOR"],
"cost": 2500, "cost": 2500,
"description": strings.ITEM_DESCRIPTIONS["monitor"], "description": strings.ITEM_DESCRIPTIONS["monitor"],
}, },
"cat6": { "cat6": {
"name": "Cat6 kaabel", "name": "Cat6 kaabel",
"emoji": "<:TipiCAT:1483014337663602718>", "emoji": E["TipiCAT"],
"cost": 3500, "cost": 3500,
"description": strings.ITEM_DESCRIPTIONS["cat6"], "description": strings.ITEM_DESCRIPTIONS["cat6"],
}, },
# ----- Tier 3 ----- # ----- Tier 3 -----
"monitor_360": { "monitor_360": {
"name": "360Hz monitor", "name": "360Hz monitor",
"emoji": "<:TipiMONITOR2:1483387699514839162>", "emoji": E["TipiMONITOR2"],
"cost": 7500, "cost": 7500,
"description": strings.ITEM_DESCRIPTIONS["monitor_360"], "description": strings.ITEM_DESCRIPTIONS["monitor_360"],
}, },
"karikas": { "karikas": {
"name": "TipiLAN karikas", "name": "TipiLAN karikas",
"emoji": "<:TipiKARIKAS:1483014841148112977>", "emoji": E["TipiKARIKAS"],
"cost": 6000, "cost": 6000,
"description": strings.ITEM_DESCRIPTIONS["karikas"], "description": strings.ITEM_DESCRIPTIONS["karikas"],
}, },
"gaming_tool": { "gaming_tool": {
"name": "Gaming tool", "name": "Gaming tool",
"emoji": "<:TipiTOOL:1483014341648187613>", "emoji": E["TipiTOOL"],
"cost": 9000, "cost": 9000,
"description": strings.ITEM_DESCRIPTIONS["gaming_tool"], "description": strings.ITEM_DESCRIPTIONS["gaming_tool"],
}, },
@@ -204,7 +201,7 @@ class PrestigeItem(TypedDict):
PRESTIGE_SHOP: dict[str, PrestigeItem] = { PRESTIGE_SHOP: dict[str, PrestigeItem] = {
"coin_mult": { "coin_mult": {
"emoji": "<:TipiCOIN:1483000209188589628>", "emoji": E["TipiCOIN"],
"max_level": 5, "max_level": 5,
"pp_cost": 5, "pp_cost": 5,
"effect": 0.08, "effect": 0.08,
@@ -317,16 +314,18 @@ LEVEL_ROLES: list[tuple[int, str]] = [
def get_level(exp: int) -> int: def get_level(exp: int) -> int:
"""Level = max(1, floor(sqrt(exp/6))). """Level = max(1, floor(sqrt(exp/10))).
Level 5 @ 150 EXP, 10 @ 600, 20 @ 2400, 30 @ 5400.""" Level 5 @ 250 EXP, 10 @ 1000, 20 @ 4000, 30 @ 9000."""
return max(1, int(math.sqrt(max(0, exp) / 6))) return max(1, int(math.sqrt(max(0, exp) / 10)))
def exp_for_level(level: int) -> int: 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: if level <= 1:
return 0 return 0
return level * level * 6 return 10 * level * level
def level_role_name(level: int) -> str: 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
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
_SLOTS_SYMBOLS: list[tuple[str, int]] = [ _SLOTS_SYMBOLS: list[tuple[str, int]] = [
("<:TipiHEART:1483431377561976853>", 27), (E["TipiHEART"], 27),
("<:TipiFIRE:1483431381668335687>", 22), (E["TipiFIRE"], 22),
("<:TipiTROLL:1483431380166774895>", 18), (E["TipiTROLL"], 18),
("<:TipICRY:1483431288852709387>", 15), (E["TipICRY"], 15),
("<:TipiSKULL:1483431378929451028>", 10), (E["TipiSKULL"], 10),
("<:TipiKARIKAS:1483014841148112977>", 8), (E["TipiKARIKAS"], 8),
] ]
_SLOTS_JACKPOT = "<:TipiKARIKAS:1483014841148112977>" _SLOTS_JACKPOT = E["TipiKARIKAS"]
_SLOTS_TRIPLE_MULT: dict[str, int] = { _SLOTS_TRIPLE_MULT: dict[str, int] = {
"<:TipiHEART:1483431377561976853>": 4, E["TipiHEART"]: 4,
"<:TipiFIRE:1483431381668335687>": 5, E["TipiFIRE"]: 5,
"<:TipiTROLL:1483431380166774895>": 7, E["TipiTROLL"]: 7,
"<:TipICRY:1483431288852709387>": 10, E["TipICRY"]: 10,
"<:TipiSKULL:1483431378929451028>": 15, E["TipiSKULL"]: 15,
"<:TipiKARIKAS:1483014841148112977>": 25, # jackpot 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 from __future__ import annotations
import calendar
import logging import logging
from datetime import datetime, date from datetime import datetime, date
from dataclasses import dataclass, field from dataclasses import dataclass, field
from zoneinfo import ZoneInfo
import discord import discord
@@ -13,6 +15,20 @@ from . import sheets
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
_PLACEHOLDER = {"-", "x", "n/a", "none", "ei"} _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: def _is_placeholder(val: str) -> bool:
@@ -34,6 +50,7 @@ class SyncResult:
roles_added: list[str] = field(default_factory=list) roles_added: list[str] = field(default_factory=list)
roles_removed: list[str] = field(default_factory=list) roles_removed: list[str] = field(default_factory=list)
birthday_soon: bool = False birthday_soon: bool = False
birthday_today: bool = False
not_found: bool = False not_found: bool = False
errors: list[str] = field(default_factory=list) errors: list[str] = field(default_factory=list)
synced: bool = False # True when no errors; caller writes this to the sheet 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() raw = str(raw).strip()
if _is_placeholder(raw): if _is_placeholder(raw):
return None return None
today = date.today() today = today_local()
for fmt, has_year in [("%d/%m/%Y", True), ("%Y-%m-%d", True), ("%m-%d", False)]: for fmt, has_year in [("%d/%m/%Y", True), ("%Y-%m-%d", True), ("%m-%d", False)]:
try: try:
parsed = datetime.strptime(raw, fmt).date() 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: if bday is None:
return False return False
window = window_days or config.BIRTHDAY_WINDOW_DAYS window = window_days or config.BIRTHDAY_WINDOW_DAYS
today = date.today() today = today_local()
this_year_bday = bday.replace(year=today.year) this_year_bday = _shift_year_safe(bday, today.year)
if this_year_bday < today: 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 delta = (this_year_bday - today).days
return 0 <= delta <= window return 0 <= delta <= window
def is_birthday_today(birthday_str: str) -> bool: 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) bday = _parse_birthday(birthday_str)
if bday is None: if bday is None:
return False return False
today = date.today() today = today_local()
return bday.month == today.month and bday.day == today.day 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( async def sync_member(
@@ -186,8 +214,9 @@ async def sync_member(
# --- Birthday check --- # --- Birthday check ---
birthday_str = str(row.get("Sünnipäev", "")).strip() birthday_str = str(row.get("Sünnipäev", "")).strip()
if not _is_placeholder(birthday_str) and _is_birthday_soon(birthday_str): if not _is_placeholder(birthday_str):
result.birthday_soon = True 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) --- # --- Mark synced (caller is responsible for writing to sheet) ---
result.synced = not bool(result.errors) 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. without touching any logic code.
""" """
from core.emoji import EMOJI as E
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Flavour text # Flavour text
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -256,22 +258,22 @@ HELP_CATEGORIES: dict[str, dict] = {
"description": "TipiBOTi poe esemed ja nende efektid", "description": "TipiBOTi poe esemed ja nende efektid",
"color": 0xF4C430, "color": 0xF4C430,
"fields": [ "fields": [
("<:TipiHIIR:1483004306012504128> Mängurihiir - 500 ⬡", "Teeni töötades 50% rohkem TipiCOINe."), (f"{E['TipiHIIR']} Mängurihiir - 500 ⬡", "Teeni töötades 50% rohkem TipiCOINe."),
("<:TipiMATT:1483387697132208128> XL hiirematt - 600 ⬡", "Kerjamise ooteaeg 5min → 3min."), (f"{E['TipiMATT']} XL hiirematt - 600 ⬡", "Kerjamise ooteaeg 5min → 3min."),
("<:TipiKLAPID:1483387694083084349> Kõrvaklapid - 1200 ⬡", "Päevase boonuse ooteaeg 20h → 18h."), (f"{E['TipiKLAPID']} Kõrvaklapid - 1200 ⬡", "Päevase boonuse ooteaeg 20h → 18h."),
("<:TipiPILET:1483004308353060904> LAN pilet (2025) - 1200 ⬡", "Päevane boonus on duubeldatud."), (f"{E['TipiPILET']} 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."), (f"{E['TipiVAC']} 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."), (f"{E['TipiBULL']} 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."), (f"{E['TipiLAP']} 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)."), (f"{E['TipiLAUD']} 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%."), (f"{E['TipiSERVER']} Jellyfin server - 4000 ⬡ *(T2)*", "Röövimise edu tõenäosus 45% → 60%."),
("<:TipiMIC:1483387698499551313> Mikrofon - 2800 ⬡ *(T2)*", "Teeni 30% rohkem eduka /crime puhul."), (f"{E['TipiMIC']} Mikrofon - 2800 ⬡ *(T2)*", "Teeni 30% rohkem eduka /crime puhul."),
("<:TipiKLAVA:1483014339228078140> Mehhaaniline klaviatuur - 1800 ⬡ *(T2)*", "/beg teenib 2x rohkem."), (f"{E['TipiKLAVA']} Mehhaaniline klaviatuur - 1800 ⬡ *(T2)*", "/beg teenib 2x rohkem."),
("<:TipiMONITOR:1483014340327243908> Ultralai monitor - 2500 ⬡ *(T2)*", "/work ooteaeg: 1h → 40min."), (f"{E['TipiMONITOR']} Ultralai monitor - 2500 ⬡ *(T2)*", "/work ooteaeg: 1h → 40min."),
("<:TipiCAT:1483014337663602718> CAT6 netikaabel - 3500 ⬡ *(T2)*", "/crime edu tõenäosus tõuseb 60% → 75%."), (f"{E['TipiCAT']} 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."), (f"{E['TipiMONITOR2']} 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."), (f"{E['TipiKARIKAS']} 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['TipiTOOL']} Mänguritool - 9000 ⬡ *(T3)*", "/crime ebaõnnestumine ei saada sind vanglasse."),
], ],
}, },
"games": { "games": {
@@ -347,10 +349,10 @@ REMINDER_OPTS: list[tuple[str, str, str]] = [
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
SLOTS_TIERS: dict[str, tuple[str, int]] = { SLOTS_TIERS: dict[str, tuple[str, int]] = {
"jackpot": ("<:TipiFIRE:1483431381668335687> JACKPOT!!!", 0xF4C430), "jackpot": (f"{E['TipiFIRE']} JACKPOT!!!", 0xF4C430),
"triple": ("🎰 Kolmik!", 0x57F287), "triple": ("🎰 Kolmik!", 0x57F287),
"pair": ("🎰 Paar", 0x99AAB5), "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", "daily": "📅 Päevane boonus",
"work": "💼 Töö", "work": "💼 Töö",
"beg": "🙏 Kerjamine", "beg": "🙏 Kerjamine",
"crime_win": "<:TipiFIRE:1483431381668335687> Kuritegu õnnestus!", "crime_win": f"{E['TipiFIRE']} Kuritegu õnnestus!",
"crime_fail": "<:TipiTROLL:1483431380166774895> Vahele jäid!", "crime_fail": f"{E['TipiTROLL']} Vahele jäid!",
"rob_win": "<:TipiFIRE:1483431381668335687> Rööv õnnestus!", "rob_win": f"{E['TipiFIRE']} Rööv õnnestus!",
"rob_fail": "<:TipiTROLL:1483431380166774895> Rööv ebaõnnestus!", "rob_fail": f"{E['TipiTROLL']} Rööv ebaõnnestus!",
"rob_anticheat": "<:TipiVAC:1483004309510819860> Anticheat peatas sind!", "rob_anticheat": f"{E['TipiVAC']} Anticheat peatas sind!",
"jailbreak": "🎲 Vanglast põgenemine", "jailbreak": "🎲 Vanglast põgenemine",
"jailbreak_free": "🎲 <:TipiFIRE:1483431381668335687> DUUBEL! Oled vaba!", "jailbreak_free": f"🎲 {E['TipiFIRE']} DUUBEL! Oled vaba!",
"jailbreak_fail": "<:TipICRY:1483431288852709387> Kolm katset läbi!", "jailbreak_fail": f"{E['TipICRY']} Kolm katset läbi!",
"jailbreak_miss": "🎲 <:TipICRY:1483431288852709387> Ei saanud duublit ({tries}/{max})", "jailbreak_miss": "🎲 " + E["TipICRY"] + " Ei saanud duublit ({tries}/{max})",
"jailbreak_bail": "💸 Kautsjon", "jailbreak_bail": "💸 Kautsjon",
"give": "<:TipiHEART:1483431377561976853> TipiCOINi ülekanne", "give": f"{E['TipiHEART']} TipiCOINi ülekanne",
"stats": "📊 Mängustatistika", "stats": "📊 Mängustatistika",
"leaderboard_coins": "🪙 TipiBOTi edetabel - Mündid", "leaderboard_coins": "🪙 TipiBOTi edetabel - Mündid",
"leaderboard_exp": "📊 TipiBOTi edetabel - EXP / Tase", "leaderboard_exp": "📊 TipiBOTi edetabel - EXP / Tase",
"leaderboard_season": "🏆 TipiBOTi edetabel - Hooaja EXP", "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_wagered": "🎲 TipiBOTi edetabel - Hasartmängud",
"leaderboard_fish": "🎣 TipiBOTi edetabel - Kalapüük", "leaderboard_fish": "🎣 TipiBOTi edetabel - Kalapüük",
"rps": "⚔️ Kivi, Paber, Käärid", "rps": "⚔️ Kivi, Paber, Käärid",
@@ -560,26 +562,26 @@ TITLE: dict[str, str] = {
"rps_duel_expire": "⚔️ KPK duell - aegus", "rps_duel_expire": "⚔️ KPK duell - aegus",
"rps_duel_decline": "⚔️ KPK duell - keelduti", "rps_duel_decline": "⚔️ KPK duell - keelduti",
"heist_lobby": "🔫 Grupirööv - kogunemine", "heist_lobby": "🔫 Grupirööv - kogunemine",
"heist_win": "<:TipiFIRE:1483431381668335687> Grupirööv õnnestus!", "heist_win": f"{E['TipiFIRE']} Grupirööv õnnestus!",
"heist_fail": "<:TipiSKULL:1483431378929451028> Grupirööv ebaõnnestus!", "heist_fail": f"{E['TipiSKULL']} Grupirööv ebaõnnestus!",
"heist_cancel": "🔫 Grupirööv tühistatud", "heist_cancel": "🔫 Grupirööv tühistatud",
"request": "<:TipiHEART:1483431377561976853> Rahataotlus", "request": f"{E['TipiHEART']} Rahataotlus",
"reminders": "⏰ Meeldetuletused", "reminders": "⏰ Meeldetuletused",
"cooldowns": "⏱️ Sinu ooteajad", "cooldowns": "⏱️ Sinu ooteajad",
"adminseason": "🏆 Hooaeg lõppes!", "adminseason": "🏆 Hooaeg lõppes!",
"economysetup": "⚙️ Majanduse seadistamine", "economysetup": "⚙️ Majanduse seadistamine",
"blackjack": "🃏 Blackjack", "blackjack": "🃏 Blackjack",
"blackjack_bj": "🃏 <:TipiFIRE:1483431381668335687> BLACKJACK!", "blackjack_bj": f"🃏 {E['TipiFIRE']} BLACKJACK!",
"blackjack_win": "<:TipiFIRE:1483431381668335687> Võitsid!", "blackjack_win": f"{E['TipiFIRE']} Võitsid!",
"blackjack_lose": "<:TipiSKULL:1483431378929451028> Kaotasid!", "blackjack_lose": f"{E['TipiSKULL']} Kaotasid!",
"blackjack_bust": "<:TipiSKULL:1483431378929451028> Üle 21 - kaotasid!", "blackjack_bust": f"{E['TipiSKULL']} Üle 21 - kaotasid!",
"blackjack_push": "🤝 Viik!", "blackjack_push": "🤝 Viik!",
"blackjack_dbust": "<:TipiSKULL:1483431378929451028> Üle 21 - mõlemad kaotasid!", "blackjack_dbust": f"{E['TipiSKULL']} Üle 21 - mõlemad kaotasid!",
"blackjack_dwin": "<:TipiFIRE:1483431381668335687> Topeltpanus võitis!", "blackjack_dwin": f"{E['TipiFIRE']} Topeltpanus võitis!",
"prestige_confirm": "🔥 Prestiiž - kinnita", "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_too_low": "❌ Prestiiž pole saadaval",
"prestige_shop": "<:TipiFIRE:1483431381668335687> Prestiižipood", "prestige_shop": f"{E['TipiFIRE']} Prestiižipood",
"prestige_buy_ok": "✅ Uuendus ostetud!", "prestige_buy_ok": "✅ Uuendus ostetud!",
"fish_cast": "🎣 Otsid kala...", "fish_cast": "🎣 Otsid kala...",
"fish_bite": "🐟 KALA NÄKKAB!", "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_active": "❌ Serveris on juba aktiivne grupirööv käimas! Oota, kuni see lõpeb.",
"heist_full": "❌ Grupirööv on täis!", "heist_full": "❌ Grupirööv on täis!",
"heist_min_players": "❌ Grupiröövi alustamiseks on vaja vähemalt **{min}** osalejat.", "heist_min_players": "❌ Grupiröövi alustamiseks on vaja vähemalt **{min}** osalejat.",
"broke": "<:TipICRY:1483431288852709387> Sul pole piisavalt TipiCOINe. Saldo: {bal}", "broke": E["TipICRY"] + " Sul pole piisavalt TipiCOINe. Saldo: {bal}",
"broke_need": "<:TipICRY:1483431288852709387> Sul pole piisavalt TipiCOINe. Vajad veel {need}.", "broke_need": E["TipICRY"] + " Sul pole piisavalt TipiCOINe. Vajad veel {need}.",
"item_owned": "❌ Sul on see ese juba olemas.", "item_owned": "❌ Sul on see ese juba olemas.",
"item_not_found": "❌ Eset ei leitud.", "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.", "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}.", "rob": "⏳ Saad uuesti röövida {ts}.",
"heist": "⏳ Saad uuesti heisti teha {ts}.", "heist": "⏳ Saad uuesti heisti teha {ts}.",
"heist_global": "⏳ Pangahoidla alles kosub eelmisest röövist. Järgmine heist võimalik {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}.", "fish": "🎣 Saad uuesti kalastada {ts}.",
} }
@@ -837,8 +839,8 @@ CHANNEL_UI: dict[str, str] = {
DAILY_UI: dict[str, str] = { DAILY_UI: dict[str, str] = {
"earned": "✅ Said {earned}!", "earned": "✅ Said {earned}!",
"interest": "<:TipiLAP:1483004307161874566> Bot Farm tootis: +{interest}", "interest": E["TipiLAP"] + " Bot Farm tootis: +{interest}",
"vip": "<:TipiPILET:1483004308353060904> LAN pileti boonus rakendus!", "vip": f"{E['TipiPILET']} LAN pileti boonus rakendus!",
"footer": "Streak: {streak_str} · Saldo: {balance}", "footer": "Streak: {streak_str} · Saldo: {balance}",
} }
@@ -1050,9 +1052,9 @@ RANK_UI: dict[str, str] = {
WORK_UI: dict[str, str] = { WORK_UI: dict[str, str] = {
"desc": "Sa {job} ja teenisid {earned}!", "desc": "Sa {job} ja teenisid {earned}!",
"redbull": "\n<:TipiBULL:1483004310924300409> Red Bull aktiveerus - 3x boonus!", "redbull": f"\n{E['TipiBULL']} Red Bull aktiveerus - 3x boonus!",
"hiir": "\n<:TipiHIIR:1483004306012504128> Mängurihiir: +50% palk", "hiir": f"\n{E['TipiHIIR']} Mängurihiir: +50% palk",
"laud": "\n<:TipiLAUD:1483387695576125440> Reguleeritav laud: +25% palk", "laud": f"\n{E['TipiLAUD']} Reguleeritav laud: +25% palk",
"balance": "\nSaldo: {balance}", "balance": "\nSaldo: {balance}",
} }
@@ -1062,7 +1064,7 @@ WORK_UI: dict[str, str] = {
BEG_UI: dict[str, str] = { BEG_UI: dict[str, str] = {
"desc": "Sa {text} ja said {earned}.", "desc": "Sa {text} ja said {earned}.",
"klaviatuur": "<:TipiKLAVA:1483014339228078140> Mehhaaniline klaviatuur: 2x tulu", "klaviatuur": f"{E['TipiKLAVA']} Mehhaaniline klaviatuur: 2x tulu",
"balance": "Saldo: {balance}", "balance": "Saldo: {balance}",
} }
@@ -1075,8 +1077,8 @@ CRIME_UI: dict[str, str] = {
"fail_base": "Sa {text} ja said trahvi {fine}.", "fail_base": "Sa {text} ja said trahvi {fine}.",
"fail_jailed": "\n\ud83d\udd12 Oled vangis! P\u00e4\u00e4sed {ts}.", "fail_jailed": "\n\ud83d\udd12 Oled vangis! P\u00e4\u00e4sed {ts}.",
"fail_shield": "\n\ud83d\udee1\ufe0f Gaming Tool hoidis sind vanglast!", "fail_shield": "\n\ud83d\udee1\ufe0f Gaming Tool hoidis sind vanglast!",
"mikrofon": "\n<:TipiMIC:1483387698499551313> Mikrofon: +30% saak", "mikrofon": f"\n{E['TipiMIC']} Mikrofon: +30% saak",
"cat6": "\n<:TipiCAT:1483014337663602718> CAT6: 75% edu t\u00f5en\u00e4osus", "cat6": f"\n{E['TipiCAT']} CAT6: 75% edu t\u00f5en\u00e4osus",
"balance": "\nSaldo: {balance}", "balance": "\nSaldo: {balance}",
} }
@@ -1116,7 +1118,7 @@ BUY_UI: dict[str, str] = {
JAILBREAK_UI: dict[str, str] = { JAILBREAK_UI: dict[str, str] = {
"btn_roll": "🎲 Viska täringud ({try_}/{max})", "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!", "free_desc": "{d1} {d2}\n\n✅ Viskasid duubli - pääsesid vanglast!",
"miss_desc": "{d1} {d2}\n\n{left} katset jäänud. Proovi uuesti!", "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.", "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] = { SLOTS_UI: dict[str, str] = {
"playing": "🎰 Mängimas...", "playing": "🎰 Mängimas...",
"jackpot_footer": "<:TipiKARIKAS:1483014841148112977> Kolm karikat! +{change}", "jackpot_footer": E["TipiKARIKAS"] + " Kolm karikat! +{change}",
"triple_footer": "✅ Kolm ühesugust! +{change}", "triple_footer": "✅ Kolm ühesugust! +{change}",
"pair_footer": "Kaks ühesugust! +{change}", "pair_footer": "Kaks ühesugust! +{change}",
"miss_footer": "-{amount}", "miss_footer": "-{amount}",
@@ -1264,7 +1266,7 @@ PRESTIGE_SHOP_DESCRIPTIONS: dict[str, str] = {
PRESTIGE_UI: dict[str, str] = { PRESTIGE_UI: dict[str, str] = {
"confirm_desc": ( "confirm_desc": (
"Oled tasemel **{level}** ({exp} EXP).\n\n" "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" "• Saldo, EXP, esemed, ooteajad\n\n"
"**Kalakogu jääb alles!**\n\nKas oled kindel?" "**Kalakogu jääb alles!**\n\nKas oled kindel?"
), ),
@@ -1273,21 +1275,21 @@ PRESTIGE_UI: dict[str, str] = {
"btn_tab_status": "⭐ Prestiiz", "btn_tab_status": "⭐ Prestiiz",
"btn_tab_shop": "🛍️ Uuendused", "btn_tab_shop": "🛍️ Uuendused",
"success_desc": ( "success_desc": (
"Said **{pp}** <:TipiFIRE:1483431381668335687>\n" "Said **{pp}** " + E["TipiFIRE"] + "\n"
"Prestiiži tase: **{level}**\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!*" "*Kõik lähtestati. Alusta otsast!*"
), ),
"too_low_desc": "Prestiiži jaoks vajad taset **{required}** (sul on tase {level}).", "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_maxed": "✅ Max",
"shop_level_fmt": "Tase {cur}/{max}", "shop_level_fmt": "Tase {cur}/{max}",
"shop_cost_fmt": "{cost} <:TipiFIRE:1483431381668335687>", "shop_cost_fmt": "{cost} " + E["TipiFIRE"],
"buy_success_desc":"**{name}** uuendatud tasemele **{new_level}/{max_level}**!\nPP alles: **{pp}** <:TipiFIRE:1483431381668335687>", "buy_success_desc":"**{name}** uuendatud tasemele **{new_level}/{max_level}**!\nPP alles: **{pp}** " + E["TipiFIRE"],
"buy_no_pp": "<:TipICRY:1483431288852709387> Sul pole piisavalt PP. Sul on **{have}**, vajad **{need}** <:TipiFIRE:1483431381668335687>.", "buy_no_pp": E["TipICRY"] + " Sul pole piisavalt PP. Sul on **{have}**, vajad **{need}** " + E["TipiFIRE"] + ".",
"buy_maxed": "❌ See uuendus on juba maksimumtasemel.", "buy_maxed": "❌ See uuendus on juba maksimumtasemel.",
"buy_not_found": "❌ Sellist uuendust ei leitud. Vaata `/prestigeshop`.", "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}**", "rank_season": "🏆 Hooaja EXP: **{exp}**",
"btn_buy_upgrade": "{emoji} {name} +1 ({cost} PP)", "btn_buy_upgrade": "{emoji} {name} +1 ({cost} PP)",
"status_footer": "⭐ Prestiiž {level} · {pp} PP", "status_footer": "⭐ Prestiiž {level} · {pp} PP",