"""
PokeGambler - A Pokemon themed gambling bot for Discord.
Copyright (C) 2021 Harshith Thota
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
----------------------------------------------------------------------------
Profile Commands Module
"""
# pylint: disable=unused-argument
# pylint: disable=too-many-locals, too-many-lines
from __future__ import annotations
import random
from datetime import datetime, timedelta
from io import BytesIO
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
import discord
from PIL import Image
from ..base.items import Chest
from ..base.modals import CallbackReplyModal
from ..base.models import (
Blacklist, Boosts, CommandData, Inventory,
Loots, Matches, Minigame, Profiles, Votes
)
from ..base.shop import BoostItem
from ..base.views import CallbackButton, LinkView, SelectView
from ..helpers.checks import user_check
from ..helpers.imageclasses import (
BadgeGenerator, LeaderBoardGenerator,
ProfileCardGenerator, WalletGenerator
)
from ..helpers.unicodex import UnicodeProgressBar, Unicodex
from ..helpers.utils import (
LineTimer, dm_send, get_embed, get_formatted_time,
get_modules, img2file, wait_for
)
from ..helpers.validators import HexValidator, ImageUrlValidator
from .basecommand import (
Commands, alias, check_completion, cache_images,
cooldown, defer, get_commands_btn_view, model,
get_profile, needs_ticket
)
if TYPE_CHECKING:
from discord import Member, Message
[docs]class ProfileCommands(Commands):
"""
Commands that deal with the profile system of PokeGambler.
One of the most feature rich command category.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.pcg = ProfileCardGenerator(self.ctx.assets_path)
self.walletgen = WalletGenerator(self.ctx.assets_path)
self.lbg = LeaderBoardGenerator(self.ctx.assets_path)
self.bdgen = BadgeGenerator(self.ctx.assets_path)
[docs] @needs_ticket("Background Change")
@check_completion
@model([Profiles, Inventory])
@alias('bg')
async def cmd_background(self, message: Message, **kwargs):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
.. meta::
:description: Change profile background.
:aliases: bg
.. rubric:: Syntax
.. code:: coffee
/background
.. rubric:: Description
Change the background in your Profile card.
.. note::
The Background Change ticket can be purchased
from the Consumables :class:`~scripts.base.shop.Shop`
"""
profile = Profiles(message.author)
inp_msg = await dm_send(
message, message.author,
embed=get_embed(
"The image size should be greater than **960x540**.\n"
"Make sure it uses the *same aspect ratio* as well.\n"
"Supported Extension: `PNG` and `JPEG`\n"
"> Will rollback to default background "
"if link is not accessible any time.\n"
"⚠️Using inappropriate images will get you blacklisted.\n"
"**Mention me in the message if you upload the image.**",
title="Enter the image url or upload it.",
color=profile.get('embed_color')
)
)
reply = await wait_for(
inp_msg.channel, self.ctx,
init_msg=inp_msg,
check=lambda msg: user_check(
msg, message, inp_msg.channel
) and msg.attachments,
timeout="inf"
)
url = await self.__background_get_url(message, reply)
if not url:
return
profile.update(
background=url
)
inv = Inventory(message.author)
tickets = kwargs["tickets"]
inv.delete(tickets[0], quantity=1)
await dm_send(
message, message.author,
embed=get_embed(
"You can check your profile now.",
title="Succesfully updated Profile Background.",
color=profile.get('embed_color')
)
)
[docs] @alias("bdg")
@model(Profiles)
@cache_images
async def cmd_badges(self, message: Message, **kwargs):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
.. meta::
:description: Check badge progress.
:aliases: bdg
.. rubric:: Syntax
.. code:: coffee
/badges
.. rubric:: Description
Check the list of available badges and what all you have unlocked.
"""
profile = await get_profile(self.ctx, message, message.author)
badges = profile.get_badges()
badgestrip = self.bdgen.get(badges)
discord_file = img2file(badgestrip, "badges.png", ext="PNG")
msg = await message.reply(file=discord_file)
self.cmd_badges.__dict__["image_cache"][message.author.id].register(
msg.attachments[0].proxy_url
)
[docs] @alias(["bal", "chips"])
@model(Profiles)
@cache_images
async def cmd_balance(self, message: Message, **kwargs):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
.. meta::
:description: Check balance pokechips.
:aliases: bal, chips
.. rubric:: Syntax
.. code:: coffee
/bal
.. rubric:: Description
Quickly check how many {pokechip_emoji} you have.
"""
profile = (
await get_profile(self.ctx, message, message.author)
).get()
data = {
key: (
f"{int(val):,}" if key in [
"won_chips", "pokebonds", "balance"
]
else str(val)
)
for key, val in profile.items()
}
wallet = self.walletgen.get(data)
discord_file = img2file(wallet, "wallet.png", ext="PNG")
msg = await message.reply(
file=discord_file
)
self.cmd_balance.__dict__["image_cache"][message.author.id].register(
msg.attachments[0].proxy_url
)
[docs] @model([Boosts, BoostItem, Profiles])
# pylint: disable=no-self-use
async def cmd_boosts(self, message: Message, **kwargs):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
.. meta::
:description: Check active boosts.
.. rubric:: Syntax
.. code:: coffee
/boosts
.. rubric:: Description
Check your active purchased boosts.
"""
def get_desc(boost):
prm_bst = perm_boosts[boost['name'].lower().replace(' ', '_')]
total = boost['stack'] + prm_bst
desc_str = f"{boost['description']}\nStack: {total}"
if prm_bst > 0:
desc_str += f" ({prm_bst} Permanent)"
expires_in = (30 * 60) - (
datetime.utcnow() - boost["added_on"]
).total_seconds()
if expires_in > 0 and boost['stack'] > 0:
expires_in = get_formatted_time(
expires_in, show_hours=False
).replace('**', '')
else:
expires_in = "Expired / Not Purchased Yet"
desc_str += f"\nExpires in: {expires_in}"
return f"```css\n{desc_str}\n```"
boosts = BoostItem.get_boosts(str(message.author.id))
profile = Profiles(message.author)
perm_boosts = Boosts(message.author).get()
perm_boosts.pop('user_id')
if not (
boosts or any(
bst > 1
for bst in perm_boosts.values()
)
):
await message.reply(
embed=get_embed(
"You don't have any active boosts.",
title="No Boosts",
color=profile.get('embed_color')
)
)
return
emb = get_embed(
"\u200B",
title="Active Boosts",
color=profile.get('embed_color')
)
if not boosts:
boosts = BoostItem.default_boosts()
for val in boosts.values():
emb.add_field(
name=val["name"],
value=get_desc(val),
inline=False
)
await message.reply(embed=emb)
[docs] @alias(['timers', 'cd', 'cds', 'tm', 'tms'])
@cooldown(120)
@model([Profiles, Loots, Votes, Boosts, BoostItem])
async def cmd_cooldowns(self, message: Message, **kwargs):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
.. meta::
:description: Check the timers for loot, vote and daily.
.. rubric:: Syntax
.. code:: coffee
/timers
.. rubric:: Description
Check the time remaining before you can use the command again for:
* Loot
* Daily
* Vote
"""
def rand_style():
return random.choice(['squares', 'circles'])
def _clean_cmd(cmd):
return cmd.__name__.replace('cmd_', '').title()
def _get_cmd_cd(cmd, timestamp):
return cmd.__dict__['cooldown'] - (
datetime.now() - timestamp
).total_seconds()
def _format_cmd_cd(cmd, timestamp):
time_remaining = _get_cmd_cd(cmd, timestamp)
if time_remaining <= 0:
return "**now**."
cmd_pb = UnicodeProgressBar(style=rand_style()).get(
int(
(
(
cmd.__dict__['cooldown'] - time_remaining
) / cmd.__dict__['cooldown']
) * 5
)
)
return f"in {get_formatted_time(time_remaining)}\n`{cmd_pb}`"
def _get_message(key, elapsed, total, show_hours=True):
message = "**now**."
if elapsed < total:
prog_bar = UnicodeProgressBar(style=rand_style()).get(
int(
(elapsed / total) * 5
)
)
ts_fmt = get_formatted_time(
total - elapsed, show_hours=show_hours
)
message = f"in {ts_fmt}.\n`{prog_bar}`"
return f"You can use {key.title()} again {message}"
emb_data = {}
loot_elapsed, loot_total = self.__loot_get_cooldown(
message, Boosts(message.author),
BoostItem.get_boosts(str(message.author.id))
)
emb_data['Loot'] = _get_message(
'loot', loot_elapsed, loot_total,
show_hours=False
)
(
daily_elapsed, daily_base_cd, *_
) = self.__daily_get_cooldown(
Loots(message.author)
)
emb_data['Daily'] = _get_message('daily', daily_elapsed, daily_base_cd)
_, vote_elapsed, vote_total = self.__vote_get_cooldown(
Votes(message.author)
)
emb_data['Vote'] = _get_message('vote', vote_elapsed, vote_total)
emb_data.update({
_clean_cmd(cmd): f"You can use **{_clean_cmd(cmd)}** again "
f"{_format_cmd_cd(cmd, user_obj[message.author])}"
for cmd, user_obj in self.ctx.cooldown_cmds.items()
if message.author in user_obj and _get_cmd_cd(
cmd, user_obj[message.author]
) > 0
})
emb = get_embed(
title='Your cooldowns',
color=Profiles(message.author).get('embed_color')
)
for key, value in emb_data.items():
emb.add_field(name=key, value=value, inline=False)
await dm_send(message, message.author, embed=emb)
[docs] @model([Loots, Profiles, Chest, Inventory])
@alias('dl')
async def cmd_daily(self, message: Message, **kwargs):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
.. meta::
:description: Daily source of pokechips.
.. rubric:: Syntax
.. code:: coffee
/daily
.. rubric:: Description
Claim free {pokechip_emoji} and a chest everyday.
The chips and the chest both scale with Tier.
There are 3 tiers
* Everyone starts at Tier 1.
* Win 25 gamble matches to unlock Tier 2.
* Win 100 gamble matches to unlock Tier 3.
.. tip::
You can maintain a daily streak.
Get more (scalable) chips for every 5 day streak.
"""
profile = Profiles(message.author)
loot_model = Loots(message.author)
boost_model = Boosts(message.author)
loot_info = loot_model.get()
boost_info = boost_model.get()
boost = boost_info["lucky_looter"] + 1
earned = loot_info["earned"]
tier = loot_info["tier"]
daily_streak = loot_info["daily_streak"]
(
elapsed_time, cd_time,
time_remaining, last_claim
) = self.__daily_get_cooldown(loot_info)
if elapsed_time < cd_time:
await message.add_reaction("⌛")
await message.reply(
embed=get_embed(
f"Please wait {time_remaining} before claiming Daily.",
embed_type="warning",
title="Too early"
)
)
return
if (
datetime.now() - last_claim
).total_seconds() >= (cd_time * 2):
# Reset streak on missing by a day
daily_streak = 0
else:
daily_streak += 1
loot = random.randint(5, 10) * boost * (10 ** tier)
if daily_streak % 5 == 0 and daily_streak > 0:
loot += 100 * (daily_streak / 5)
chest = Chest.get_chest(tier=tier)
chest.save()
Inventory(message.author).save(chest.itemid)
embed = get_embed(
f"Here's your daily **{chest}**.\n"
f"Claim the chest with `/open {chest.itemid}`.",
title="**Daily Chest**",
thumbnail=chest.asset_url,
footer="You get bonus pokechips for every 5 streak.",
color=profile.get('embed_color')
)
profile.credit(int(loot))
loot_model.update(
earned=(earned + loot),
daily_streak=daily_streak,
daily_claimed_on=datetime.today()
)
stk_name, stk_val = Unicodex.format_streak(daily_streak)
embed.add_field(
name=stk_name,
value=stk_val
)
await message.reply(
f"**Daily loot of {int(loot)} {self.chip_emoji} "
"added to your balance.**",
embed=embed
)
# pylint: disable=no-self-use
[docs] @needs_ticket("Embed Color Change")
@check_completion
@model([Profiles, Inventory])
@alias(['embed', 'ec', 'color'])
async def cmd_embed_color(self, message: Message, **kwargs):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
.. meta::
:description: Change Embed Color.
:aliases: embed, ec, color
.. rubric:: Syntax
.. code:: coffee
/embed_color
.. rubric:: Description
Change the color of the embeds you get from PokeGambler.
.. note::
The Embed Color Change ticket can be purchased
from the secret Consumables :class:`~scripts.base.shop.Shop`
"""
async def callback(modal, interaction):
if not modal.results:
return
color_hex = modal.results[0]
proceed = await HexValidator(
message=(modal.latest_interaction or message),
on_error={
"title": "Invalid Hexadecimal Color Code",
"description": "You need to enter a valid hex code."
},
dm_user=True
).validate(color_hex)
if not proceed:
return
hexcode = color_hex.lstrip('#')
profile = Profiles(message.author)
profile.update(
embed_color=int(hexcode, 16)
)
inv = Inventory(message.author)
tickets = kwargs["tickets"]
inv.delete(tickets[0], quantity=1)
await dm_send(
interaction, message.author,
embed=get_embed(
title="Succesfully updated your Embed Color.",
color=profile.get('embed_color')
)
)
modal = CallbackReplyModal(
callback=callback,
title="Enter the Color Hexadecimal Code",
check=lambda x: x.user.id == message.author.id
)
modal.add_short("Enter Color Code")
await message.response.send_modal(modal)
[docs] @defer
@model(Profiles)
@alias("lb")
async def cmd_leaderboard(
self, message: Message,
sort_by: Optional[str] = "Balance",
**kwargs
):
# pylint: disable=too-many-locals
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
:param sort_by: The field to sort by.
:type sort_by: Optional[str]
:choices sort_by: [Balance, Minigame]
:default sort_by: Balance
.. meta::
:description: Check the global leaderboard.
:aliases: lb
.. rubric:: Syntax
.. code:: coffee
/leaderboard [sort_by:Balance/Minigame]
.. rubric:: Description
Check the global PokeGambler leaderboard.
By default, ranks are sorted according to number of wins.
You can also sort it according to balance and any minigame.
.. rubric:: Examples
* To check the leaderboard
.. code:: coffee
:force:
/leaderboard
* To check the leaderboard in terms of balance
.. code:: coffee
:force:
/leaderboard sort_by:Balance
* To check the leaderboard for QuickFlip
.. code:: coffee
:force:
/leaderboard sort_by:Minigame
"""
if sort_by == "Minigame":
leaderboard = await self.__lb_handle_mg_input(message)
if leaderboard is None:
return
else:
sort_by = [
"num_wins", "num_matches"
] if not sort_by else ["balance"]
leaderboard = Profiles.get_leaderboard(
sort_by=sort_by
)
if not leaderboard:
await message.reply(
embed=get_embed(
"No matches were played yet.",
embed_type="warning"
)
)
return
lbd = []
idx = 0
for data in leaderboard:
if not self.ctx.get_user(int(data["user_id"])):
continue
data["rank"] = idx + 1
data["balance"] = f'{int(data["balance"]):,}'
lbd.append(data)
idx += 1
embeds = []
files = []
with LineTimer(self.logger, "Create Leaderboard Images"):
for i in range(0, len(lbd), 4):
batch_4 = lbd[i: i + 4]
img = await self.lbg.get(self.ctx, batch_4)
lb_fl = img2file(img, f"leaderboard{i}.jpg")
emb = discord.Embed(
title="",
description="",
color=discord.Colour.dark_theme()
)
embeds.append(emb)
files.append(lb_fl)
if not embeds:
await message.reply(
embed=get_embed(
"No matches were played yet.",
embed_type="warning"
)
)
return
with LineTimer(self.logger, "Leaderboard Pagination"):
await self.paginate(message, embeds, files)
[docs] @model([Loots, Profiles, Chest, Inventory])
@alias('lt')
async def cmd_loot(self, message: Message, **kwargs):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
.. meta::
:description: Stable source of Pokechips.
:aliases: lt
.. rubric:: Syntax
.. code:: coffee
/loot
.. rubric:: Description
Search the void for free {pokechip_emoji}.
The number of chips is randomly choosen from 5 to 10.
There is a cooldown of 10 minutes between loots.
.. tip::
Loot Increase and Cooldown Reduction
:class:`~scripts.base.models.Boosts` can be purchased from
the :class:`~scripts.base.shop.Shop`.
"""
perm_boosts = Boosts(message.author).get()
boosts = BoostItem.get_boosts(str(message.author.id))
on_cooldown = self.ctx.loot_cd.get(message.author, None)
elapsed, total_cd = self.__loot_get_cooldown(
message, perm_boosts, boosts
)
on_cd = await self.__loot_handle_cd(
message, on_cooldown, (total_cd - elapsed)
)
if on_cd:
return
loot_mult = 1 + (perm_boosts["lucky_looter"] * 0.05)
tr_mult = 0.1 * (perm_boosts["fortune_burst"] + 1)
loot_mult += 0.05 * boosts['boost_lt']['stack']
tr_mult += 0.1 * boosts['boost_tr']['stack']
profile = Profiles(message.author)
loot_model = Loots(message.author)
loot_info = loot_model.get()
earned = loot_info["earned"]
tier = loot_info["tier"]
loot = int(
random.randint(5, 10) * (
10 ** (tier - 1)
) * loot_mult
)
embed = None
if random.uniform(0, 1.0) <= tr_mult:
embed = self.__loot_handle_treasure(message, profile, tier)
profile.credit(loot)
loot_model.update(earned=earned + loot)
await message.reply(
f"**You found {loot} {self.chip_emoji}! "
"Added to your balance.**",
embed=embed
)
[docs] @model([Profiles, Blacklist])
@alias("pr")
@cache_images
async def cmd_profile(
self, message: Message,
user: Optional[discord.Member] = None,
**kwargs
):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
:param user: The user to get the profile of.
:type user: Optional[:class:`discord.Member`]
:default user: Author of the Message
.. meta::
:description: Get the profile of a user.
:aliases: pr
.. rubric:: Syntax
.. code:: coffee
/profile [user:@User]
.. rubric:: Description
Check your or someone's PokeGambler profile.
To check someone's profile, provide their ID or mention them.
.. rubric:: Examples
* To check your own profile
.. code:: coffee
:force:
/profile
* To check Alan's profile
.. code:: coffee
:force:
/profile user:@Alan#1234
* To check profile of user with ID 12345
.. code:: coffee
:force:
/profile user:12345
"""
if user is None:
user = message.author
profile = await get_profile(self.ctx, message, user)
if not profile:
return
badges = profile.get_badges()
profile = profile.get()
avatar_byio = BytesIO()
await user.display_avatar.with_size(512).save(avatar_byio)
avatar = Image.open(avatar_byio)
name = profile["name"]
balance = f'{int(profile["balance"]):,}'
num_played = str(profile["num_matches"])
num_won = str(profile["num_wins"])
background = None
if profile.get("background", None):
background = await self.__profile_get_bg(profile)
profilecard = self.pcg.get(
name, avatar, balance,
num_played, num_won, badges,
blacklisted=Blacklist.is_blacklisted(
str(user.id)
), background=background
)
discord_file = img2file(profilecard, "profilecard.jpg")
msg = await message.reply(file=discord_file)
self.cmd_profile.__dict__["image_cache"][user.id].register(
msg.attachments[0].proxy_url
)
[docs] @defer
@model(Profiles)
@alias("#")
@cache_images
async def cmd_rank(self, message: Message, **kwargs):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
.. meta::
:description: Check user rank.
:aliases: #
.. rubric:: Syntax
.. code:: coffee
/rank
.. rubric:: Description
Creates your PokeGambler Rank card.
Rank is decided based on number of wins.
"""
with LineTimer(self.logger, "Get Profile"):
profile = await get_profile(
self.ctx,
message,
message.author
)
rank = profile.get_rank()
data = profile.get()
data["rank"] = rank or 0
data["balance"] = f'{int(data["balance"]):,}'
with LineTimer(self.logger, "Create Rank Image"):
img = await self.lbg.get_rankcard(self.ctx, data, heading=True)
discord_file = img2file(
img,
f"rank_{message.author}.png",
ext="PNG"
)
with LineTimer(self.logger, "Send Rank Image"):
msg = await message.reply(file=discord_file)
self.cmd_rank.__dict__["image_cache"][message.author.id].register(
msg.attachments[0].proxy_url
)
[docs] @model([Minigame, Loots, CommandData])
# pylint: disable=no-self-use
async def cmd_stats(self, message: Message, **kwargs):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
.. meta::
:description: Check match and minigame stats.
.. rubric:: Syntax
.. code:: coffee
/stats
.. rubric:: Description
Check the number of gamble matches & minigames you've played and won.
"""
match_stats = Matches(
message.author
).get_stats()
stat_dict = {
"Gambles": f"Played: {match_stats[0]}\n"
f"Won: {match_stats[1]}"
}
for minigame_cls in Minigame.__subclasses__():
minigame = minigame_cls(message.author)
stat_dict[
minigame_cls.__name__
] = f"Played: {minigame.num_plays}\n" + \
f"Won: {minigame.num_wins}"
loots_earned = Loots(message.author).earned
num_cmds = CommandData.num_user_cmds(str(message.author.id))
stat_dict["Misc."] = f"Looted: {loots_earned}\n" + \
f"Commands: {num_cmds}"
emb = get_embed(
"Here's how you've performed till now.",
title=f"Statistics for **{message.author.name}**",
color=Profiles(message.author).get('embed_color')
)
for idx, (key, val) in enumerate(stat_dict.items()):
if idx and (idx % 2 == 0):
emb.add_field(name="\u200B", value="\u200B")
emb.add_field(
name=f"**{key}**",
value=f"```rb\n{val}\n```"
)
await message.reply(embed=emb)
[docs] @model([Profiles, Votes, Loots, Inventory])
@alias("v")
async def cmd_vote(self, message: Message, **kwargs):
"""
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
.. meta::
:description: Vote for the bot.
:aliases: v
.. rubric:: Syntax
.. code:: coffee
/vote
.. rubric:: Description
Vote for the bot on Top.gg.
Get 100 chips for every vote.
.. tip::
Bonus rewards on every 5 streak.
"""
user = getattr(message, "author", message.user)
votes = Votes(user)
streak = votes.vote_streak
profile = Profiles(user)
emb, elapsed = self.__vote_prep_embed(votes, streak, profile)
loot_view = None
if not votes.reward_claimed:
profile.credit(100)
content = "Thanks for voting, you've been given " + \
f"100 {self.chip_emoji}."
if streak % 5 == 0 and votes.vote_streak > 0:
tier = Loots(user).tier
chest = Chest.get_chest(tier=tier)
chest.save()
Inventory(user).save(chest.itemid)
content += "\nYou've been given a bonus Chest!\n" + \
f"『{chest.emoji}』**{chest}** - **{chest.itemid}**"
cleared_cds = ["Loot"]
cleared_cds.extend(
cmd.__name__.replace("cmd_", "").title()
for cmd in self.ctx.cooldown_cmds
if self.ctx.cooldown_cmds[cmd].pop(
user, None
)
)
self.ctx.loot_cd.pop(user, None)
cd_cmd_str = "\n".join(
f"{idx + 1}. {cmd}"
for idx, cmd in enumerate(cleared_cds)
)
content += "\nCooldowns cleared for following commands:" + \
f"\n```md\n{cd_cmd_str}\n```"
loot_view = get_commands_btn_view(
message, [self.ctx.profilecommands.cmd_loot], [{}]
)
votes.update(reward_claimed=True)
emb.add_field(
name="Rewards Added",
value=content,
inline=False
)
else:
emb.add_field(
name="Streak Rewards",
value="```md\n1. Tier scaled Chest\n"
"2. Clears all cooldowns (except daily)\n```",
inline=False
)
async def retrigger_command(view, interaction):
# pylint: disable=import-outside-toplevel
from ..base.handlers import CustomInteraction
custom_interaction = CustomInteraction(interaction)
await self.cmd_vote(custom_interaction)
vote_button = LinkView(
url=f"https://top.gg/bot/{self.ctx.user.id}/vote",
label="Vote Now!",
check=lambda inctn: inctn.user.id == user.id
)
if votes.reward_claimed:
vote_button.add_item(
CallbackButton(
callback=retrigger_command,
label="Check"
)
)
to_send = {
"embed": emb
}
if elapsed >= 12:
to_send["view"] = vote_button
if loot_view:
to_send["view"] = loot_view
await message.reply(**to_send)
@staticmethod
async def __background_get_url(message, reply):
if len(reply.attachments) > 0:
if reply.attachments[0].content_type not in (
"image/png", "image/jpeg"
):
await dm_send(
message, message.author,
embed=get_embed(
"Please make sure it's a png or a jpeg image.",
embed_type="error",
title="Invalid Image"
)
)
return None
return reply.attachments[0].proxy_url
proceed = await ImageUrlValidator(
message=message,
on_error={
"title": "Invalid Image",
"description": "Please make sure it's a png or a jpeg image."
},
dm_user=True
).validate(reply.content)
if not proceed:
return None
return reply.content
@staticmethod
def __daily_get_cooldown(
loot_info: Union[Dict, Loots]
) -> Tuple[str, int, int, datetime]:
"""Get a list of time counts for the daily cooldown.
:param loot_info: The Loot object for the User.
:type loot_info: :class:`~scripts.base.models.Loots`
:return: Time remaining till next daily, elapsed time, cd & last claim.
:rtype: Tuple[str, int, int, datetime]
"""
if isinstance(loot_info, Loots):
loot_info = loot_info.get()
last_claim = loot_info["daily_claimed_on"]
if isinstance(last_claim, str):
last_claim = datetime.strptime(
last_claim,
"%Y-%m-%d %H:%M:%S"
)
cd_time = 24 * 60 * 60
elapsed_time = (datetime.now() - last_claim).total_seconds()
time_remaining = get_formatted_time(
cd_time - elapsed_time
)
return elapsed_time, cd_time, time_remaining, last_claim
def __lb_get_minigame_lb(self, mg_name: str, user: Member) -> List[Dict]:
def _commands(module):
return [
attr.replace("cmd_", "")
for attr in dir(module)
if all([
attr.startswith("cmd_"),
attr not in getattr(module, "alias", [])
])
]
def _aliases(module):
return [
alias.replace("cmd_", "")
for alias in getattr(module, "alias", [])
]
def _get_lb(modules, mg_name, user):
leaderboard = None
for module in modules:
possibilities = _aliases(module) + _commands(module)
if any([
mg_name in possibilities,
mg_name.rstrip('s') in possibilities
]):
command = getattr(
module,
f"cmd_{mg_name}",
getattr(module, f"cmd_{mg_name.rstrip('s')}")
)
models = getattr(command, "models", [])
if not models:
continue
for model_ in models:
if issubclass(model_, Minigame):
leaderboard = model_(
user
).get_lb()
if leaderboard:
return leaderboard
return leaderboard
modules = get_modules(self.ctx)
return _get_lb(modules, mg_name, user)
async def __lb_handle_mg_input(self, message):
mg_view = SelectView(
heading="Choose the Minigame",
options={
mg.__name__: ""
for mg in Minigame.__subclasses__()
},
no_response=True,
check=lambda x: x.user.id == message.author.id
)
await message.channel.send(
content="Which minigame do you need the Leaderboard for?",
view=mg_view
)
await mg_view.dispatch(self)
if mg_view.result is None:
return
lbrd = self.__lb_get_minigame_lb(
mg_view.result.lower(),
message.author
)
leaderboard = []
lbrd = [
{
"member": (
message.guild.get_member(int(res["_id"]))
or self.ctx.get_guild(
self.ctx.official_server
).get_member(int(res["_id"]))
),
**res,
"rank": idx + 1
}
for idx, res in enumerate(lbrd)
if (
message.guild.get_member(int(res["_id"]))
or self.ctx.get_guild(
self.ctx.official_server
).get_member(int(res["_id"]))
)
]
for res in lbrd:
profile = Profiles(res["member"]).get()
balance = res.get("earned", 0) or profile["balance"]
name = profile["name"]
leaderboard.append({
"rank": res["rank"],
"user_id": res["_id"],
"name": name,
"num_matches": res["num_matches"],
"num_wins": res["num_wins"],
"balance": balance
})
return leaderboard
def __loot_get_cooldown(
self, message: Message,
perm_boosts: Union[Dict, Boosts],
temp_boosts: Dict
) -> Tuple[int, int]:
"""Get the loot cooldown for the user.
:param message: The message which triggered this command.
:type message: :class:`discord.Message`
:param perm_boosts: The permanent boosts for the user.
:type perm_boosts: Union[Dict, Boosts]
:param temp_boosts: The temporary boosts for the user.
:type temp_boosts: Dict
:return: The time remaining and total cd time in seconds.
:rtype: Tuple[int, int]
"""
if isinstance(perm_boosts, Boosts):
perm_boosts = perm_boosts.get()
cd_reducer = perm_boosts["loot_lust"]
cd_reducer += temp_boosts['boost_lt_cd']['stack']
cd_time = 60 * (10 - cd_reducer)
loot_cd = self.ctx.loot_cd.get(
message.author,
datetime.now() - timedelta(minutes=10)
)
elapsed = (
datetime.now() - loot_cd
).total_seconds()
return elapsed, cd_time
async def __loot_handle_cd(self, message, on_cooldown, time_remaining):
if on_cooldown and time_remaining > 0:
await message.add_reaction("⌛")
remaining = get_formatted_time(time_remaining, show_hours=False)
await message.reply(
embed=get_embed(
f"Please wait {remaining} before looting again.",
embed_type="warning",
title="On Cooldown"
)
)
return True
self.ctx.loot_cd[message.author] = datetime.now()
return False
@staticmethod
def __loot_handle_treasure(message, profile, tier):
chest = Chest.get_chest(tier=tier)
chest.save()
Inventory(message.author).save(chest.itemid)
return get_embed(
f"Woah! You got lucky and found a **{chest}**.\n"
"It's been added to your inventory.",
title="**FOUND A TREASURE CHEST**",
thumbnail=chest.asset_url,
footer=f"Chest ID: {chest.itemid}",
color=profile.get('embed_color')
)
async def __profile_get_bg(self, profile):
bg_byio = BytesIO()
try:
async with self.ctx.sess.get(profile["background"]) as resp:
data = await resp.read()
bg_byio.write(data)
bg_byio.seek(0)
background = Image.open(bg_byio).resize((960, 540))
except Exception: # pylint: disable=broad-except
background = None
return background
@staticmethod
def __vote_get_cooldown(votes: Votes) -> Tuple[str, int, int]:
"""Get the cooldown for the vote command.
:param votes: The votes object for the User.
:type votes: :class:`~scripts.base.models.Votes`
:return: The cooldown message, elapsed time, base cooldown(12hrs).
:rtype: Tuple[str, int, int]
"""
last_voted = votes.last_voted
now = datetime.now()
elapsed = (now - last_voted).total_seconds()
base_cd = 12
if elapsed // 3600 >= base_cd:
cd_msg = "You can vote **now**!"
else:
ends_on = last_voted + timedelta(hours=base_cd)
tot_secs = (ends_on - now).total_seconds()
cd_msg = f"You can vote again in {get_formatted_time(tot_secs)}."
return cd_msg, elapsed, (base_cd * 3600)
def __vote_prep_embed(self, votes, streak, profile):
emb = get_embed(
title="Vote",
content="Vote for the bot on **Top.gg** to get rewards.\n"
f"You'll get **100** {self.chip_emoji} on every vote.\n"
"Bonus rewards for every **5** streak.",
color=profile.get('embed_color'),
footer="Re-use this command after voting "
"to autoclaim rewards."
)
stk_name, stk_val = Unicodex.format_streak(
streak,
mode="vote"
)
if votes.reward_claimed:
stk_val = stk_val.replace("🎁", "")
emb.add_field(
name=stk_name,
value=stk_val
)
cd_msg, elapsed, _ = self.__vote_get_cooldown(votes)
emb.add_field(
name="Vote Cooldown",
value=cd_msg,
inline=False
)
return emb, elapsed