846 lines
36 KiB
Python
846 lines
36 KiB
Python
import asyncio
|
|
import logging
|
|
import typing as t
|
|
from contextlib import suppress
|
|
from io import BytesIO
|
|
from pathlib import Path
|
|
|
|
import discord
|
|
from aiocache import cached
|
|
from discord import app_commands
|
|
from discord.app_commands import Choice
|
|
from redbot.core import commands
|
|
from redbot.core.bot import Red
|
|
from redbot.core.i18n import Translator, cog_i18n
|
|
from redbot.core.utils.chat_formatting import humanize_list
|
|
|
|
from ..abc import MixinMeta
|
|
from ..common import const, formatter, utils
|
|
from ..generator import imgtools
|
|
from ..generator.tenor.converter import sanitize_url
|
|
from ..views.dynamic_menu import DynamicMenu
|
|
|
|
_ = Translator("LevelUp", __file__)
|
|
log = logging.getLogger("red.vrt.levelup.commands.user")
|
|
|
|
|
|
@app_commands.context_menu(name="View Profile")
|
|
@app_commands.guild_only()
|
|
async def view_profile_context(interaction: discord.Interaction, member: discord.Member):
|
|
"""View a user's profile"""
|
|
bot: Red = interaction.client
|
|
cog = bot.get_cog("LevelUp")
|
|
if not cog:
|
|
return await interaction.response.send_message(_("LevelUp is not loaded!"), ephemeral=True)
|
|
if member.bot and cog.db.ignore_bots:
|
|
return await interaction.response.send_message(_("Bots cannot have profiles!"), ephemeral=True)
|
|
if not isinstance(interaction.user, discord.Member):
|
|
return await interaction.response.send_message(_("This user is no longer in the server!"), ephemeral=True)
|
|
if not isinstance(member, discord.Member):
|
|
return await interaction.response.send_message(_("This user is no longer in the server!"), ephemeral=True)
|
|
with suppress(discord.HTTPException):
|
|
await interaction.response.defer(ephemeral=True)
|
|
result = await cog.get_user_profile_cached(member)
|
|
try:
|
|
if isinstance(result, discord.Embed):
|
|
await interaction.followup.send(embed=result, ephemeral=True)
|
|
else:
|
|
await interaction.followup.send(file=result, ephemeral=True)
|
|
except discord.HTTPException:
|
|
if isinstance(result, discord.Embed):
|
|
await interaction.channel.send(embed=result)
|
|
else:
|
|
result.fp.seek(0)
|
|
await interaction.channel.send(file=result)
|
|
|
|
|
|
@cog_i18n(_)
|
|
class User(MixinMeta):
|
|
@commands.hybrid_command(name="leveltop", aliases=["lvltop", "topstats", "membertop", "topranks"])
|
|
@commands.guild_only()
|
|
async def leveltop(
|
|
self,
|
|
ctx: commands.Context,
|
|
stat: str = "xp",
|
|
globalstats: bool = False,
|
|
displayname: bool = True,
|
|
):
|
|
"""View the LevelUp leaderboard
|
|
|
|
**Arguments**
|
|
`stat` - The stat to view the leaderboard for, defaults to `exp` but can be any of the following:
|
|
- `xp` - Experience
|
|
- `level` - Level
|
|
- `voice` - Voicetime
|
|
- `messages` - Messages
|
|
- `stars` - Stars
|
|
`globalstats` - View the global leaderboard instead of the server leaderboard
|
|
`displayname` - Use display names instead of usernames
|
|
"""
|
|
stat = stat.lower()
|
|
pages = await asyncio.to_thread(
|
|
formatter.get_leaderboard,
|
|
bot=self.bot,
|
|
guild=ctx.guild,
|
|
db=self.db,
|
|
stat=stat,
|
|
lbtype="lb",
|
|
is_global=globalstats,
|
|
member=ctx.author,
|
|
use_displayname=displayname,
|
|
color=await self.bot.get_embed_color(ctx),
|
|
)
|
|
if isinstance(pages, str):
|
|
return await ctx.send(pages)
|
|
await DynamicMenu(ctx, pages).refresh()
|
|
|
|
@commands.command(name="roletop")
|
|
@commands.guild_only()
|
|
async def role_group_leaderboard(self, ctx: commands.Context):
|
|
"""View the leaderboard for roles"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if not conf.role_groups:
|
|
return await ctx.send(_("Role groups have not been configured in this server yet!"))
|
|
pages = await asyncio.to_thread(
|
|
formatter.get_role_leaderboard,
|
|
rolegroups=conf.role_groups,
|
|
color=await self.bot.get_embed_color(ctx),
|
|
)
|
|
if not pages:
|
|
return await ctx.send(_("No data available yet!"))
|
|
await DynamicMenu(ctx, pages).refresh()
|
|
|
|
@commands.command(name="profiledata")
|
|
@commands.guild_only()
|
|
@commands.mod_or_permissions(manage_messages=True)
|
|
async def profile_data(self, ctx: commands.Context, user_id: int):
|
|
"""View a user's profile by ID
|
|
|
|
Useful if user has left the server
|
|
"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if not conf.enabled:
|
|
return await ctx.send(_("Leveling is disabled in this server!"))
|
|
if user_id not in conf.users:
|
|
return await ctx.send(_("That user has no level data yet!"))
|
|
profile = conf.get_profile(user_id)
|
|
txt = _(
|
|
"XP: **{}**\nLevel: **{}**\nPrestige: **{}**\nVoicetime: **{}**\nMessages: **{}**\nStars: **{}**\n"
|
|
).format(
|
|
profile.xp,
|
|
profile.level,
|
|
profile.prestige,
|
|
utils.humanize_delta(int(profile.voice)),
|
|
profile.messages,
|
|
profile.stars,
|
|
)
|
|
await ctx.send(txt)
|
|
|
|
@commands.hybrid_command(name="profile", aliases=["pf"])
|
|
@commands.guild_only()
|
|
@commands.cooldown(3, 10, commands.BucketType.user)
|
|
async def profile(self, ctx: commands.Context, *, user: t.Optional[discord.Member] = None):
|
|
"""View User Profile"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if not conf.enabled:
|
|
txt = _("Leveling is disabled in this server!")
|
|
if await self.bot.is_admin(ctx.author):
|
|
txt += _("\nYou can enable it with `{}`").format(f"{ctx.clean_prefix}lset toggle")
|
|
return await ctx.send(txt)
|
|
|
|
if not user:
|
|
user = ctx.author
|
|
|
|
if user.bot and self.db.ignore_bots:
|
|
return await ctx.send(_("Bots cannot have profiles!"))
|
|
|
|
profile = conf.get_profile(user)
|
|
new_user_txt = None
|
|
if user.id == ctx.author.id and profile.show_tutorial:
|
|
# New user, tell them about how they can customize their profile
|
|
new_user_txt = _(
|
|
"Welcome to LevelUp!\n"
|
|
"Use {} to view your profile settings and the available customization commands!\n"
|
|
"*You can use {} to view your profile settings at any time*"
|
|
).format(f"`{ctx.clean_prefix}setprofile`", f"`{ctx.clean_prefix}setprofile view`")
|
|
profile.show_tutorial = False
|
|
self.save()
|
|
|
|
try:
|
|
if ctx.interaction is None:
|
|
async with ctx.typing():
|
|
result = await self.get_user_profile_cached(user)
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if isinstance(result, discord.Embed):
|
|
try:
|
|
await ctx.reply(content=new_user_txt, embed=result, mention_author=conf.notifymention)
|
|
except discord.HTTPException:
|
|
await ctx.send(content=new_user_txt, embed=result)
|
|
else: # File
|
|
try:
|
|
await ctx.reply(content=new_user_txt, file=result, mention_author=conf.notifymention)
|
|
except discord.HTTPException:
|
|
result.fp.seek(0)
|
|
await ctx.send(content=new_user_txt, file=result)
|
|
else:
|
|
await ctx.defer(ephemeral=True)
|
|
result = await self.get_user_profile_cached(user)
|
|
if isinstance(result, discord.Embed):
|
|
await ctx.send(content=new_user_txt, embed=result, ephemeral=True)
|
|
else: # File
|
|
await ctx.send(content=new_user_txt, file=result, ephemeral=True)
|
|
except Exception as e:
|
|
log.error("Error generating profile", exc_info=e)
|
|
if "Payload Too Large" in str(e):
|
|
txt = _("Your profile image is too large to send!")
|
|
else:
|
|
txt = _("An error occurred while generating your profile!\n{}").format(str(e))
|
|
await ctx.send(txt)
|
|
|
|
@commands.hybrid_command(name="prestige")
|
|
@commands.guild_only()
|
|
@commands.bot_has_permissions(manage_roles=True, embed_links=True)
|
|
async def prestige(self, ctx: commands.Context):
|
|
"""
|
|
Prestige your rank!
|
|
Once you have reached this servers prestige level requirement, you can
|
|
reset your level and experience to gain a prestige level and any perks associated with it
|
|
|
|
If you are over level and xp when you prestige, your xp and levels will carry over
|
|
"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if ctx.author.id not in conf.users:
|
|
return await ctx.send(_("You have no level data yet!"))
|
|
if not conf.prestigelevel or not conf.prestigedata:
|
|
return await ctx.send(_("Prestige has not been configured for this server!"))
|
|
profile = conf.get_profile(ctx.author)
|
|
if profile.level < conf.prestigelevel:
|
|
return await ctx.send(_("You need to be at least level {} to prestige!").format(conf.prestigelevel))
|
|
|
|
next_prestige = profile.prestige + 1
|
|
if next_prestige not in conf.prestigedata:
|
|
return await ctx.send(_("You have reached the maximum prestige level!"))
|
|
|
|
pdata = conf.prestigedata[next_prestige]
|
|
role = ctx.guild.get_role(pdata.role)
|
|
if not role:
|
|
return await ctx.send(_("The prestige role for this level no longer exists, please contact an admin!"))
|
|
|
|
current_xp = int(profile.xp)
|
|
xp_at_prestige = conf.algorithm.get_xp(conf.prestigelevel)
|
|
leftover_xp = current_xp - xp_at_prestige if current_xp > xp_at_prestige else 0
|
|
newlevel = conf.algorithm.get_level(leftover_xp)
|
|
|
|
profile.level = newlevel
|
|
profile.xp = leftover_xp
|
|
profile.prestige = next_prestige
|
|
self.save()
|
|
|
|
txt = _("You have reached Prestige {}!\n").format(f"**{next_prestige}**")
|
|
added, removed = await self.ensure_roles(ctx.author, conf, _("Reached prestige {}").format(next_prestige))
|
|
embed = discord.Embed(description=txt, color=await self.bot.get_embed_color(ctx))
|
|
if added:
|
|
added_roles = humanize_list([r.mention for r in added])
|
|
embed.add_field(name=_("Roles Added"), value=added_roles)
|
|
if removed:
|
|
removed_roles = humanize_list([r.mention for r in removed])
|
|
embed.add_field(name=_("Roles Removed"), value=removed_roles)
|
|
await ctx.send(embed=embed)
|
|
|
|
@commands.hybrid_group(name="setprofile", aliases=["myprofile", "mypf", "pfset"])
|
|
@commands.guild_only()
|
|
async def set_profile(self, ctx: commands.Context):
|
|
"""Customize your profile"""
|
|
|
|
@set_profile.command(name="view")
|
|
@commands.bot_has_permissions(embed_links=True)
|
|
async def view_profile_settings(self, ctx: commands.Context):
|
|
"""View your profile settings"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
profile = conf.get_profile(ctx.author)
|
|
if not conf.use_embeds:
|
|
desc = _(
|
|
"`Profile Style: `{}\n"
|
|
"`Show Nickname: `{}\n"
|
|
"`Blur: `{}\n"
|
|
"`Font: `{}\n"
|
|
"`Background: `{}\n"
|
|
).format(
|
|
profile.style.title(),
|
|
profile.show_displayname,
|
|
_("Enabled") if profile.blur else _("Disabled"),
|
|
str(profile.font).title(),
|
|
profile.background,
|
|
)
|
|
else:
|
|
desc = _("`Show Nickname: `{}\n").format(profile.show_displayname)
|
|
color = ctx.author.color
|
|
if color == discord.Color.default():
|
|
color = await self.bot.get_embed_color(ctx)
|
|
embed = discord.Embed(
|
|
title=_("Your Profile Settings"),
|
|
description=desc,
|
|
color=color,
|
|
)
|
|
bg = profile.background
|
|
file = None
|
|
if not conf.use_embeds:
|
|
if bg.startswith("http"):
|
|
embed.set_image(url=bg)
|
|
elif bg not in ("default", "random"):
|
|
available = list(self.backgrounds.iterdir()) + list(self.custom_backgrounds.iterdir())
|
|
for path in available:
|
|
if bg.lower() in path.name.lower():
|
|
embed.set_image(url=f"attachment://{path.name}")
|
|
file = discord.File(str(path), filename=path.name)
|
|
break
|
|
|
|
if self.db.cache_seconds:
|
|
embed.add_field(
|
|
name=_("Cache Time"),
|
|
value=_("Profiles are cached for {} seconds, this is configured by the bot owner.").format(
|
|
self.db.cache_seconds
|
|
),
|
|
)
|
|
|
|
embeds = [embed]
|
|
if not conf.use_embeds:
|
|
embeds += [
|
|
discord.Embed(
|
|
description=_("Name color: {}").format(f"**{str(profile.namecolor).title()}**"),
|
|
color=utils.string_to_rgb(profile.namecolor, as_discord_color=True),
|
|
),
|
|
discord.Embed(
|
|
description=_("Stat color: {}").format(f"**{str(profile.statcolor).title()}**"),
|
|
color=utils.string_to_rgb(profile.statcolor, as_discord_color=True),
|
|
),
|
|
discord.Embed(
|
|
description=_("Level bar color: {}").format(f"**{str(profile.barcolor).title()}**"),
|
|
color=utils.string_to_rgb(profile.barcolor, as_discord_color=True),
|
|
),
|
|
]
|
|
if ctx.channel.permissions_for(ctx.me).attach_files:
|
|
return await ctx.send(embeds=embeds, file=file, ephemeral=True)
|
|
await ctx.send(embeds=embeds, ephemeral=True)
|
|
|
|
@set_profile.command(name="bgpath")
|
|
@commands.is_owner()
|
|
async def get_bg_path(self, ctx: commands.Context):
|
|
"""Get the folder paths for this cog's backgrounds"""
|
|
txt = ""
|
|
txt += _("- Defaults: `{}`\n").format(self.backgrounds)
|
|
txt += _("- Custom: `{}`\n").format(self.custom_backgrounds)
|
|
await ctx.send(txt)
|
|
|
|
@set_profile.command(name="addbackground")
|
|
@commands.is_owner()
|
|
async def add_background(self, ctx: commands.Context, preferred_filename: str = None):
|
|
"""
|
|
Add a custom background to the cog from discord
|
|
|
|
**Arguments**
|
|
`preferred_filename` - If a name is given, it will be saved as this name instead of the filename
|
|
|
|
**DISCLAIMER**
|
|
- Do not replace any existing file names with custom images
|
|
- If you add broken or corrupt images it can break the cog
|
|
- Do not include the file extension in the preferred name, it will be added automatically
|
|
"""
|
|
content = utils.get_attachments(ctx)
|
|
if not content:
|
|
return await ctx.send(_("No images found in the message!"))
|
|
valid = [".png", ".jpg", ".jpeg", ".gif", ".webp"]
|
|
filename = content[0].filename
|
|
if not filename.endswith(tuple(valid)):
|
|
return await ctx.send(
|
|
_("That is not a valid format, must be on of the following extensions: ") + humanize_list(valid)
|
|
)
|
|
for ext in valid:
|
|
if ext in filename.lower():
|
|
break
|
|
else:
|
|
ext = ".png"
|
|
filebytes = await content[0].read()
|
|
if preferred_filename:
|
|
if Path(filename).suffix.lower() == Path(preferred_filename).suffix.lower():
|
|
# User already included the extension
|
|
filename = preferred_filename
|
|
else:
|
|
filename = preferred_filename + ext
|
|
path = self.custom_backgrounds / filename
|
|
path.write_bytes(filebytes)
|
|
await ctx.send(_("Your custom background has been saved as {}").format(f"`{filename}`"))
|
|
|
|
@set_profile.command(name="rembackground")
|
|
@commands.is_owner()
|
|
async def remove_background(self, ctx: commands.Context, *, filename: str):
|
|
"""Remove a default background from the cog's backgrounds folder"""
|
|
for path in self.custom_backgrounds.iterdir():
|
|
if filename.lower() in path.name.lower():
|
|
break
|
|
else:
|
|
return await ctx.send(_("No background found with that name!"))
|
|
msg = await ctx.send(_("Are you sure you want to delete {}?").format(f"`{path}`"))
|
|
yes = await utils.confirm_msg(ctx)
|
|
if not yes:
|
|
return await msg.edit(content=_("Cancelled"))
|
|
path.unlink()
|
|
await msg.edit(content=_("The background {} has been deleted").format(f"`{path}`"))
|
|
|
|
@set_profile.command(name="fontpath")
|
|
@commands.is_owner()
|
|
async def get_font_path(self, ctx: commands.Context):
|
|
"""Get folder paths for this cog's fonts"""
|
|
txt = ""
|
|
txt += _("- Defaults: {}\n").format(self.fonts)
|
|
txt += _("- Custom: {}\n").format(self.custom_fonts)
|
|
await ctx.send(txt)
|
|
|
|
@set_profile.command(name="addfont")
|
|
@commands.is_owner()
|
|
async def add_font(self, ctx: commands.Context, preferred_filename: str = None):
|
|
"""
|
|
Add a custom font to the cog from discord
|
|
|
|
**Arguments**
|
|
`preferred_filename` - If a name is given, it will be saved as this name instead of the filename
|
|
**Note:** do not include the file extension in the preferred name, it will be added automatically
|
|
"""
|
|
content = utils.get_attachments(ctx)
|
|
if not content:
|
|
return await ctx.send(_("No fonts found in the message!"))
|
|
valid = [".ttf", ".otf"]
|
|
filename = content[0].filename
|
|
if not filename.endswith(tuple(valid)):
|
|
return await ctx.send(
|
|
_("That is not a valid format, must be on of the following extensions: ") + humanize_list(valid)
|
|
)
|
|
for ext in valid:
|
|
if ext in filename.lower():
|
|
break
|
|
else:
|
|
ext = ".ttf"
|
|
filebytes = await content[0].read()
|
|
if preferred_filename:
|
|
if Path(filename).suffix.lower() == Path(preferred_filename).suffix.lower():
|
|
# User already included the extension
|
|
filename = preferred_filename
|
|
else:
|
|
filename = preferred_filename + ext
|
|
path = self.custom_fonts / filename
|
|
path.write_bytes(filebytes)
|
|
await ctx.send(_("Your custom font has been saved as {}").format(f"`{filename}`"))
|
|
|
|
@set_profile.command(name="remfont")
|
|
@commands.is_owner()
|
|
async def remove_font(self, ctx: commands.Context, *, filename: str):
|
|
"""Remove a default font from the cog's fonts folder"""
|
|
for path in self.custom_fonts.iterdir():
|
|
if filename.lower() in path.name.lower():
|
|
break
|
|
else:
|
|
return await ctx.send(_("No font found with that name!"))
|
|
msg = await ctx.send(_("Are you sure you want to delete {}?").format(f"`{path}`"))
|
|
yes = await utils.confirm_msg(ctx)
|
|
if not yes:
|
|
return await msg.edit(content=_("Cancelled"))
|
|
path.unlink()
|
|
await msg.edit(content=_("The font {} has been deleted").format(f"`{path}`"))
|
|
|
|
@set_profile.command(name="backgrounds")
|
|
@commands.cooldown(1, 5, commands.BucketType.user)
|
|
@commands.bot_has_permissions(attach_files=True)
|
|
async def view_all_backgrounds(self, ctx: commands.Context):
|
|
"""View the all available backgrounds"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if conf.use_embeds:
|
|
return await ctx.send(_("Image profiles are disabled here, this setting has no effect!"))
|
|
|
|
paths = list(self.backgrounds.iterdir()) + list(self.custom_backgrounds.iterdir())
|
|
paths = [str(x) for x in paths]
|
|
|
|
def _run() -> discord.File:
|
|
img = imgtools.format_backgrounds(paths)
|
|
buffer = BytesIO()
|
|
img.save(buffer, format="WEBP")
|
|
buffer.seek(0)
|
|
return discord.File(buffer, filename="backgrounds.webp")
|
|
|
|
async with ctx.typing():
|
|
file = await asyncio.to_thread(_run)
|
|
txt = _("Here are all the available backgrounds!\nYou can use {} to set your background").format(
|
|
f"`{ctx.clean_prefix}setprofile background <image name>`"
|
|
)
|
|
await ctx.send(txt, file=file)
|
|
|
|
@set_profile.command(name="fonts")
|
|
@commands.cooldown(1, 5, commands.BucketType.user)
|
|
@commands.bot_has_permissions(attach_files=True)
|
|
async def view_fonts(self, ctx: commands.Context):
|
|
"""View the available fonts you can use"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if conf.use_embeds:
|
|
return await ctx.send(_("Image profiles are disabled here, this setting has no effect!"))
|
|
paths = list(self.fonts.iterdir()) + list(self.custom_fonts.iterdir())
|
|
paths = [str(x) for x in paths]
|
|
|
|
def _run() -> discord.File:
|
|
img = imgtools.format_fonts(paths)
|
|
buffer = BytesIO()
|
|
img.save(buffer, format="WEBP")
|
|
buffer.seek(0)
|
|
return discord.File(buffer, filename="fonts.webp")
|
|
|
|
async with ctx.typing():
|
|
file = await asyncio.to_thread(_run)
|
|
txt = _("Here are all the available fonts!\nYou can use {} to set your font").format(
|
|
f"`{ctx.clean_prefix}setprofile font <font name>`"
|
|
)
|
|
await ctx.send(txt, file=file)
|
|
|
|
@set_profile.command(name="style")
|
|
async def toggle_profile_style(self, ctx: commands.Context, style: t.Literal["default", "runescape"]):
|
|
"""
|
|
Set your profile image style
|
|
|
|
- `default` is the default profile style, very customizable
|
|
- `runescape` is a runescape style profile, less customizable but more nostalgic
|
|
- (WIP) - more to come
|
|
|
|
"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if conf.use_embeds:
|
|
return await ctx.send(_("Image profiles are disabled here, this setting has no effect!"))
|
|
if style not in const.PROFILE_TYPES:
|
|
txt = _("That is not a valid profile style, please choose from the following: {}").format(
|
|
humanize_list(const.PROFILE_TYPES)
|
|
)
|
|
return await ctx.send(txt)
|
|
|
|
profile = conf.get_profile(ctx.author)
|
|
profile.style = style
|
|
self.save()
|
|
await ctx.send(_("Your profile type has been set to {}").format(style.capitalize()))
|
|
|
|
@set_profile.command(name="shownick")
|
|
async def toggle_show_nickname(self, ctx: commands.Context):
|
|
"""Toggle whether your nickname or username is shown in your profile"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
profile = conf.get_profile(ctx.author)
|
|
profile.show_displayname = not profile.show_displayname
|
|
self.save()
|
|
txt = (
|
|
_("Your nickname will now be shown in your profile!")
|
|
if profile.show_displayname
|
|
else _("Your username will now be shown in your profile!")
|
|
)
|
|
await ctx.send(txt)
|
|
|
|
@set_profile.command(name="namecolor", aliases=["name"])
|
|
@commands.bot_has_permissions(embed_links=True, attach_files=True)
|
|
@app_commands.describe(color="Name of color, hex or integer value")
|
|
async def set_name_color(self, ctx: commands.Context, *, color: str):
|
|
"""
|
|
Set a color for your username
|
|
|
|
For a specific color, try **[Google's hex color picker](https://htmlcolorcodes.com/)**
|
|
|
|
Set to `default` to randomize the color each time your profile is generated
|
|
"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if conf.use_embeds:
|
|
return await ctx.send(_("Image profiles are disabled here, this setting has no effect!"))
|
|
profile = conf.get_profile(ctx.author)
|
|
if profile.style in const.STATIC_FONT_STYLES:
|
|
return await ctx.send(_("You cannot change your name color with the current profile style!"))
|
|
if color == "default":
|
|
profile.namecolor = None
|
|
self.save()
|
|
return await ctx.send(_("Your name color has been set to random!"))
|
|
try:
|
|
rgb = utils.string_to_rgb(color)
|
|
except ValueError:
|
|
file = discord.File(imgtools.COLORTABLE)
|
|
return await ctx.send(
|
|
_("That is an invalid color, please use a valid name, integer, or hex color."), file=file
|
|
)
|
|
embed = discord.Embed(
|
|
description=_("Name color has been updated to {}!").format(f"`{color}`"),
|
|
color=discord.Color.from_rgb(*rgb),
|
|
)
|
|
profile.namecolor = color
|
|
self.save()
|
|
await ctx.send(embed=embed)
|
|
|
|
@set_profile.command(name="statcolor", aliases=["stat"])
|
|
@commands.bot_has_permissions(embed_links=True, attach_files=True)
|
|
async def set_stat_color(self, ctx: commands.Context, *, color: str):
|
|
"""
|
|
Set a color for your server stats
|
|
|
|
For a specific color, try **[Google's hex color picker](https://htmlcolorcodes.com/)**
|
|
|
|
Set to `default` to randomize the color each time your profile is generated
|
|
"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if conf.use_embeds:
|
|
return await ctx.send(_("Image profiles are disabled here, this setting has no effect!"))
|
|
profile = conf.get_profile(ctx.author)
|
|
if color == "default":
|
|
profile.statcolor = None
|
|
self.save()
|
|
return await ctx.send(_("Your stat color has been set to random!"))
|
|
try:
|
|
rgb = utils.string_to_rgb(color)
|
|
except ValueError:
|
|
file = discord.File(imgtools.COLORTABLE)
|
|
return await ctx.send(
|
|
_("That is an invalid color, please use a valid name, integer, or hex color."), file=file
|
|
)
|
|
embed = discord.Embed(
|
|
description=_("Stat color has been updated to {}!").format(f"`{color}`"),
|
|
color=discord.Color.from_rgb(*rgb),
|
|
)
|
|
profile.statcolor = color
|
|
self.save()
|
|
await ctx.send(embed=embed)
|
|
|
|
@set_profile.command(name="barcolor", aliases=["levelbar", "lvlbar", "bar"])
|
|
@commands.bot_has_permissions(embed_links=True, attach_files=True)
|
|
async def set_levelbar_color(self, ctx: commands.Context, *, color: str):
|
|
"""
|
|
Set a color for your level bar
|
|
|
|
For a specific color, try **[Google's hex color picker](https://htmlcolorcodes.com/)**
|
|
|
|
Set to `default` to randomize the color each time your profile is generated
|
|
"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if conf.use_embeds:
|
|
return await ctx.send(_("Image profiles are disabled here, this setting has no effect!"))
|
|
profile = conf.get_profile(ctx.author)
|
|
if profile.style in const.STATIC_FONT_STYLES:
|
|
return await ctx.send(_("You cannot change your name color with the current profile style!"))
|
|
if color == "default":
|
|
profile.barcolor = None
|
|
self.save()
|
|
return await ctx.send(_("Your level bar color has been set to random!"))
|
|
try:
|
|
rgb = utils.string_to_rgb(color)
|
|
except ValueError:
|
|
file = discord.File(imgtools.COLORTABLE)
|
|
return await ctx.send(
|
|
_("That is an invalid color, please use a valid name, integer, or hex color."), file=file
|
|
)
|
|
embed = discord.Embed(
|
|
description=_("Level bar color has been updated to {}!").format(f"`{color}`"),
|
|
color=discord.Color.from_rgb(*rgb),
|
|
)
|
|
profile.barcolor = color
|
|
self.save()
|
|
await ctx.send(embed=embed)
|
|
|
|
@set_levelbar_color.autocomplete("color")
|
|
@set_stat_color.autocomplete("color")
|
|
@set_name_color.autocomplete("color")
|
|
async def set_name_color_autocomplete(self, interaction: discord.Interaction, current: str) -> t.List[Choice]:
|
|
choices = await self.get_color_choices_cached(current)
|
|
return choices
|
|
|
|
@cached(ttl=120)
|
|
async def get_color_choices_cached(self, current: str) -> t.List[Choice]:
|
|
current = current.lower()
|
|
choices: t.List[Choice] = []
|
|
for color in const.COLORS:
|
|
if current in color.lower() or not current:
|
|
choices.append(Choice(name=color, value=color))
|
|
if len(choices) >= 25:
|
|
break
|
|
return choices
|
|
|
|
@set_profile.command(name="background", aliases=["bg"])
|
|
@commands.bot_has_permissions(embed_links=True)
|
|
async def set_user_background(
|
|
self,
|
|
ctx: commands.Context,
|
|
url: t.Optional[str] = None,
|
|
):
|
|
"""
|
|
Set a background for your profile
|
|
|
|
This will override your profile banner as the background
|
|
|
|
**WARNING**
|
|
The default profile style is wide (1050 by 450 pixels) with an aspect ratio of 21:9.
|
|
Portrait images will be cropped.
|
|
|
|
Tip: Googling "dual monitor backgrounds" gives good results for the right images
|
|
|
|
Here are some good places to look.
|
|
[dualmonitorbackgrounds](https://www.dualmonitorbackgrounds.com/)
|
|
[setaswall](https://www.setaswall.com/dual-monitor-wallpapers/)
|
|
[pexels](https://www.pexels.com/photo/panoramic-photography-of-trees-and-lake-358482/)
|
|
[teahub](https://www.teahub.io/searchw/dual-monitor/)
|
|
|
|
**Additional Options**
|
|
- Leave `url` blank or specify `default` to reset back to using your profile banner (or random if you don't have one)
|
|
- `random` will randomly select from a pool of default backgrounds each time
|
|
- `filename` run `[p]mypf backgrounds` to view default options you can use by including their filename
|
|
"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if conf.use_embeds:
|
|
txt = _("Image profiles are disabled here, this setting has no effect!")
|
|
return await ctx.send(txt)
|
|
profile = conf.get_profile(ctx.author)
|
|
if profile.style in const.STATIC_FONT_STYLES:
|
|
return await ctx.send(_("You cannot change your name color with the current profile style!"))
|
|
|
|
cached_txt = _("\n\nProfiles are cached for {} seconds so you may not see the change immediately").format(
|
|
self.db.cache_seconds
|
|
)
|
|
|
|
if url and url == "random":
|
|
profile.background = "random"
|
|
self.save()
|
|
txt = _("Your profile background has been set to random!")
|
|
if self.db.cache_seconds:
|
|
txt += cached_txt
|
|
return await ctx.send(txt)
|
|
if url and url == "default":
|
|
profile.background = "default"
|
|
self.save()
|
|
txt = _("Your profile background has been set to default!")
|
|
if self.db.cache_seconds:
|
|
txt += cached_txt
|
|
return await ctx.send(txt)
|
|
|
|
attachments = utils.get_attachments(ctx)
|
|
|
|
# If image url is given, run some checks
|
|
if not url and not attachments:
|
|
if profile.background == "default":
|
|
return await ctx.send(_("You must provide a url, filename, or attach a file"))
|
|
else:
|
|
profile.background = "default"
|
|
self.save()
|
|
txt = _("Your background has been reset to default!")
|
|
if self.db.cache_seconds:
|
|
txt += cached_txt
|
|
return await ctx.send(txt)
|
|
|
|
if url is None:
|
|
if attachments[0].size > ctx.guild.filesize_limit:
|
|
return await ctx.send(_("That image is too large for this server's upload limit!"))
|
|
profile.background = attachments[0].url
|
|
try:
|
|
file: discord.File = await self.get_user_profile(ctx.author, reraise=True)
|
|
if file.__sizeof__() > ctx.guild.filesize_limit:
|
|
profile.background = "default"
|
|
return await ctx.send(_("That image is too large for this server's upload limit!"))
|
|
except Exception as e:
|
|
profile.background = "default"
|
|
return await ctx.send(_("That image is not a valid profile background!\n{}").format(str(e)))
|
|
self.save()
|
|
txt = _("Your profile background has been set!")
|
|
if self.db.cache_seconds:
|
|
txt += cached_txt
|
|
return await ctx.send(txt)
|
|
|
|
if url.startswith("http"):
|
|
if self.tenor is None and await self.bot.is_owner(ctx.author):
|
|
txt = _(
|
|
"Set a Tenor API key to allow setting backgrounds from Discord's GIF links!\n"
|
|
"[Click here to get one](https://developers.google.com/tenor/guides/quickstart)\n"
|
|
"Then set it with `[p]set api tenor api_key <your_key>`"
|
|
)
|
|
await ctx.send(txt)
|
|
log.debug("Sanitizing link")
|
|
url = await sanitize_url(url, ctx)
|
|
profile.background = url
|
|
try:
|
|
file: discord.File = await self.get_user_profile(ctx.author, reraise=True)
|
|
if file.__sizeof__() > ctx.guild.filesize_limit:
|
|
profile.background = "default"
|
|
return await ctx.send(_("That image is too large for this server's upload limit!"))
|
|
except Exception as e:
|
|
profile.background = "default"
|
|
return await ctx.send(_("That image is not a valid profile background!\n{}").format(str(e)))
|
|
self.save()
|
|
txt = _("Your profile background has been set!")
|
|
if self.db.cache_seconds:
|
|
txt += cached_txt
|
|
return await ctx.send(txt)
|
|
|
|
# Check if the user provided a filename
|
|
backgrounds = list(self.backgrounds.iterdir()) + list(self.custom_backgrounds.iterdir())
|
|
for path in backgrounds:
|
|
if url.lower() in path.name.lower():
|
|
break
|
|
else:
|
|
return await ctx.send(_("No background found with that name!"))
|
|
file = discord.File(path)
|
|
profile.background = path.stem
|
|
self.save()
|
|
txt = _("Your profile background has been set to {}").format(f"`{path.name}`")
|
|
if self.db.cache_seconds:
|
|
txt += cached_txt
|
|
await ctx.send(txt, file=file)
|
|
|
|
@set_profile.command(name="font")
|
|
async def set_user_font(self, ctx: commands.Context, *, font_name: str):
|
|
"""
|
|
Set a font for your profile
|
|
|
|
To view available fonts, type `[p]myprofile fonts`
|
|
To revert to the default font, use `default` for the `font_name` argument
|
|
"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if conf.use_embeds:
|
|
txt = _("Image profiles are disabled here, this setting has no effect!")
|
|
return await ctx.send(txt)
|
|
profile = conf.get_profile(ctx.author)
|
|
if profile.style in const.STATIC_FONT_STYLES:
|
|
return await ctx.send(_("You cannot change your name color with the current profile style!"))
|
|
|
|
if font_name == "default":
|
|
profile.font = "default"
|
|
self.save()
|
|
return await ctx.send(_("Your font has been reset to default!"))
|
|
fonts = list(self.fonts.iterdir()) + list(self.custom_fonts.iterdir())
|
|
for path in fonts:
|
|
if font_name.lower() in path.name.lower():
|
|
break
|
|
else:
|
|
return await ctx.send(_("No font found with that name!"))
|
|
profile.font = path.name
|
|
self.save()
|
|
txt = _("Your font has been set to {}").format(f"`{path.name}`")
|
|
await ctx.send(txt)
|
|
|
|
@set_user_font.autocomplete("font_name")
|
|
async def set_user_font_autocomplete(self, interaction: discord.Interaction, current: str) -> t.List[Choice]:
|
|
choices = []
|
|
current = current.lower()
|
|
for path in list(self.fonts.iterdir()) + list(self.custom_fonts.iterdir()):
|
|
if current in path.name.lower() or not current:
|
|
choices.append(Choice(name=path.stem, value=path.stem))
|
|
if len(choices) >= 25:
|
|
break
|
|
return choices
|
|
|
|
@set_profile.command(name="blur")
|
|
async def set_user_blur(self, ctx: commands.Context):
|
|
"""
|
|
Toggle a slight blur effect on the background image where the text is displayed.
|
|
"""
|
|
conf = self.db.get_conf(ctx.guild)
|
|
if conf.use_embeds:
|
|
txt = _("Image profiles are disabled here, this setting has no effect!")
|
|
return await ctx.send(txt)
|
|
profile = conf.get_profile(ctx.author)
|
|
if profile.style in const.STATIC_FONT_STYLES:
|
|
return await ctx.send(_("You cannot change your name color with the current profile style!"))
|
|
profile.blur = not profile.blur
|
|
self.save()
|
|
txt = _("Your profile blur has been set to {}").format(_("Enabled") if profile.blur else _("Disabled"))
|
|
await ctx.send(txt)
|