forked from sass/tipibot
Compare commits
14 Commits
b0a3dcb03b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cdd8dac63 | ||
|
|
6101a278e7 | ||
|
|
1e9ec56761 | ||
|
|
a96488fe9e | ||
|
|
25cf60d2e1 | ||
|
|
3939c879c9 | ||
|
|
42f7bae681 | ||
|
|
b0e23c1a17 | ||
|
|
24de79c503 | ||
|
|
ee4e639c30 | ||
|
|
48cc8398ec | ||
|
|
b83b347d6a | ||
|
|
8d7ac504ca | ||
|
|
15e3121d55 |
6
bot.py
6
bot.py
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"]))
|
||||
|
||||
@@ -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)
|
||||
|
||||
24
config.py
24
config.py
@@ -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
27
core/admin.py
Normal 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)
|
||||
@@ -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
97
core/emoji.py
Normal 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)}")
|
||||
@@ -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)
|
||||
|
||||
122
strings.py
122
strings.py
@@ -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 **(20–30% 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",
|
||||
|
||||
Reference in New Issue
Block a user