from __future__ import annotations import asyncio import datetime from collections.abc import Awaitable, Callable import discord from discord import app_commands import economy import strings as S def register_economy_profile_commands( tree: app_commands.CommandTree, coin: Callable[[int], str], cd_ts: Callable[[datetime.timedelta], str], ensure_level_role: Callable[[discord.Member, int], Awaitable[None]], ) -> None: def _profile_main_embed(target: discord.User | discord.Member, data: dict) -> discord.Embed: exp = data.get("exp", 0) level = economy.get_level(exp) role_name = economy.level_role_name(level) next_level = level + 1 exp_this = economy.exp_for_level(level) exp_next = economy.exp_for_level(next_level) progress = exp - exp_this needed = exp_next - exp_this pct = progress / needed if needed > 0 else 1.0 filled = int(pct * 12) bar = "█" * filled + "░" * (12 - filled) embed = discord.Embed( title=S.PROFILE_UI["main_title"].format(name=target.display_name), color=0xF4C430, ) embed.add_field(name=S.PROFILE_UI["f_balance"], value=coin(data.get("balance", 0)), inline=True) embed.add_field( name=S.PROFILE_UI["f_level"], value=S.PROFILE_UI["level_val"].format(level=level, role=role_name), inline=True, ) streak = data.get("daily_streak", 0) if streak: embed.add_field( name=S.PROFILE_UI["f_streak"], value=S.BALANCE_UI["streak_val"].format(streak=streak), inline=True, ) p_level = data.get("prestige_level", 0) if p_level > 0: p_pp = data.get("prestige_points", 0) embed.add_field( name=S.PROFILE_UI["f_prestige"], value=S.PROFILE_UI["prestige_val"].format(level=p_level, pp=p_pp), inline=True, ) jail_remaining = economy._is_jailed(data) if jail_remaining: embed.add_field(name=S.PROFILE_UI["f_jail"], value=cd_ts(jail_remaining), inline=True) embed.add_field( name=S.PROFILE_UI["f_progress"].format(next=next_level), value=S.PROFILE_UI["progress_bar"].format(bar=bar, done=progress, needed=needed), inline=False, ) if level < 10: embed.set_footer(text=S.PROFILE_UI["footer_t1"]) elif level < 20: embed.set_footer(text=S.PROFILE_UI["footer_t2"]) else: embed.set_footer(text=S.PROFILE_UI["footer_t3"]) return embed def _profile_items_embed(target: discord.User | discord.Member, data: dict) -> discord.Embed: embed = discord.Embed( title=S.PROFILE_UI["items_title"].format(name=target.display_name), color=0xF4C430, ) uses_map = data.get("item_uses", {}) item_lines = [] for item_id in data.get("items", []): if item_id not in economy.SHOP: continue line = f"{economy.SHOP[item_id]['emoji']} **{economy.SHOP[item_id]['name']}**" if item_id in uses_map: uses = uses_map[item_id] line += ( S.BALANCE_UI["uses_one"].format(uses=uses) if uses == 1 else S.BALANCE_UI["uses_many"].format(uses=uses) ) item_lines.append(line) embed.description = "\n".join(item_lines) if item_lines else S.PROFILE_UI["items_empty"] return embed def _profile_stats_embed(target: discord.User | discord.Member, data: dict) -> discord.Embed: def _s(key: str) -> int: return data.get(key, 0) embed = discord.Embed( title=S.PROFILE_UI["stats_title"].format(name=target.display_name), color=0x5865F2, ) embed.add_field( name=S.STATS_UI["economy_field"], value=S.STATS_UI["economy_val"].format( peak=coin(_s("peak_balance")), earned=coin(_s("lifetime_earned")), lost=coin(_s("lifetime_lost")), ), inline=True, ) embed.add_field( name=S.STATS_UI["work_field"], value=S.STATS_UI["work_val"].format(work=_s("work_count"), beg=_s("beg_count")), inline=True, ) embed.add_field(name="\u200b", value="\u200b", inline=False) embed.add_field( name=S.STATS_UI["gamble_field"], value=S.STATS_UI["gamble_val"].format( wagered=coin(_s("total_wagered")), win=coin(_s("biggest_win")), loss=coin(_s("biggest_loss")), jackpots=_s("slots_jackpots"), ), inline=True, ) embed.add_field( name=S.STATS_UI["crime_field"], value=S.STATS_UI["crime_val"].format( crimes=_s("crimes_attempted"), succeeded=_s("crimes_succeeded"), heists=_s("heists_joined"), heists_won=_s("heists_won"), jailed=_s("times_jailed"), bail=coin(_s("total_bail_paid")), ), inline=True, ) embed.add_field(name="\u200b", value="\u200b", inline=False) embed.add_field( name=S.STATS_UI["social_field"], value=S.STATS_UI["social_val"].format( given=coin(_s("total_given")), received=coin(_s("total_received")), ), inline=True, ) embed.add_field( name=S.STATS_UI["records_field"], value=S.STATS_UI["records_val"].format(streak=_s("best_daily_streak")), inline=True, ) return embed def _profile_fish_embed(target: discord.User | discord.Member, fish_res: dict) -> discord.Embed: embed = discord.Embed( title=S.PROFILE_UI["fish_title"].format(name=target.display_name), color=0x5865F2, ) book: dict = fish_res["book"] if not book: embed.description = S.FISH_UI["book_empty"] return embed inv_counts: dict = fish_res.get("inv_counts", {}) caught_count = fish_res["unique_caught"] total = fish_res["total_species"] lines = [S.FISH_UI["book_caught"].format(caught=caught_count, total=total)] for fish_id, fish_data in economy.FISH_CATALOGUE.items(): rarity = fish_data["rarity"] emoji = S.FISH_RARITY_EMOJI[rarity] rarity_name = S.FISH_RARITY_NAMES[rarity] count = book.get(fish_id, 0) if count > 0: n_inv = inv_counts.get(fish_id, 0) inv_str = S.FISH_UI["book_inv"].format(n=n_inv) if n_inv > 0 else "" lines.append( S.FISH_UI["book_yes"].format( emoji=emoji, name=S.FISH_NAMES[fish_id], rarity=rarity_name, count=count, inv=inv_str, ) ) else: lines.append(S.FISH_UI["book_no"].format(rarity=rarity_name)) embed.description = "\n".join(lines) embed.set_footer( text=S.FISH_UI["book_footer"].format( page=1, total_pages=1, caught=caught_count, total=total, ) ) return embed class ProfileView(discord.ui.View): def __init__(self, target: discord.User | discord.Member, invoker_id: int, tab: str = "main"): super().__init__(timeout=120) self.target = target self.invoker_id = invoker_id self.tab = tab self._rebuild() def _rebuild(self): self.clear_items() tabs = [ ("main", S.PROFILE_UI["btn_profile"]), ("items", S.PROFILE_UI["btn_items"]), ("stats", S.PROFILE_UI["btn_stats"]), ("fish", S.PROFILE_UI["btn_fish"]), ] for tab_id, label in tabs: btn = discord.ui.Button( label=label, style=( discord.ButtonStyle.primary if tab_id == self.tab else discord.ButtonStyle.secondary ), disabled=(tab_id == self.tab), ) btn.callback = self._make_cb(tab_id) self.add_item(btn) def _make_cb(self, tab_id: str): async def _cb(interaction: discord.Interaction): if interaction.user.id != self.invoker_id: await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True) return self.tab = tab_id self._rebuild() await interaction.response.defer() data = await economy.get_user(self.target.id) if tab_id == "fish": fish_res = await economy.do_fishbook(self.target.id) embed = _profile_fish_embed(self.target, fish_res) inv: list = data.get("fish_inventory") or [] if inv and self.target.id == self.invoker_id: total_value = sum(entry.get("value", 0) for entry in inv) sell_btn = discord.ui.Button( label=( f"{S.FISH_UI['btn_sell']} " f"({len(inv)} kala · {total_value:,} {economy.COIN})" ), style=discord.ButtonStyle.success, row=1, ) sell_btn.callback = self._sell_fish_cb() self.add_item(sell_btn) elif tab_id == "items": embed = _profile_items_embed(self.target, data) elif tab_id == "stats": embed = _profile_stats_embed(self.target, data) else: embed = _profile_main_embed(self.target, data) await interaction.edit_original_response(embed=embed, view=self) return _cb def _sell_fish_cb(self): async def _cb(interaction: discord.Interaction): if interaction.user.id != self.invoker_id: await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True) return await interaction.response.defer() res = await economy.do_fish_sell(self.invoker_id) self.tab = "fish" self._rebuild() fish_res = await economy.do_fishbook(self.target.id) embed = _profile_fish_embed(self.target, fish_res) sold_line = S.FISH_UI["inv_sold_all"].format( count=res.get("count", 0), coins=coin(res.get("coins", 0)), balance=coin(res.get("balance", 0)), ) embed.description = f"{sold_line}\n\n{embed.description or ''}" await interaction.edit_original_response(embed=embed, view=self) return _cb def _balance_embed(user: discord.User | discord.Member, data: dict) -> discord.Embed: embed = discord.Embed( title=f"{economy.COIN} {user.display_name}", color=0xF4C430, ) embed.add_field(name=S.BALANCE_UI["saldo"], value=coin(data["balance"]), inline=True) streak = data.get("daily_streak", 0) if streak: embed.add_field( name=S.BALANCE_UI["streak"], value=S.BALANCE_UI["streak_val"].format(streak=streak), inline=True, ) jail_remaining = economy._is_jailed(data) if jail_remaining: embed.add_field(name=S.BALANCE_UI["jailed_until"], value=cd_ts(jail_remaining), inline=True) item_lines = [] uses_map = data.get("item_uses", {}) for item_id in data.get("items", []): if item_id not in economy.SHOP: continue line = f"{economy.SHOP[item_id]['emoji']} {economy.SHOP[item_id]['name']}" if item_id in uses_map: uses = uses_map[item_id] line += ( S.BALANCE_UI["uses_one"].format(uses=uses) if uses == 1 else S.BALANCE_UI["uses_many"].format(uses=uses) ) item_lines.append(line) if item_lines: embed.add_field(name=S.BALANCE_UI["items"], value="\n".join(item_lines), inline=False) return embed @tree.command(name="profile", description=S.CMD["profile"]) @app_commands.describe(kasutaja=S.OPT["profile_kasutaja"]) async def cmd_profile(interaction: discord.Interaction, kasutaja: discord.Member | None = None): target = kasutaja or interaction.user data = await economy.get_user(target.id) embed = _profile_main_embed(target, data) invoker_id = interaction.user.id await interaction.response.send_message(embed=embed, view=ProfileView(target, invoker_id)) if not kasutaja and interaction.guild: member = interaction.guild.get_member(target.id) if member: asyncio.create_task(ensure_level_role(member, economy.get_level(data.get("exp", 0)))) @tree.command(name="balance", description=S.CMD["balance"]) async def cmd_balance(interaction: discord.Interaction, kasutaja: discord.Member | None = None): target = kasutaja or interaction.user data = await economy.get_user(target.id) await interaction.response.send_message(embed=_balance_embed(target, data)) @tree.command(name="cooldowns", description=S.CMD["cooldowns"]) async def cmd_cooldowns(interaction: discord.Interaction): data = await economy.get_user(interaction.user.id) now = datetime.datetime.now(datetime.timezone.utc) items = set(data.get("items", [])) def _status(last_key: str, cooldown: datetime.timedelta) -> str: raw = data.get(last_key) if not raw: return S.COOLDOWNS_UI["ready"] last = economy._parse_dt(raw) if last is None: return S.COOLDOWNS_UI["ready"] expires = last + cooldown if expires <= now: return S.COOLDOWNS_UI["ready"] ts = int(expires.timestamp()) return f"⏳ " work_cd = datetime.timedelta(minutes=40) if "monitor" in items else economy.COOLDOWNS["work"] beg_cd = datetime.timedelta(minutes=3) if "hiirematt" in items else economy.COOLDOWNS["beg"] daily_cd = datetime.timedelta(hours=18) if "korvaklapid" in items else economy.COOLDOWNS["daily"] fish_cd = datetime.timedelta(seconds=90) if "ussipurk" in items else economy.COOLDOWNS["fish"] lines = [ S.COOLDOWNS_UI["daily_line"].format( status=_status("last_daily", daily_cd), note=S.COOLDOWNS_UI["note_korvak"] if "korvaklapid" in items else "", ), S.COOLDOWNS_UI["work_line"].format( status=_status("last_work", work_cd), note=S.COOLDOWNS_UI["note_monitor"] if "monitor" in items else "", ), S.COOLDOWNS_UI["beg_line"].format( status=_status("last_beg", beg_cd), note=S.COOLDOWNS_UI["note_hiirematt"] if "hiirematt" in items else "", ), S.COOLDOWNS_UI["crime_line"].format(status=_status("last_crime", economy.COOLDOWNS["crime"])), S.COOLDOWNS_UI["rob_line"].format(status=_status("last_rob", economy.COOLDOWNS["rob"])), S.COOLDOWNS_UI["fish_line"].format( status=_status("last_fish", fish_cd), note=S.COOLDOWNS_UI["note_ussipurk"] if "ussipurk" in items else "", ), ] jailed = data.get("jailed_until") if jailed: jail_dt = datetime.datetime.fromisoformat(jailed) if jail_dt > now: ts = int(jail_dt.timestamp()) lines.append(S.COOLDOWNS_UI["jailed"].format(ts=ts)) else: lines.append(S.COOLDOWNS_UI["jail_expired"]) embed = discord.Embed( title=S.TITLE["cooldowns"], description="\n".join(lines), color=0x5865F2, ) await interaction.response.send_message(embed=embed, ephemeral=True) @tree.command(name="jailed", description=S.CMD["jailed"]) @app_commands.guild_only() async def cmd_jailed(interaction: discord.Interaction): await interaction.response.defer() jailed = await economy.do_get_jailed() if not jailed: embed = discord.Embed( title=S.JAILED_UI["title"], description=S.JAILED_UI["empty"], color=0x57F287, ) await interaction.followup.send(embed=embed) return now = datetime.datetime.now(datetime.timezone.utc) lines = [] for uid, remaining in jailed: ts = int((now + remaining).timestamp()) member = interaction.guild.get_member(uid) if interaction.guild else None mention = member.mention if member else f"<@{uid}>" lines.append(S.JAILED_UI["entry"].format(mention=mention, ts=ts)) plural = "" if len(jailed) == 1 else "i" embed = discord.Embed( title=S.JAILED_UI["title"], description="\n".join(lines), color=0xED4245, ) embed.set_footer(text=S.JAILED_UI["footer"].format(count=len(jailed), plural=plural)) await interaction.followup.send(embed=embed) @tree.command(name="rank", description=S.CMD["rank"]) @app_commands.describe(kasutaja=S.OPT["rank_kasutaja"]) async def cmd_rank(interaction: discord.Interaction, kasutaja: discord.Member | None = None): target = kasutaja or interaction.user data = await economy.get_user(target.id) exp = data.get("exp", 0) level = economy.get_level(exp) role_name = economy.level_role_name(level) next_level = level + 1 exp_this = economy.exp_for_level(level) exp_next = economy.exp_for_level(next_level) progress = exp - exp_this needed = exp_next - exp_this pct = progress / needed if needed > 0 else 1.0 filled = int(pct * 12) bar = "█" * filled + "░" * (12 - filled) embed = discord.Embed( title=S.RANK_UI["title"].format(name=target.display_name, level=level), color=0x5865F2, ) embed.add_field(name=S.RANK_UI["field_title"], value=f"**{role_name}**", inline=True) embed.add_field(name=S.RANK_UI["field_exp"], value=str(exp), inline=True) embed.add_field( name=S.RANK_UI["field_progress"].format(next=next_level), value=S.RANK_UI["progress_val"].format(bar=bar, progress=progress, needed=needed), inline=False, ) p_level = data.get("prestige_level", 0) p_pp = data.get("prestige_points", 0) s_exp = data.get("season_total_exp", 0) if p_level > 0 or s_exp > 0: embed.add_field( name="\u200b", value=( S.PRESTIGE_UI["rank_line"].format(level=p_level, pp=p_pp) + "\n" + S.PRESTIGE_UI["rank_season"].format(exp=s_exp) ), inline=False, ) if level < 10: embed.set_footer(text=S.RANK_UI["footer_t1"]) elif level < 20: embed.set_footer(text=S.RANK_UI["footer_t2"]) else: embed.set_footer(text=S.RANK_UI["footer_t3"]) await interaction.response.send_message(embed=embed, ephemeral=True) if not kasutaja and interaction.guild: member = interaction.guild.get_member(target.id) if member: asyncio.create_task(ensure_level_role(member, level)) @tree.command(name="stats", description=S.CMD["stats"]) @app_commands.describe(kasutaja=S.OPT["stats_kasutaja"]) async def cmd_stats(interaction: discord.Interaction, kasutaja: discord.Member | None = None): target = kasutaja or interaction.user data = await economy.get_user(target.id) def _s(key: str) -> int: return data.get(key, 0) embed = discord.Embed( title=f"{S.TITLE['stats']} - {target.display_name}", color=0x5865F2, ) embed.add_field( name=S.STATS_UI["economy_field"], value=S.STATS_UI["economy_val"].format( peak=coin(_s("peak_balance")), earned=coin(_s("lifetime_earned")), lost=coin(_s("lifetime_lost")), ), inline=True, ) embed.add_field( name=S.STATS_UI["work_field"], value=S.STATS_UI["work_val"].format(work=_s("work_count"), beg=_s("beg_count")), inline=True, ) embed.add_field(name="\u200b", value="\u200b", inline=False) embed.add_field( name=S.STATS_UI["gamble_field"], value=S.STATS_UI["gamble_val"].format( wagered=coin(_s("total_wagered")), win=coin(_s("biggest_win")), loss=coin(_s("biggest_loss")), jackpots=_s("slots_jackpots"), ), inline=True, ) embed.add_field( name=S.STATS_UI["crime_field"], value=S.STATS_UI["crime_val"].format( crimes=_s("crimes_attempted"), succeeded=_s("crimes_succeeded"), heists=_s("heists_joined"), heists_won=_s("heists_won"), jailed=_s("times_jailed"), bail=coin(_s("total_bail_paid")), ), inline=True, ) embed.add_field(name="\u200b", value="\u200b", inline=False) embed.add_field( name=S.STATS_UI["social_field"], value=S.STATS_UI["social_val"].format( given=coin(_s("total_given")), received=coin(_s("total_received")), ), inline=True, ) embed.add_field( name=S.STATS_UI["records_field"], value=S.STATS_UI["records_val"].format(streak=_s("best_daily_streak")), inline=True, ) await interaction.response.send_message(embed=embed, ephemeral=True)