diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cc353a6..c194e1c 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ jobs: - uses: actions/checkout@v3 - uses: psf/black@stable with: - options: "--check --line-length=100 --verbose" + options: "--check --line-length=88 --verbose" ruff: runs-on: ubuntu-latest steps: diff --git a/.github/workflows/reuse-complicance.yml b/.github/workflows/reuse-complicance.yml new file mode 100644 index 0000000..be53cb3 --- /dev/null +++ b/.github/workflows/reuse-complicance.yml @@ -0,0 +1,10 @@ +name: REUSE Compliance Check +on: [ push, pull_request ] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: REUSE Compliance Check + uses: fsfe/reuse-action@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 163a097..e0c7dbb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ repos: - repo: https://github.com/ambv/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black - args: [--line-length=100] + args: [--line-length=88] language_version: python3 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.270 + rev: v0.0.280 hooks: - id: ruff args: [--line-length=100, --ignore=E501] @@ -17,3 +17,8 @@ repos: hooks: - id: isort args: ["--profile", "black", "--filter-files"] + + - repo: https://github.com/fsfe/reuse-tool + rev: v2.1.0 + hooks: + - id: reuse diff --git a/cogs/configuration.py b/cogs/configuration.py index 3e755ce..64c4b7f 100644 --- a/cogs/configuration.py +++ b/cogs/configuration.py @@ -6,9 +6,9 @@ import discord from discord.ext import commands +from modules.misobot import MisoBot from modules import emoji_literals, exceptions, queries, util -from modules.misobot import MisoBot class ChannelSetting(commands.TextChannelConverter): @@ -85,7 +85,9 @@ async def greeter_toggle(self, ctx: commands.Context, value: bool): await util.send_success(ctx, "Greeter is now **disabled**") @greeter.command(name="channel") - async def greeter_channel(self, ctx: commands.Context, *, channel: discord.TextChannel): + async def greeter_channel( + self, ctx: commands.Context, *, channel: discord.TextChannel + ): """Set the greeter channel""" await queries.update_setting(ctx, "greeter_settings", "channel_id", channel.id) await util.send_success(ctx, f"Greeter channel is now {channel.mention}") @@ -96,6 +98,13 @@ async def greeter_message(self, ctx: commands.Context, *, message): Change the greeter welcome message format Use with "default" to reset to the default format. + + Use placeholders within {curly braces}: + + available placeholders: `mention`, `user`, `id`, `server`, `guild`, `username` + + Example: + >greeter message "{user} just joined {server}!" """ if message.lower() == "default": message = None @@ -125,10 +134,14 @@ async def goodbye_toggle(self, ctx: commands.Context, value: bool): await util.send_success(ctx, "Goodbye messages are now **disabled**") @goodbyemessage.command(name="channel") - async def goodbye_channel(self, ctx: commands.Context, *, channel: discord.TextChannel): + async def goodbye_channel( + self, ctx: commands.Context, *, channel: discord.TextChannel + ): """Set the goodbye message channel""" await queries.update_setting(ctx, "goodbye_settings", "channel_id", channel.id) - await util.send_success(ctx, f"Goodbye messages channel is now {channel.mention}") + await util.send_success( + ctx, f"Goodbye messages channel is now {channel.mention}" + ) @goodbyemessage.command(name="message", usage="") async def goodbye_message(self, ctx: commands.Context, *, message): @@ -136,6 +149,13 @@ async def goodbye_message(self, ctx: commands.Context, *, message): Change the goodbye message format Use with "default" to reset to the default format. + + Use placeholders within {curly braces}: + + available placeholders: `mention`, `user`, `id`, `server`, `guild`, `username` + + Example: + >goodbyemessage message "{user} just left {server}!" """ if message.lower() == "default": message = None @@ -157,7 +177,10 @@ async def logger(self, ctx: commands.Context): @logger.command(name="members", usage="") async def logger_members( - self, ctx: commands.Context, *, channel: Annotated[discord.TextChannel, ChannelSetting] + self, + ctx: commands.Context, + *, + channel: Annotated[discord.TextChannel, ChannelSetting], ): """ Set channel for the membership log @@ -174,11 +197,16 @@ async def logger_members( if channel is None: await util.send_success(ctx, "Members logging **disabled**") else: - await util.send_success(ctx, f"Member changes will now be logged to {channel.mention}") + await util.send_success( + ctx, f"Member changes will now be logged to {channel.mention}" + ) @logger.command(name="bans", usage="") async def logger_bans( - self, ctx: commands.Context, *, channel: Annotated[discord.TextChannel, ChannelSetting] + self, + ctx: commands.Context, + *, + channel: Annotated[discord.TextChannel, ChannelSetting], ): """ Set channel where bans are logged @@ -195,7 +223,9 @@ async def logger_bans( if channel is None: await util.send_success(ctx, "Bans logging **disabled**") else: - await util.send_success(ctx, f"Bans will now be logged to {channel.mention}") + await util.send_success( + ctx, f"Bans will now be logged to {channel.mention}" + ) @logger.group(name="deleted") async def logger_deleted(self, ctx: commands.Context): @@ -204,7 +234,10 @@ async def logger_deleted(self, ctx: commands.Context): @logger_deleted.command(name="channel", usage="") async def deleted_channel( - self, ctx: commands.Context, *, channel: Annotated[discord.TextChannel, ChannelSetting] + self, + ctx: commands.Context, + *, + channel: Annotated[discord.TextChannel, ChannelSetting], ): """ Set channel for message log @@ -226,7 +259,9 @@ async def deleted_channel( ) @logger_deleted.command(name="ignore") - async def deleted_ignore(self, ctx: commands.Context, *, channel: discord.TextChannel): + async def deleted_ignore( + self, ctx: commands.Context, *, channel: discord.TextChannel + ): """Ignore a channel from being logged in message log""" if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") @@ -236,10 +271,14 @@ async def deleted_ignore(self, ctx: commands.Context, *, channel: discord.TextCh ctx.guild.id, channel.id, ) - await util.send_success(ctx, f"No longer logging any messages deleted in {channel.mention}") + await util.send_success( + ctx, f"No longer logging any messages deleted in {channel.mention}" + ) @logger_deleted.command(name="unignore") - async def deleted_unignore(self, ctx: commands.Context, *, channel: discord.TextChannel): + async def deleted_unignore( + self, ctx: commands.Context, *, channel: discord.TextChannel + ): """Unignore a channel from being logged in message log""" if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") @@ -262,9 +301,13 @@ async def starboard(self, ctx: commands.Context): await util.command_group_help(ctx) @starboard.command(name="channel") - async def starboard_channel(self, ctx: commands.Context, channel: discord.TextChannel): + async def starboard_channel( + self, ctx: commands.Context, channel: discord.TextChannel + ): """Set the starboard channel""" - await queries.update_setting(ctx, "starboard_settings", "channel_id", channel.id) + await queries.update_setting( + ctx, "starboard_settings", "channel_id", channel.id + ) await util.send_success(ctx, f"Starboard channel is now {channel.mention}") await self.bot.cache.cache_starboard_settings() @@ -274,7 +317,9 @@ async def starboard_amount(self, ctx: commands.Context, amount: int): if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") - await queries.update_setting(ctx, "starboard_settings", "reaction_count", amount) + await queries.update_setting( + ctx, "starboard_settings", "reaction_count", amount + ) emoji_name, emoji_id, emoji_type = await self.bot.db.fetch_row( """ SELECT emoji_name, emoji_id, emoji_type @@ -309,7 +354,8 @@ async def starboard_emoji(self, ctx: commands.Context, emoji): # is custom emoji if not await queries.is_donator(ctx, ctx.author, 2): raise exceptions.CommandInfo( - "You have to be a [donator](https://misobot.xyz/donate) to use custom emojis with the starboard!" + "You have to be a [donator](https://misobot.xyz/donate) " + "to use custom emojis with the starboard!" ) emoji_obj = await util.get_emoji(ctx, emoji) if emoji_obj is None: @@ -357,19 +403,29 @@ async def starboard_emoji(self, ctx: commands.Context, emoji): @starboard.command(name="log", usage="") async def starboard_log( - self, ctx: commands.Context, channel: Annotated[discord.TextChannel, ChannelSetting] + self, + ctx: commands.Context, + channel: Annotated[discord.TextChannel, ChannelSetting], ): """Set starboard logging channel to log starring events""" if channel is None: - await queries.update_setting(ctx, "starboard_settings", "log_channel_id", None) + await queries.update_setting( + ctx, "starboard_settings", "log_channel_id", None + ) await util.send_success(ctx, "Starboard log is now disabled") else: - await queries.update_setting(ctx, "starboard_settings", "log_channel_id", channel.id) - await util.send_success(ctx, f"Starboard log channel is now {channel.mention}") + await queries.update_setting( + ctx, "starboard_settings", "log_channel_id", channel.id + ) + await util.send_success( + ctx, f"Starboard log channel is now {channel.mention}" + ) await self.bot.cache.cache_starboard_settings() @starboard.command(name="blacklist") - async def starboard_blacklist(self, ctx: commands.Context, channel: discord.TextChannel): + async def starboard_blacklist( + self, ctx: commands.Context, channel: discord.TextChannel + ): """Blacklist a channel from being counted for starboard""" if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") @@ -384,11 +440,15 @@ async def starboard_blacklist(self, ctx: commands.Context, channel: discord.Text ctx.guild.id, channel.id, ) - await util.send_success(ctx, f"Stars are no longer counted in {channel.mention}") + await util.send_success( + ctx, f"Stars are no longer counted in {channel.mention}" + ) await self.bot.cache.cache_starboard_settings() @starboard.command(name="unblacklist") - async def starboard_unblacklist(self, ctx: commands.Context, channel: discord.TextChannel): + async def starboard_unblacklist( + self, ctx: commands.Context, channel: discord.TextChannel + ): """Unblacklist a channel from being counted for starboard""" if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") @@ -400,7 +460,9 @@ async def starboard_unblacklist(self, ctx: commands.Context, channel: discord.Te ctx.guild.id, channel.id, ) - await util.send_success(ctx, f"Stars are now again counted in {channel.mention}") + await util.send_success( + ctx, f"Stars are now again counted in {channel.mention}" + ) await self.bot.cache.cache_starboard_settings() @starboard.command(name="current") @@ -411,7 +473,9 @@ async def starboard_current(self, ctx: commands.Context): starboard_settings = self.bot.cache.starboard_settings.get(str(ctx.guild.id)) if not starboard_settings: - raise exceptions.CommandWarning("Nothing has been configured on this server yet!") + raise exceptions.CommandWarning( + "Nothing has been configured on this server yet!" + ) ( is_enabled, @@ -431,7 +495,9 @@ async def starboard_current(self, ctx: commands.Context): ctx.guild.id, ) - content = discord.Embed(title=":star: Current starboard settings", color=int("ffac33", 16)) + content = discord.Embed( + title=":star: Current starboard settings", color=int("ffac33", 16) + ) content.add_field( name="State", value=":white_check_mark: Enabled" if is_enabled else ":x: Disabled", @@ -461,7 +527,9 @@ async def starboard_current(self, ctx: commands.Context): async def muterole(self, ctx: commands.Context, *, role: discord.Role): """Set the role given when muting people using the mute command""" await queries.update_setting(ctx, "guild_settings", "mute_role_id", role.id) - await util.send_success(ctx, f"Muting someone now gives them the role {role.mention}") + await util.send_success( + ctx, f"Muting someone now gives them the role {role.mention}" + ) @commands.group() @commands.guild_only() @@ -481,7 +549,9 @@ async def autorole_add(self, ctx: commands.Context, *, role: discord.Role): ctx.guild.id, role.id, ) - await util.send_success(ctx, f"New members will now automatically get {role.mention}") + await util.send_success( + ctx, f"New members will now automatically get {role.mention}" + ) @autorole.command(name="remove") async def autorole_remove(self, ctx: commands.Context, *, role): @@ -542,11 +612,17 @@ async def blacklist(self, ctx: commands.Context): @commands.has_permissions(manage_guild=True) async def blacklist_delete(self, ctx: commands.Context, value: bool): """Toggle whether to delete the message on blacklist trigger""" - await queries.update_setting(ctx, "guild_settings", "delete_blacklisted_usage", value) + await queries.update_setting( + ctx, "guild_settings", "delete_blacklisted_usage", value + ) if value: - await util.send_success(ctx, "Now deleting messages that trigger any blacklists.") + await util.send_success( + ctx, "Now deleting messages that trigger any blacklists." + ) else: - await util.send_success(ctx, "No longer deleting messages that trigger blacklists.") + await util.send_success( + ctx, "No longer deleting messages that trigger blacklists." + ) @blacklist.command(name="show") @commands.has_permissions(manage_guild=True) @@ -624,7 +700,9 @@ async def blacklist_channel(self, ctx: commands.Context, *channels): fails = [] for channel_arg in channels: try: - channel = await commands.TextChannelConverter().convert(ctx, channel_arg) + channel = await commands.TextChannelConverter().convert( + ctx, channel_arg + ) except commands.errors.BadArgument: fails.append(f"Cannot find channel {channel_arg}") else: @@ -692,7 +770,9 @@ async def blacklist_command(self, ctx: commands.Context, *, command): cmd = self.bot.get_command(command) if cmd is None: - raise exceptions.CommandWarning(f"Command `{ctx.prefix}{command}` not found.") + raise exceptions.CommandWarning( + f"Command `{ctx.prefix}{command}` not found." + ) await self.bot.db.execute( "INSERT IGNORE blacklisted_command VALUES (%s, %s)", @@ -700,7 +780,9 @@ async def blacklist_command(self, ctx: commands.Context, *, command): ctx.guild.id, ) try: - self.bot.cache.blacklist[str(ctx.guild.id)]["command"].add(cmd.qualified_name.lower()) + self.bot.cache.blacklist[str(ctx.guild.id)]["command"].add( + cmd.qualified_name.lower() + ) except KeyError: self.bot.cache.blacklist[str(ctx.guild.id)] = { "member": set(), @@ -712,9 +794,13 @@ async def blacklist_command(self, ctx: commands.Context, *, command): @blacklist.command(name="global", hidden=True) @commands.is_owner() - async def blacklist_global(self, ctx: commands.Context, user: discord.User, *, reason): + async def blacklist_global( + self, ctx: commands.Context, user: discord.User, *, reason + ): """Blacklist someone globally from Miso Bot""" - await self.bot.db.execute("INSERT IGNORE blacklisted_user VALUES (%s, %s)", user.id, reason) + await self.bot.db.execute( + "INSERT IGNORE blacklisted_user VALUES (%s, %s)", user.id, reason + ) self.bot.cache.blacklist["global"]["user"].add(user.id) await util.send_success(ctx, f"**{user}** can no longer use Miso Bot!") @@ -749,7 +835,9 @@ async def unblacklist_channel(self, ctx: commands.Context, *channels): fails = [] for channel_arg in channels: try: - channel = await commands.TextChannelConverter().convert(ctx, channel_arg) + channel = await commands.TextChannelConverter().convert( + ctx, channel_arg + ) except commands.errors.BadArgument: fails.append(f"Cannot find channel {channel_arg}") else: @@ -797,7 +885,9 @@ async def unblacklist_command(self, ctx: commands.Context, *, command): cmd = self.bot.get_command(command) if cmd is None: - raise exceptions.CommandWarning(f"Command `{ctx.prefix}{command}` not found.") + raise exceptions.CommandWarning( + f"Command `{ctx.prefix}{command}` not found." + ) await self.bot.db.execute( """ @@ -806,7 +896,9 @@ async def unblacklist_command(self, ctx: commands.Context, *, command): ctx.guild.id, cmd.qualified_name, ) - self.bot.cache.blacklist[str(ctx.guild.id)]["command"].discard(cmd.qualified_name.lower()) + self.bot.cache.blacklist[str(ctx.guild.id)]["command"].discard( + cmd.qualified_name.lower() + ) await util.send_success(ctx, f"`{ctx.prefix}{cmd}` is no longer blacklisted.") @unblacklist.command(name="global", hidden=True) @@ -833,7 +925,9 @@ async def unblacklist_guild(self, ctx: commands.Context, guild_id: int): guild_id, ) self.bot.cache.blacklist["global"]["guild"].discard(guild_id) - await util.send_success(ctx, f"Guild with id `{guild_id}` can use Miso Bot again!") + await util.send_success( + ctx, f"Guild with id `{guild_id}` can use Miso Bot again!" + ) async def setup(bot): diff --git a/cogs/customcommands.py b/cogs/customcommands.py index ae33afb..c480055 100644 --- a/cogs/customcommands.py +++ b/cogs/customcommands.py @@ -3,15 +3,16 @@ # https://git.joinemm.dev/miso-bot import asyncio +import io import json import arrow import discord from discord.ext import commands from loguru import logger +from modules.misobot import MisoBot from modules import emojis, exceptions, queries, util -from modules.misobot import MisoBot class CustomCommands(commands.Cog, name="Commands"): @@ -57,7 +58,9 @@ async def custom_command_list(self, guild_id, match=""): "SELECT command_trigger FROM custom_command WHERE guild_id = %s", guild_id ) return { - command_trigger for command_trigger in data if match == "" or match in command_trigger + command_trigger + for command_trigger in data + if match == "" or match in command_trigger } @commands.Cog.listener() @@ -106,14 +109,19 @@ async def add(self, ctx: commands.Context, name, *, response): if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") - if not ctx.author.guild_permissions.manage_guild and await self.bot.db.fetch_value( - "SELECT restrict_custom_commands FROM guild_settings WHERE guild_id = %s", - ctx.guild.id, + if ( + not ctx.author.guild_permissions.manage_guild + and await self.bot.db.fetch_value( + "SELECT restrict_custom_commands FROM guild_settings WHERE guild_id = %s", + ctx.guild.id, + ) ): raise commands.MissingPermissions(["manage_server"]) if name in self.bot_command_list(): - raise exceptions.CommandWarning(f"`{ctx.prefix}{name}` is already a built in command!") + raise exceptions.CommandWarning( + f"`{ctx.prefix}{name}` is already a built in command!" + ) if await self.bot.db.fetch_value( "SELECT content FROM custom_command WHERE guild_id = %s AND command_trigger = %s", ctx.guild.id, @@ -152,7 +160,9 @@ async def command_remove(self, ctx: commands.Context, name): ctx.guild.id, ) if not owner_id: - raise exceptions.CommandWarning(f"Custom command `{ctx.prefix}{name}` does not exist") + raise exceptions.CommandWarning( + f"Custom command `{ctx.prefix}{name}` does not exist" + ) owner = ctx.guild.get_member(owner_id) if ( @@ -161,7 +171,10 @@ async def command_remove(self, ctx: commands.Context, name): and not ctx.author.guild_permissions.manage_guild ): raise exceptions.CommandWarning( - f"`{ctx.prefix}{name}` can only be removed by **{owner}** unless you have `manage_server` permission." + ( + f"`{ctx.prefix}{name}` can only be removed by **{owner}** " + "unless you have `manage_server` permission." + ) ) await self.bot.db.execute( @@ -169,7 +182,9 @@ async def command_remove(self, ctx: commands.Context, name): ctx.guild.id, name, ) - await util.send_success(ctx, f"Custom command `{ctx.prefix}{name}` has been deleted") + await util.send_success( + ctx, f"Custom command `{ctx.prefix}{name}` has been deleted" + ) @command.command(name="search") async def command_search(self, ctx: commands.Context, name): @@ -203,25 +218,44 @@ async def command_list(self, ctx: commands.Context): raise exceptions.CommandError("Unable to get current guild") rows = [ - f"{ctx.prefix}{command}" for command in await self.custom_command_list(ctx.guild.id) + f"{ctx.prefix}{command}" + for command in await self.custom_command_list(ctx.guild.id) ] if rows: content = discord.Embed(title=f"{ctx.guild.name} custom commands") await util.send_as_pages(ctx, content, rows) else: - raise exceptions.CommandInfo("No custom commands have been added on this server yet") + raise exceptions.CommandInfo( + "No custom commands have been added on this server yet" + ) - @commands.is_owner() @command.command(name="import") + @commands.has_permissions(manage_guild=True) async def command_import(self, ctx: commands.Context): - """Attach a json file in format {command: xxx, text: xxx}""" + """Attach a json file with commands you want to import + + The syntax for the json file: + ``` + [ + { + "command": string, + "text": string, + "owner": user id (int, optional), + "added_on": UNIX timestamp (int, optional) + } + ], [...] + ``` + """ + if not ctx.message.attachments: + raise exceptions.CommandWarning( + "Please attach a `.json` file to the message" + ) + jsonfile = ctx.message.attachments[0] imported = json.loads(await jsonfile.read()) tasks = [] for command in imported: - name = command["command"] - text = command["text"] - tasks.append(self.import_command(ctx, name, text)) + tasks.append(self.import_command(ctx, command)) load = await ctx.send(emojis.LOADING) results = await asyncio.gather(*tasks) @@ -232,9 +266,49 @@ async def command_import(self, ctx: commands.Context): failed_operations=[r[1] for r in filter(lambda x: not x[0], results)], ) - async def import_command(self, ctx: commands.Context, name, text): + @command.command(name="export") + @commands.has_permissions(manage_guild=True) + async def command_export(self, ctx: commands.Context): + """Exports all custom commands in json format""" if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") + data = await self.bot.db.fetch( + """ + SELECT command_trigger, content, added_by, added_on + FROM custom_command WHERE guild_id = %s + """, + ctx.guild.id, + ) + if not data: + raise exceptions.CommandInfo( + "No custom commands have been added on this server yet" + ) + + jsondata = [ + { + "command": trigger, + "text": content, + "owner": added_by, + "timestamp": int(added_on.timestamp()), + } + for (trigger, content, added_by, added_on) in data + ] + buffer = json.dumps(jsondata, indent=4).encode("utf-8") + await ctx.send( + file=discord.File( + fp=io.BytesIO(buffer), + filename=f"{ctx.guild} commands.json", + ) + ) + + async def import_command(self, ctx: commands.Context, command: dict): + if ctx.guild is None: + raise exceptions.CommandError("Unable to get current guild") + + name = command["command"] + text = command["text"] + owner_id = command.get("owner", ctx.author.id) + added_on = command.get("added_on", arrow.utcnow().int_timestamp) if name in self.bot_command_list(): return False, f"`{ctx.prefix}{name}` is already a built in command!" @@ -243,15 +317,18 @@ async def import_command(self, ctx: commands.Context, name, text): ctx.guild.id, name, ): - return False, f"Custom command `{ctx.prefix}{name}` already exists on this server!" + return ( + False, + f"Custom command `{ctx.prefix}{name}` already exists on this server!", + ) await self.bot.db.execute( "INSERT INTO custom_command VALUES(%s, %s, %s, %s, %s)", ctx.guild.id, name, text, - arrow.utcnow().datetime, - ctx.author.id, + arrow.get(added_on).datetime, + owner_id, ) return True, name @@ -259,7 +336,9 @@ async def import_command(self, ctx: commands.Context, name, text): @commands.has_permissions(manage_guild=True) async def command_restrict(self, ctx: commands.Context, value: bool): """Restrict command management to only people with manage_server permission""" - await queries.update_setting(ctx, "guild_settings", "restrict_custom_commands", value) + await queries.update_setting( + ctx, "guild_settings", "restrict_custom_commands", value + ) if value: await util.send_success( ctx, "Adding custom commands is now restricted to server managers." @@ -289,8 +368,13 @@ async def command_clear(self, ctx: commands.Context): if count < 1: raise exceptions.CommandWarning("This server has no custom commands yet!") - content = discord.Embed(title=":warning: Are you sure?", color=int("ffcc4d", 16)) - content.description = f"This action will delete all **{count}** custom commands on this server and is **irreversible**." + content = discord.Embed( + title=":warning: Are you sure?", color=int("ffcc4d", 16) + ) + content.description = ( + f"This action will delete all **{count}** custom commands on " + "this server and is **irreversible**." + ) msg = await ctx.send(embed=content) async def confirm(): @@ -311,7 +395,9 @@ async def cancel(): functions = {"✅": confirm, "❌": cancel} asyncio.ensure_future( - util.reaction_buttons(ctx, msg, functions, only_author=True, single_use=True) + util.reaction_buttons( + ctx, msg, functions, only_author=True, single_use=True + ) ) diff --git a/cogs/errorhandler.py b/cogs/errorhandler.py index d43a094..8687d66 100644 --- a/cogs/errorhandler.py +++ b/cogs/errorhandler.py @@ -8,35 +8,46 @@ import discord from discord.ext import commands from loguru import logger - -from modules import emojis, exceptions, queries, util from modules.instagram import InstagramError from modules.misobot import MisoBot from modules.tiktok import TiktokError +from modules import emojis, exceptions, queries, util + @dataclass class ErrorMessages: - disabled_command = "This command is temporarily disabled, sorry for the inconvenience!" + disabled_command = ( + "This command is temporarily disabled, sorry for the inconvenience!" + ) no_private_message = "This command cannot be used in a DM!" missing_permissions = "You require {0} permission to use this command!" - bot_missing_permissions = "Unable execute command due to missing permissions! (I need {0})" + bot_missing_permissions = ( + "Unable execute command due to missing permissions! (I need {0})" + ) not_donator = "This command is exclusive to Miso Bot donators! Consider donating to get access" - server_too_big = "This command cannot be used in large servers for performance reasons!" + server_too_big = ( + "This command cannot be used in large servers for performance reasons!" + ) not_allowed = "You cannot use this command." max_concurrency = "Stop spamming! >:(" command_on_cooldown = "You are on cooldown! Please wait `{0:.0f} seconds.`" -class ErrorHander(commands.Cog): +class ErrorHandler(commands.Cog): """Any errors during command invocation will propagate here""" def __init__(self, bot): self.bot: MisoBot = bot @staticmethod - def log_format(ctx: commands.Context, error: Exception | None, message: str | None = None): - return f"{ctx.guild} @ {ctx.author} : {ctx.message.content} => {type(error).__name__}: {message or str(error)}" + def log_format( + ctx: commands.Context, error: Exception | None, message: str | None = None + ): + return ( + f"{ctx.guild} @ {ctx.author} : {ctx.message.content} " + f"=> {type(error).__name__}: {message or str(error)}" + ) async def reinvoke_command(self, ctx: commands.Context): try: @@ -65,16 +76,26 @@ async def send_embed( **kwargs, ) except discord.Forbidden: - logger.warning(f"403 Forbidden when trying to send error message : {message}") + logger.warning( + f"403 Forbidden when trying to send error message : {message}" + ) async def send_info( - self, ctx: commands.Context, message: str, error: Exception | None = None, **kwargs + self, + ctx: commands.Context, + message: str, + error: Exception | None = None, + **kwargs, ): logger.info(self.log_format(ctx, error, message)) await self.send_embed(ctx, message, ":information_source:", "3b88c3", **kwargs) async def send_warning( - self, ctx: commands.Context, message: str, error: Exception | None = None, **kwargs + self, + ctx: commands.Context, + message: str, + error: Exception | None = None, + **kwargs, ): logger.warning(self.log_format(ctx, error, message)) await self.send_embed(ctx, message, ":warning:", "ffcc4d", **kwargs) @@ -88,15 +109,23 @@ async def send_error( **kwargs, ): logger.error(self.log_format(ctx, error, message)) - await self.send_embed(ctx, f"```{language}\n{message}```", color="be1931", **kwargs) + await self.send_embed( + ctx, f"```{language}\n{message}```", color="be1931", **kwargs + ) - async def send_lastfm_error(self, ctx: commands.Context, error: exceptions.LastFMError): + async def send_lastfm_error( + self, ctx: commands.Context, error: exceptions.LastFMError + ): match error.error_code: case 8: - message = "There was a problem connecting to LastFM servers. LastFM might be down. Try again later." + message = ( + "There was a problem connecting to LastFM servers. " + "LastFM might be down. Try again later." + ) case 17: message = ( - "Unable to get listening information. Please check you LastFM privacy settings." + "Unable to get listening information. " + "Please check you LastFM privacy settings." ) case 29: message = "LastFM rate limit exceeded. Please try again later." @@ -105,7 +134,9 @@ async def send_lastfm_error(self, ctx: commands.Context, error: exceptions.LastF await self.send_embed(ctx, message, emojis.LASTFM, "b90000") - async def handle_blacklist(self, ctx: commands.Context, error: exceptions.Blacklist): + async def handle_blacklist( + self, ctx: commands.Context, error: exceptions.Blacklist + ): if ctx.author.id == ctx.bot.owner_id or ( isinstance( error, @@ -138,7 +169,9 @@ async def handle_blacklist(self, ctx: commands.Context, error: exceptions.Blackl await asyncio.sleep(5) await ctx.message.delete() - async def handle_cooldown(self, ctx: commands.Context, error: commands.CommandOnCooldown): + async def handle_cooldown( + self, ctx: commands.Context, error: commands.CommandOnCooldown + ): if ( ctx.author.id == ctx.bot.owner_id or await queries.is_donator(ctx, ctx.author, 2) @@ -154,7 +187,9 @@ async def handle_cooldown(self, ctx: commands.Context, error: commands.CommandOn ) @commands.Cog.listener() - async def on_command_error(self, ctx: commands.Context, error_wrapper: commands.CommandError): + async def on_command_error( + self, ctx: commands.Context, error_wrapper: commands.CommandError + ): """The event triggered when an error is raised while invoking a command""" # extract the original error from the CommandError wrapper @@ -248,9 +283,11 @@ async def on_command_error(self, ctx: commands.Context, error_wrapper: commands. await self.send_warning(ctx, error.message) case _: - await self.send_error(ctx, f"{type(error).__name__}: {error}", error, language="ex") + await self.send_error( + ctx, f"{type(error).__name__}: {error}", error, language="ex" + ) logger.opt(exception=error).error("Unhandled exception traceback:") async def setup(bot): - await bot.add_cog(ErrorHander(bot)) + await bot.add_cog(ErrorHandler(bot)) diff --git a/cogs/events.py b/cogs/events.py index 402bd9b..bf6c31c 100644 --- a/cogs/events.py +++ b/cogs/events.py @@ -9,11 +9,11 @@ import discord from discord.ext import commands, tasks from loguru import logger - -from modules import emoji_literals, queries, util from modules.media_embedders import InstagramEmbedder, TikTokEmbedder from modules.misobot import MisoBot +from modules import emoji_literals, queries, util + class Events(commands.Cog): """Event handlers for various discord events""" @@ -64,7 +64,9 @@ async def on_command_completion(self, ctx: commands.Context): if ctx.guild is not None: await queries.save_command_usage(ctx) - if random.randint(1, 69) == 1 and not await queries.is_donator(ctx, ctx.author): + if random.randint(1, 69) == 1 and not await queries.is_donator( + ctx, ctx.author + ): logger.info("Sending donation beg message") await util.send_donation_beg(ctx.channel) @@ -79,7 +81,9 @@ async def on_guild_join(self, guild): guild.id, ) if blacklisted: - logger.info(f"Tried to join guild {guild}. Reason for blacklist: {blacklisted}") + logger.info( + f"Tried to join guild {guild}. Reason for blacklist: {blacklisted}" + ) return await guild.leave() logger.info(f"New guild : {guild}") @@ -129,7 +133,9 @@ async def on_member_join(self, member): """Called when a new member joins a guild""" await self.bot.wait_until_ready() logging_channel_id = None - if logging_settings := self.bot.cache.logging_settings.get(str(member.guild.id)): + if logging_settings := self.bot.cache.logging_settings.get( + str(member.guild.id) + ): logging_channel_id = logging_settings.get("member_log_channel_id") if logging_channel_id: @@ -155,7 +161,10 @@ async def on_member_join(self, member): # welcome message greeter = await self.bot.db.fetch_row( - "SELECT channel_id, is_enabled, message_format FROM greeter_settings WHERE guild_id = %s", + """ + SELECT channel_id, is_enabled, message_format + FROM greeter_settings WHERE guild_id = %s + """, member.guild.id, ) if greeter: @@ -165,7 +174,9 @@ async def on_member_join(self, member): if greeter_channel is not None: try: await greeter_channel.send( - embed=util.create_welcome_embed(member, member.guild, message_format) + embed=util.create_welcome_embed( + member, member.guild, message_format + ) ) except discord.errors.Forbidden: pass @@ -197,7 +208,9 @@ async def on_member_remove(self, member): """Called when member leaves a guild""" await self.bot.wait_until_ready() logging_channel_id = None - if logging_settings := self.bot.cache.logging_settings.get(str(member.guild.id)): + if logging_settings := self.bot.cache.logging_settings.get( + str(member.guild.id) + ): logging_channel_id = logging_settings.get("member_log_channel_id") if logging_channel_id: @@ -212,7 +225,10 @@ async def on_member_remove(self, member): # goodbye message goodbye = await self.bot.db.fetch_row( - "SELECT channel_id, is_enabled, message_format FROM goodbye_settings WHERE guild_id = %s", + """ + SELECT channel_id, is_enabled, message_format + FROM goodbye_settings WHERE guild_id = %s + """, member.guild.id, ) if goodbye: @@ -225,7 +241,9 @@ async def on_member_remove(self, member): try: await channel.send( - util.create_goodbye_message(member, member.guild, message_format) + util.create_goodbye_message( + member, member.guild, message_format + ) ) except discord.errors.Forbidden: pass @@ -256,7 +274,9 @@ async def on_raw_message_delete(self, payload): return channel_id = None - if logging_settings := self.bot.cache.logging_settings.get(str(message.guild.id)): + if logging_settings := self.bot.cache.logging_settings.get( + str(message.guild.id) + ): channel_id = logging_settings.get("message_log_channel_id") if channel_id: log_channel = message.guild.get_channel(channel_id) @@ -292,29 +312,74 @@ async def on_message(self, message: discord.Message): media_settings = self.bot.cache.media_auto_embed.get(str(message.guild.id), {}) if True in media_settings.values(): - await self.parse_media_auto_embed(message, media_settings) + try: + await self.parse_media_auto_embed(message, media_settings) + except Exception as e: + await self.bot.get_cog("ErrorHandler").on_command_error(ctx, e) if self.bot.cache.autoresponse.get(str(message.guild.id), True): await self.easter_eggs(message) - async def parse_media_auto_embed(self, message: discord.Message, media_settings: dict): + async def get_autoembed_options( + self, guild_id: int, provider: str + ) -> tuple[str | None, bool | None]: + options_data = await self.bot.db.fetch_row( + """ + SELECT options, reply FROM media_auto_embed_options + WHERE guild_id = %s AND provider = %s + """, + guild_id, + provider, + ) + print(options_data) + if options_data: + return options_data + + return None, None + + async def parse_media_auto_embed( + self, message: discord.Message, media_settings: dict + ): if media_settings["instagram"]: embedder = InstagramEmbedder(self.bot) posts = embedder.extract_links(message.content, include_shortcodes=False) - for post in posts: - async with message.channel.typing(): - await embedder.send_reply(message, post) if posts: + options, should_reply = await self.get_autoembed_options( + message.guild.id, "instagram" + ) + for post in posts: + async with message.channel.typing(): + if not should_reply: + await embedder.send_contextless( + message.channel, + message.author, + post, + embedder.get_options(options) if options else None, + ) + else: + await embedder.send_reply(message, post) await util.suppress(message) if media_settings["tiktok"]: embedder = TikTokEmbedder(self.bot) links = embedder.extract_links(message.content) - for link in links: - async with message.channel.typing(): - await embedder.send_reply(message, link) - if links: - await util.suppress(message) + if links: + options, should_reply = await self.get_autoembed_options( + message.guild.id, + "tiktok", + ) + for link in links: + async with message.channel.typing(): + if not should_reply: + await embedder.send_contextless( + message.channel, + message.author, + link, + embedder.get_options(options) if options else None, + ) + else: + await embedder.send_reply(message, link) + await util.suppress(message) @staticmethod async def easter_eggs(message: discord.Message): @@ -384,7 +449,9 @@ async def on_raw_reaction_add(self, payload): if payload.channel_id in self.bot.cache.starboard_blacklisted_channels: return - starboard_settings = self.bot.cache.starboard_settings.get(str(payload.guild_id)) + starboard_settings = self.bot.cache.starboard_settings.get( + str(payload.guild_id) + ) if not starboard_settings: return @@ -453,7 +520,9 @@ async def on_raw_reaction_add(self, payload): payload.message_id, ) emoji_display = ( - "⭐" if emoji_type == "custom" else emoji_literals.NAME_TO_UNICODE[emoji_name] + "⭐" + if emoji_type == "custom" + else emoji_literals.NAME_TO_UNICODE[emoji_name] ) board_message = None @@ -470,7 +539,10 @@ async def on_raw_reaction_add(self, payload): board_message = await board_channel.send(embed=content) except discord.Forbidden: return await message.reply( - f"I tried to starboard this but I don't have permission to send embed in {board_channel.mention} :(" + ( + "I tried to starboard this but I don't have permission to " + f"send embed in {board_channel.mention} :(" + ) ) await self.bot.db.execute( """ @@ -500,7 +572,9 @@ async def on_raw_reaction_add(self, payload): value="\n".join(str(x) for x in reacted_users)[:1023], inline=False, ) - log_content.add_field(name="Most recent reaction by", value=str(user)) + log_content.add_field( + name="Most recent reaction by", value=str(user) + ) try: await log_channel.send(embed=log_content) except discord.HTTPException: @@ -521,7 +595,9 @@ def starboard_embed(message: discord.Message, reaction_count: int, emoji: str): name=f"{message.author}", icon_url=message.author.display_avatar.url, ) - content.set_footer(text=f"{reaction_count} {emoji} {util.displaychannel(message.channel)}") + content.set_footer( + text=f"{reaction_count} {emoji} {util.displaychannel(message.channel)}" + ) if message.attachments: content.set_image(url=message.attachments[0].url) diff --git a/cogs/fishy.py b/cogs/fishy.py index 2b7d359..dcdcae8 100644 --- a/cogs/fishy.py +++ b/cogs/fishy.py @@ -8,9 +8,9 @@ import discord import humanize from discord.ext import commands +from modules.misobot import MisoBot from modules import exceptions, util -from modules.misobot import MisoBot class Fishy(commands.Cog): @@ -105,7 +105,9 @@ def __init__(self, bot): # idk why this doesnt work but it gets stuck all the time # @commands.max_concurrency(1, per=commands.BucketType.user) @commands.cooldown(1, 5, type=commands.BucketType.user) - @commands.command(aliases=["fish", "fihy", "fisy", "foshy", "fisyh", "fsihy", "fin", "fush"]) + @commands.command( + aliases=["fish", "fihy", "fisy", "foshy", "fisyh", "fsihy", "fin", "fush"] + ) async def fishy(self, ctx: commands.Context, user: Optional[discord.Member] = None): """Go fishing""" receiver = user or ctx.author @@ -126,7 +128,9 @@ async def fishy(self, ctx: commands.Context, user: Optional[discord.Member] = No else: last_fishy = cached_last_fishy if last_fishy: - time_since_fishy = ctx.message.created_at.timestamp() - last_fishy.timestamp() + time_since_fishy = ( + ctx.message.created_at.timestamp() - last_fishy.timestamp() + ) else: time_since_fishy = self.COOLDOWN @@ -180,12 +184,18 @@ async def fishytimer(self, ctx: commands.Context): ctx.author.id, ) if last_fishy: - time_since_fishy = ctx.message.created_at.timestamp() - last_fishy.timestamp() + time_since_fishy = ( + ctx.message.created_at.timestamp() - last_fishy.timestamp() + ) if time_since_fishy < self.COOLDOWN: remaining = self.COOLDOWN - time_since_fishy wait_time = humanize.precisedelta(remaining) - clock_face = f":clock{int(util.map_to_range(remaining, 7200, 0, 1, 12))}:" - await ctx.send(f"{clock_face} You need to wait **{wait_time}** to fish again.") + clock_face = ( + f":clock{int(util.map_to_range(remaining, 7200, 0, 1, 12))}:" + ) + await ctx.send( + f"{clock_face} You need to wait **{wait_time}** to fish again." + ) else: await ctx.send(":sparkles: Good news! You can fish right now!") else: diff --git a/cogs/information.py b/cogs/information.py index 5196a51..d63d80b 100644 --- a/cogs/information.py +++ b/cogs/information.py @@ -13,9 +13,9 @@ import orjson import psutil from discord.ext import commands +from modules.misobot import MisoBot from modules import emojis, exceptions, util -from modules.misobot import MisoBot class Information(commands.Cog): @@ -70,7 +70,9 @@ async def donate(self, ctx: commands.Context): value="`ltc1qsxmy8q8ptdlhdcamypa2uspj8zf8m0ukua9vn5`", inline=False, ) - content.set_footer(text="Donations will be used to pay for server and upkeep costs") + content.set_footer( + text="Donations will be used to pay for server and upkeep costs" + ) await ctx.send(embed=content) @commands.command(aliases=["patrons", "supporters", "sponsors"]) @@ -94,7 +96,9 @@ async def donators(self, ctx: commands.Context): donators.append(f"**{user}**") n = 20 - chunks = [donators[i * n : (i + 1) * n] for i in range((len(donators) + n - 1) // n)] + chunks = [ + donators[i * n : (i + 1) * n] for i in range((len(donators) + n - 1) // n) + ] if not donators: raise exceptions.CommandInfo( @@ -140,8 +144,12 @@ async def info(self, ctx: commands.Context): ) content.set_thumbnail(url=self.bot.user.display_avatar.url) content.add_field(name="Website", value="https://misobot.xyz", inline=False) - content.add_field(name="Github", value="https://github.com/joinemm/miso-bot", inline=False) - content.add_field(name="Discord", value="https://discord.gg/RzDW3Ne", inline=False) + content.add_field( + name="Github", value="https://github.com/joinemm/miso-bot", inline=False + ) + content.add_field( + name="Discord", value="https://discord.gg/RzDW3Ne", inline=False + ) data = await self.get_commits("joinemm", "miso-bot") last_update = data[0]["commit"]["author"].get("date") @@ -153,7 +161,9 @@ async def info(self, ctx: commands.Context): async def ping(self, ctx: commands.Context): """Get the bot's ping""" test_message = await ctx.send(":ping_pong:") - cmd_lat = (test_message.created_at - ctx.message.created_at).total_seconds() * 1000 + cmd_lat = ( + test_message.created_at - ctx.message.created_at + ).total_seconds() * 1000 discord_lat = self.bot.latency * 1000 content = discord.Embed( colour=discord.Color.red(), @@ -193,10 +203,18 @@ async def shardinfo(self, ctx: commands.Context): content = discord.Embed(title=f"Running {len(self.bot.shards)} shards") shards = [] for shard in self.bot.shards.values(): - emoji = emojis.Status["offline"] if shard.is_closed() else emojis.Status["online"] + emoji = ( + emojis.Status["offline"] + if shard.is_closed() + else emojis.Status["online"] + ) shards.append( f"{emoji.value} **Shard `{shard.id}`** - `{shard.latency * 1000:.2f}` ms" - + (" :point_left:" if ctx.guild and ctx.guild.shard_id == shard.id else "") + + ( + " :point_left:" + if ctx.guild and ctx.guild.shard_id == shard.id + else "" + ) ) content.description = "\n".join(shards) @@ -204,6 +222,7 @@ async def shardinfo(self, ctx: commands.Context): @commands.command() async def shardof(self, ctx: commands.Context, guild_id: int): + """Find the shard ID of given guild ID""" guild = self.bot.get_guild(guild_id) if guild is None: raise exceptions.CommandWarning(f"Guild `{guild_id}` not found") @@ -257,7 +276,9 @@ async def roleinfo(self, ctx: commands.Context, *, role: discord.Role): content.colour = role.color member_count = len(role.members) percentage = ( - int(member_count / ctx.guild.member_count * 100) if ctx.guild.member_count else None + int(member_count / ctx.guild.member_count * 100) + if ctx.guild.member_count + else None ) if isinstance(role.icon, discord.Asset): @@ -272,7 +293,9 @@ async def roleinfo(self, ctx: commands.Context, *, role: discord.Role): if ctx.guild.member_count else member_count, ) - content.add_field(name="Created at", value=role.created_at.strftime("%d/%m/%Y %H:%M")) + content.add_field( + name="Created at", value=role.created_at.strftime("%d/%m/%Y %H:%M") + ) content.add_field(name="Hoisted", value=str(role.hoist)) content.add_field(name="Mentionable", value=role.mentionable) content.add_field(name="Mention", value=role.mention) @@ -287,8 +310,12 @@ async def roleinfo(self, ctx: commands.Context, *, role: discord.Role): manager = "UNKNOWN" content.add_field(name="Managed by", value=manager) - if perms := [f"`{perm.upper()}`" for perm, allow in iter(role.permissions) if allow]: - content.add_field(name="Allowed permissions", value=" ".join(perms), inline=False) + if perms := [ + f"`{perm.upper()}`" for perm, allow in iter(role.permissions) if allow + ]: + content.add_field( + name="Allowed permissions", value=" ".join(perms), inline=False + ) await ctx.send(embed=content) @@ -337,7 +364,8 @@ async def commandstats_server( for i, (command_name, count) in enumerate(data, start=1): total += count rows.append( - f"`#{i:2}` **{count}** use{'' if count == 1 else 's'} : `{ctx.prefix}{command_name}`" + f"`#{i:2}` **{count}** use{'' if count == 1 else 's'} : " + f"`{ctx.prefix}{command_name}`" ) if rows: @@ -355,7 +383,8 @@ async def commandstats_global( ): """Most used commands globally""" content = discord.Embed( - title=":bar_chart: Most used commands" + ("" if user is None else f" by {user}") + title=":bar_chart: Most used commands" + + ("" if user is None else f" by {user}") ) opt = [user.id] if user is not None else [] data = await self.bot.db.fetch( @@ -379,7 +408,8 @@ async def commandstats_global( for i, (command_name, count) in enumerate(data, start=1): total += count rows.append( - f"`#{i:2}` **{count}** use{'' if count == 1 else 's'} : `{ctx.prefix}{command_name}`" + f"`#{i:2}` **{count}** use{'' if count == 1 else 's'} : " + f"`{ctx.prefix}{command_name}`" ) if rows: @@ -393,15 +423,19 @@ async def commandstats_single(self, ctx: commands.Context, command_name): """Stats of a single command""" command = self.bot.get_command(command_name) if command is None: - raise exceptions.CommandInfo(f"Command `{ctx.prefix}{command_name}` does not exist!") + raise exceptions.CommandInfo( + f"Command `{ctx.prefix}{command_name}` does not exist!" + ) - content = discord.Embed(title=f":bar_chart: `{ctx.prefix}{command.qualified_name}`") + content = discord.Embed( + title=f":bar_chart: `{ctx.prefix}{command.qualified_name}`" + ) # set command name to be tuple of subcommands if this is a command group group = hasattr(command, "commands") if group: command_name = tuple( - [f"{command.name} {x.name}" for x in command.commands] + [command_name] # type: ignore + [f"{command.name} {x.name}" for x in command.commands] + [command_name] ) else: command_name = command.qualified_name @@ -476,7 +510,9 @@ async def commandstats_single(self, ctx: commands.Context, command_name): # additional data for command groups if group: content.description = "Command Group" - subcommands_tuple = tuple(f"{command.name} {x.name}" for x in command.commands) # type: ignore + subcommands_tuple = tuple( + f"{command.name} {x.name}" for x in command.commands + ) subcommand_usage = await self.bot.db.fetch( """ SELECT command_name, SUM(uses) FROM command_usage diff --git a/cogs/kpop.py b/cogs/kpop.py index f13ce6d..515fd84 100644 --- a/cogs/kpop.py +++ b/cogs/kpop.py @@ -13,9 +13,9 @@ import humanize from bs4 import BeautifulSoup from discord.ext import commands +from modules.misobot import MisoBot from modules import exceptions, util -from modules.misobot import MisoBot class Kpop(commands.Cog): @@ -38,7 +38,9 @@ async def shutdown(self): async def google_image_search(self, keyword): try: - results = await self.google_client.search(keyword, safesearch=False, image_search=True) + results = await self.google_client.search( + keyword, safesearch=False, image_search=True + ) except async_cse.search.APIError: return "" return results[0].image_url if results else "" @@ -67,7 +69,8 @@ async def birthdays(self, ctx: commands.Context, month: int, day: int): ) rows = [ - f"{self.gender_icon.get(gender, '')} **{f'{group} ' if group is not None else ''} {name}** ({dob.year})" + f"{self.gender_icon.get(gender, '')} " + f"**{f'{group} ' if group is not None else ''} {name}** ({dob.year})" for gender, group, name, dob in idol_data ] content = discord.Embed(title=f"Kpop idols born on {humanize.naturalday(dt)}") @@ -157,7 +160,9 @@ async def send_idol(self, ctx: commands.Context, idol_id): ) content.set_image(url=image_url) content.add_field(name="Full name", value=full_name) - content.add_field(name="Korean name", value=f"{korean_stage_name} ({korean_name})") + content.add_field( + name="Korean name", value=f"{korean_stage_name} ({korean_name})" + ) content.add_field( name="Birthday", value=arrow.get(date_of_birth).format("YYYY-MM-DD") + f" (age {age})", @@ -207,11 +212,17 @@ async def scrape(category, url): artists = [] async with self.bot.session.get(url) as response: soup = BeautifulSoup(await response.text(), "lxml") - content = soup.find("div", {"class": "entry-content herald-entry-content"}) + content = soup.find( + "div", {"class": "entry-content herald-entry-content"} + ) outer = content.find_all("p") # type: ignore for p in outer: for artist in p.find_all("a"): - artist = artist.text.replace("Profile", "").replace("profile", "").strip() + artist = ( + artist.text.replace("Profile", "") + .replace("profile", "") + .strip() + ) if artist != "": artists.append([artist, category]) return artists @@ -231,7 +242,8 @@ async def scrape(category, url): await util.send_success( ctx, - f"**Artist list updated**\n" f"Stannable artist count: **{len(new_artist_list)}**", + f"**Artist list updated**\n" + f"Stannable artist count: **{len(new_artist_list)}**", ) @commands.is_owner() diff --git a/cogs/lastfm.py b/cogs/lastfm.py index 4fd5527..816889b 100644 --- a/cogs/lastfm.py +++ b/cogs/lastfm.py @@ -19,11 +19,11 @@ from bs4 import BeautifulSoup from discord.ext import commands from loguru import logger +from modules.genius import Genius +from modules.misobot import MisoBot from PIL import Image from modules import emojis, exceptions, util -from modules.genius import Genius -from modules.misobot import MisoBot MISSING_IMAGE_HASH = "2a96cbd8b46e442fc41c2b86b821562f" @@ -136,7 +136,9 @@ async def fm_blacklist_add(self, ctx: commands.Context, *, member: discord.Membe ) @fm_blacklist.command(name="remove") - async def fm_blacklist_remove(self, ctx: commands.Context, *, member: discord.Member): + async def fm_blacklist_remove( + self, ctx: commands.Context, *, member: discord.Member + ): """Remove a member from the blacklist""" if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") @@ -154,11 +156,15 @@ async def fm_blacklist_remove(self, ctx: commands.Context, *, member: discord.Me async def set(self, ctx: commands.Context, username): """Save your Last.fm username""" if ctx.foreign_target: # type: ignore - raise exceptions.CommandWarning("You cannot set Last.fm username for someone else!") + raise exceptions.CommandWarning( + "You cannot set Last.fm username for someone else!" + ) content = await self.get_userinfo_embed(username) if content is None: - raise exceptions.CommandWarning(f"Last.fm profile `{username}` was not found") + raise exceptions.CommandWarning( + f"Last.fm profile `{username}` was not found" + ) await self.bot.db.execute( """ @@ -179,7 +185,9 @@ async def set(self, ctx: commands.Context, username): async def unset(self, ctx: commands.Context): """Unlink your Last.fm""" if ctx.foreign_target: # type: ignore - raise exceptions.CommandWarning("You cannot unset someone else's Last.fm username!") + raise exceptions.CommandWarning( + "You cannot unset someone else's Last.fm username!" + ) await self.bot.db.execute( """ @@ -198,7 +206,9 @@ async def profile(self, ctx: commands.Context): """See your Last.fm profile""" content = await self.get_userinfo_embed(ctx.username) if content is None: - raise exceptions.CommandError(f"Could not get your lastfm profile (`{ctx.username}`)") + raise exceptions.CommandError( + f"Could not get your lastfm profile (`{ctx.username}`)" + ) await ctx.send(embed=content) # type: ignore @fm.command() @@ -211,7 +221,11 @@ async def milestone(self, ctx: commands.Context, n: int): ) per_page = 100 pre_data = await self.api_request( - {"user": ctx.username, "method": "user.getrecenttracks", "limit": per_page} # type: ignore + { + "user": ctx.username, + "method": "user.getrecenttracks", + "limit": per_page, + } # type: ignore ) total = int(pre_data["recenttracks"]["@attr"]["total"]) @@ -573,7 +587,9 @@ async def recent(self, ctx: commands.Context, size: int = 15): await util.send_as_pages(ctx, content, rows, 15) @fm.command(usage="[timeframe] ") - async def artist(self, ctx: commands.Context, timeframe, datatype, *, artistname=""): + async def artist( + self, ctx: commands.Context, timeframe, datatype, *, artistname="" + ): """ Artist specific data. @@ -592,7 +608,9 @@ async def artist(self, ctx: commands.Context, timeframe, datatype, *, artistname if artistname.lower() == "np": artistname = (await self.getnowplaying(ctx))["artist"] if artistname is None: - raise exceptions.CommandWarning("Could not get currently playing artist!") + raise exceptions.CommandWarning( + "Could not get currently playing artist!" + ) if artistname == "": return await ctx.send("Missing artist name!") @@ -676,14 +694,18 @@ async def album(self, ctx: commands.Context, *, album): albumname = npd["album"] artistname = npd["artist"] if None in [albumname, artistname]: - raise exceptions.CommandWarning("Could not get currently playing album!") + raise exceptions.CommandWarning( + "Could not get currently playing album!" + ) else: try: albumname, artistname = [x.strip() for x in album.split("|")] if "" in (albumname, artistname): raise ValueError except ValueError: - raise exceptions.CommandWarning("Incorrect format! use `album | artist`") + raise exceptions.CommandWarning( + "Incorrect format! use `album | artist`" + ) album, data = await self.album_top_tracks(ctx, period, artistname, albumname) if album is None or not data: @@ -724,7 +746,9 @@ async def album(self, ctx: commands.Context, *, album): await util.send_as_pages(ctx, content, rows) - async def album_top_tracks(self, ctx: commands.Context, period, artistname, albumname): + async def album_top_tracks( + self, ctx: commands.Context, period, artistname, albumname + ): """Scrape the top tracks of given album from lastfm library page""" artistname = urllib.parse.quote_plus(artistname) albumname = urllib.parse.quote_plus(albumname) @@ -743,7 +767,9 @@ async def album_top_tracks(self, ctx: commands.Context, period, artistname, albu .find("img") .get("src") .replace("64s", "300s"), - "formatted_name": soup.find("h2", {"class": "library-header-title"}).text.strip(), + "formatted_name": soup.find( + "h2", {"class": "library-header-title"} + ).text.strip(), "artist": soup.find("header", {"class": "library-header"}) .find("a", {"class": "text-colour-link"}) .text.strip(), @@ -772,7 +798,9 @@ async def artist_top(self, ctx: commands.Context, period, artistname, datatype): .find("img") .get("src") .replace("avatar70s", "avatar300s"), - "formatted_name": soup.find("a", {"class": "library-header-crumb"}).text.strip(), + "formatted_name": soup.find( + "a", {"class": "library-header-crumb"} + ).text.strip(), } all_results = get_list_contents(soup) @@ -785,7 +813,9 @@ async def artist_overview(self, ctx: commands.Context, period, artistname): albums = [] tracks = [] metadata = [None, None, None] - artistinfo = await self.api_request({"method": "artist.getInfo", "artist": artistname}) + artistinfo = await self.api_request( + {"method": "artist.getInfo", "artist": artistname} + ) url = ( f"https://last.fm/user/{ctx.username}/library/music/" f"{urllib.parse.quote_plus(artistname)}" @@ -797,9 +827,11 @@ async def artist_overview(self, ctx: commands.Context, period, artistname): soup = BeautifulSoup(data, "lxml") try: - albumsdiv, tracksdiv, _ = soup.findAll("tbody", {"data-playlisting-add-entries": ""}) + tbodies = soup.findAll("tbody", {"data-playlisting-add-entries": ""}) - except ValueError: + albumsdiv = tbodies[1] + tracksdiv = tbodies[3] + except IndexError: artistname = discord.utils.escape_markdown(artistname) if period == "overall": return await ctx.send(f"You have never listened to **{artistname}**!") @@ -810,7 +842,9 @@ async def artist_overview(self, ctx: commands.Context, period, artistname): for container, destination in zip([albumsdiv, tracksdiv], [albums, tracks]): items = container.findAll("tr", {"class": "chartlist-row"}) for item in items: - name = item.find("td", {"class": "chartlist-name"}).find("a").get("title") + name = ( + item.find("td", {"class": "chartlist-name"}).find("a").get("title") + ) playcount = ( item.find("span", {"class": "chartlist-count-bar-value"}) .text.replace("scrobbles", "") @@ -830,7 +864,9 @@ async def artist_overview(self, ctx: commands.Context, period, artistname): .find("img") .get("src") .replace("avatar70s", "avatar300s"), - "formatted_name": soup.find("h2", {"class": "library-header-title"}).text.strip(), + "formatted_name": soup.find( + "h2", {"class": "library-header-title"} + ).text.strip(), } artistname = urllib.parse.quote_plus(artistname) @@ -885,7 +921,9 @@ async def artist_overview(self, ctx: commands.Context, period, artistname): ) if similar: - content.add_field(name="Similar artists", value=", ".join(similar), inline=False) + content.add_field( + name="Similar artists", value=", ".join(similar), inline=False + ) await ctx.send(embed=content) @@ -906,7 +944,9 @@ async def get_image(url): if image is None: return None - colors = await self.bot.loop.run_in_executor(None, lambda: colorgram.extract(image, 1)) + colors = await self.bot.loop.run_in_executor( + None, lambda: colorgram.extract(image, 1) + ) dominant_color = colors[0].rgb return ( @@ -978,7 +1018,9 @@ async def colorchart(self, ctx: commands.Context, colour, size="3x3"): albums.add(album_art_id) if not albums: - raise exceptions.CommandError("There was an unknown error while getting your albums!") + raise exceptions.CommandError( + "There was an unknown error while getting your albums!" + ) to_fetch = [] albumcolors = await self.bot.db.fetch( @@ -1002,7 +1044,9 @@ async def colorchart(self, ctx: commands.Context, colour, size="3x3"): if to_fetch: to_cache = [] - tasks = [self.fetch_color(self.bot.session, image_id) for image_id in to_fetch] + tasks = [ + self.fetch_color(self.bot.session, image_id) for image_id in to_fetch + ] if len(tasks) > 500: warn = await ctx.send( ":exclamation:Your library includes over 500 uncached album colours, " @@ -1050,7 +1094,9 @@ async def colorchart(self, ctx: commands.Context, colour, size="3x3"): (148, 0, 211), # violet ] ) - chunks = [list(tree.search_knn(rgb, width + height)) for rgb in rainbow_colors] + chunks = [ + list(tree.search_knn(rgb, width + height)) for rgb in rainbow_colors + ] random_offset = random.randint(0, 6) final_albums = [] for album_index in range(width * height): @@ -1081,7 +1127,9 @@ async def colorchart(self, ctx: commands.Context, colour, size="3x3"): for alb in nearest ] - buffer = await self.chart_factory(final_albums, width, height, show_labels=False) + buffer = await self.chart_factory( + final_albums, width, height, show_labels=False + ) if rainbow: colour = f"{'diagonal ' if diagonal else ''}rainbow" @@ -1122,7 +1170,11 @@ async def collage(self, ctx: commands.Context, *args): ) if arguments["period"] == "today": - data = await self.custom_period(ctx.username, arguments["method"]) + data = await self.custom_period( + ctx.username, + arguments["method"], + limit=arguments["amount"], + ) else: data = await self.api_request( { @@ -1157,7 +1209,9 @@ async def collage(self, ctx: commands.Context, *args): for i, artist in enumerate(artists): name = artist["name"] plays = artist["playcount"] - chart.append((scraped_images[i], f"{plays} {format_plays(plays)}
{name}")) + chart.append( + (scraped_images[i], f"{plays} {format_plays(plays)}
{name}") + ) elif arguments["method"] == "user.getrecenttracks": chart_type = "recent tracks" @@ -1184,11 +1238,15 @@ async def collage(self, ctx: commands.Context, *args): async def chart_factory(self, chart_items, width, height, show_labels=True): if show_labels: - img_div_template = '

{1}

' + img_div_template = ( + '

{1}

' + ) else: img_div_template = '
' - img_divs = "\n".join(img_div_template.format(*chart_item) for chart_item in chart_items) + img_divs = "\n".join( + img_div_template.format(*chart_item) for chart_item in chart_items + ) replacements = { "WIDTH": 300 * width, @@ -1205,7 +1263,9 @@ async def chart_factory(self, chart_items, width, height, show_labels=True): return await util.render_html(self.bot, payload) - async def server_lastfm_usernames(self, ctx: commands.Context, filter_blacklisted=False): + async def server_lastfm_usernames( + self, ctx: commands.Context, filter_blacklisted=False + ): guild_user_ids = [user.id for user in ctx.guild.members] args = [guild_user_ids] if filter_blacklisted: @@ -1233,7 +1293,9 @@ async def server(self, ctx: commands.Context): await util.command_group_help(ctx) @server.command( - name="chart", aliases=["collage"], usage="[album | artist] [timeframe] [size] 'notitle'" + name="chart", + aliases=["collage"], + usage="[album | artist] [timeframe] [size] 'notitle'", ) async def server_chart(self, ctx: commands.Context, *args): """ @@ -1269,7 +1331,9 @@ async def server_chart(self, ctx: commands.Context, *args): chart_type = "ERROR" content_map = {} if not tasks: - return await ctx.send("Nobody on this server has connected their last.fm account yet!") + return await ctx.send( + "Nobody on this server has connected their last.fm account yet!" + ) data = await asyncio.gather(*tasks) chart = [] @@ -1287,7 +1351,10 @@ async def server_chart(self, ctx: commands.Context, *args): if name in content_map: content_map[name]["plays"] += plays else: - content_map[name] = {"plays": plays, "image": album["image"][3]["#text"]} + content_map[name] = { + "plays": plays, + "image": album["image"][3]["#text"], + } elif arguments["method"] == "user.gettopartists": chart_type = "top artist" @@ -1345,12 +1412,18 @@ async def server_nowplaying(self, ctx: commands.Context): total_linked = len(tasks) if not tasks: - return await ctx.send("Nobody on this server has connected their last.fm account yet!") + return await ctx.send( + "Nobody on this server has connected their last.fm account yet!" + ) data = await asyncio.gather(*tasks) - listeners = [(song, member_ref) for song, member_ref in data if song is not None] + listeners = [ + (song, member_ref) for song, member_ref in data if song is not None + ] if not listeners: - return await ctx.send("Nobody on this server is listening to anything at the moment!") + return await ctx.send( + "Nobody on this server is listening to anything at the moment!" + ) total_listening = len(listeners) maxlen = 0 @@ -1400,12 +1473,18 @@ async def server_recent(self, ctx: commands.Context): total_listening += 1 listeners.append((song, member_ref)) else: - return await ctx.send("Nobody on this server has connected their last.fm account yet!") + return await ctx.send( + "Nobody on this server has connected their last.fm account yet!" + ) if not listeners: - return await ctx.send("Nobody on this server is listening to anything at the moment!") + return await ctx.send( + "Nobody on this server is listening to anything at the moment!" + ) - listeners = sorted(listeners, key=lambda listener: listener[0].get("date"), reverse=True) + listeners = sorted( + listeners, key=lambda listener: listener[0].get("date"), reverse=True + ) rows = [] for song, member in listeners: suffix = "" @@ -1448,7 +1527,11 @@ async def server_topartists(self, ctx: commands.Context, *args): if member is None: continue - tasks.append(self.get_server_top(lastfm_username, "artist", period=arguments["period"])) + tasks.append( + self.get_server_top( + lastfm_username, "artist", period=arguments["period"] + ) + ) if tasks: data = await asyncio.gather(*tasks) @@ -1465,7 +1548,9 @@ async def server_topartists(self, ctx: commands.Context, *args): else: artist_map[name] = plays else: - return await ctx.send("Nobody on this server has connected their last.fm account yet!") + return await ctx.send( + "Nobody on this server has connected their last.fm account yet!" + ) rows = [] formatted_timeframe = humanized_period(arguments["period"]).capitalize() @@ -1474,7 +1559,9 @@ async def server_topartists(self, ctx: commands.Context, *args): name=f"{ctx.guild} — {formatted_timeframe} top artists", icon_url=ctx.guild.icon, ) - content.set_footer(text=f"Taking into account top 100 artists of {total_users} members") + content.set_footer( + text=f"Taking into account top 100 artists of {total_users} members" + ) for i, (artistname, playcount) in enumerate( sorted(artist_map.items(), key=lambda x: x[1], reverse=True), start=1 ): @@ -1504,7 +1591,11 @@ async def server_topalbums(self, ctx: commands.Context, *args): if member is None: continue - tasks.append(self.get_server_top(lastfm_username, "album", period=arguments["period"])) + tasks.append( + self.get_server_top( + lastfm_username, "album", period=arguments["period"] + ) + ) if tasks: data = await asyncio.gather(*tasks) @@ -1522,7 +1613,9 @@ async def server_topalbums(self, ctx: commands.Context, *args): else: album_map[name] = {"plays": plays, "image": image_url} else: - return await ctx.send("Nobody on this server has connected their last.fm account yet!") + return await ctx.send( + "Nobody on this server has connected their last.fm account yet!" + ) rows = [] formatted_timeframe = humanized_period(arguments["period"]).capitalize() @@ -1531,7 +1624,9 @@ async def server_topalbums(self, ctx: commands.Context, *args): name=f"{ctx.guild} — {formatted_timeframe} top albums", icon_url=ctx.guild.icon, ) - content.set_footer(text=f"Taking into account top 100 albums of {total_users} members") + content.set_footer( + text=f"Taking into account top 100 albums of {total_users} members" + ) for i, (albumname, albumdata) in enumerate( sorted(album_map.items(), key=lambda x: x[1]["plays"], reverse=True), start=1, @@ -1542,7 +1637,9 @@ async def server_topalbums(self, ctx: commands.Context, *args): content.set_thumbnail(url=image_url) playcount = albumdata["plays"] - rows.append(f"`#{i:2}` **{playcount}** {format_plays(playcount)} : **{albumname}**") + rows.append( + f"`#{i:2}` **{playcount}** {format_plays(playcount)} : **{albumname}**" + ) await util.send_as_pages(ctx, content, rows, 15) @@ -1561,7 +1658,11 @@ async def server_toptracks(self, ctx: commands.Context, *args): if member is None: continue - tasks.append(self.get_server_top(lastfm_username, "track", period=arguments["period"])) + tasks.append( + self.get_server_top( + lastfm_username, "track", period=arguments["period"] + ) + ) if tasks: data = await asyncio.gather(*tasks) @@ -1579,7 +1680,9 @@ async def server_toptracks(self, ctx: commands.Context, *args): else: track_map[name] = {"plays": plays, "artist": artistname} else: - return await ctx.send("Nobody on this server has connected their last.fm account yet!") + return await ctx.send( + "Nobody on this server has connected their last.fm account yet!" + ) rows = [] formatted_timeframe = humanized_period(arguments["period"]).capitalize() @@ -1588,7 +1691,9 @@ async def server_toptracks(self, ctx: commands.Context, *args): name=f"{ctx.guild} — {formatted_timeframe} top tracks", icon_url=ctx.guild.icon, ) - content.set_footer(text=f"Taking into account top 100 tracks of {total_users} members") + content.set_footer( + text=f"Taking into account top 100 tracks of {total_users} members" + ) for i, (trackname, trackdata) in enumerate( sorted(track_map.items(), key=lambda x: x[1]["plays"], reverse=True), start=1, @@ -1599,7 +1704,9 @@ async def server_toptracks(self, ctx: commands.Context, *args): content.set_thumbnail(url=image_url) playcount = trackdata["plays"] - rows.append(f"`#{i:2}` **{playcount}** {format_plays(playcount)} : **{trackname}**") + rows.append( + f"`#{i:2}` **{playcount}** {format_plays(playcount)} : **{trackname}**" + ) await util.send_as_pages(ctx, content, rows, 15) @@ -1655,7 +1762,9 @@ async def whoknows(self, ctx: commands.Context, *, artistname): if artistname.lower() == "np": artistname = (await self.getnowplaying(ctx))["artist"] if artistname is None: - raise exceptions.CommandWarning("Could not get currently playing artist!") + raise exceptions.CommandWarning( + "Could not get currently playing artist!" + ) listeners = [] tasks = [] @@ -1675,7 +1784,9 @@ async def whoknows(self, ctx: commands.Context, *, artistname): if playcount > 0: listeners.append((playcount, member)) else: - return await ctx.send("Nobody on this server has connected their last.fm account yet!") + return await ctx.send( + "Nobody on this server has connected their last.fm account yet!" + ) artistname = discord.utils.escape_markdown(artistname) @@ -1717,7 +1828,9 @@ async def whoknows(self, ctx: commands.Context, *, artistname): total += playcount if not rows: - return await ctx.send(f"Nobody on this server has listened to **{artistname}**") + return await ctx.send( + f"Nobody on this server has listened to **{artistname}**" + ) content = discord.Embed(title=f"Who knows **{artistname}**?") image_url = await self.get_artist_image(artistname) @@ -1734,7 +1847,9 @@ async def whoknows(self, ctx: commands.Context, *, artistname): f"> **{util.displayname(new_king)}** just stole the **{artistname}** crown from **{util.displayname(old_king)}**" ) - @commands.command(aliases=["wkt", "whomstknowstrack"], usage=" | 'np'") + @commands.command( + aliases=["wkt", "whomstknowstrack"], usage=" | 'np'" + ) @commands.guild_only() @is_small_server() @commands.cooldown(2, 60, type=commands.BucketType.user) @@ -1752,14 +1867,18 @@ async def whoknowstrack(self, ctx: commands.Context, *, track): trackname = npd["track"] artistname = npd["artist"] if None in [trackname, artistname]: - raise exceptions.CommandWarning("Could not get currently playing track!") + raise exceptions.CommandWarning( + "Could not get currently playing track!" + ) else: try: trackname, artistname = [x.strip() for x in track.split("|")] if "" in (trackname, artistname): raise ValueError except ValueError: - raise exceptions.CommandWarning("Incorrect format! use `track | artist`") + raise exceptions.CommandWarning( + "Incorrect format! use `track | artist`" + ) listeners = [] tasks = [] @@ -1770,7 +1889,9 @@ async def whoknowstrack(self, ctx: commands.Context, *, track): if member is None: continue - tasks.append(self.get_playcount_track(artistname, trackname, lastfm_username, member)) + tasks.append( + self.get_playcount_track(artistname, trackname, lastfm_username, member) + ) if tasks: data = await asyncio.gather(*tasks) @@ -1779,7 +1900,9 @@ async def whoknowstrack(self, ctx: commands.Context, *, track): if playcount > 0: listeners.append((playcount, user)) else: - return await ctx.send("Nobody on this server has connected their last.fm account yet!") + return await ctx.send( + "Nobody on this server has connected their last.fm account yet!" + ) artistname = discord.utils.escape_markdown(artistname) trackname = discord.utils.escape_markdown(trackname) @@ -1810,7 +1933,9 @@ async def whoknowstrack(self, ctx: commands.Context, *, track): await util.send_as_pages(ctx, content, rows) - @commands.command(aliases=["wka", "whomstknowsalbum"], usage=" | 'np'") + @commands.command( + aliases=["wka", "whomstknowsalbum"], usage=" | 'np'" + ) @commands.guild_only() @is_small_server() @commands.cooldown(2, 60, type=commands.BucketType.user) @@ -1828,14 +1953,18 @@ async def whoknowsalbum(self, ctx: commands.Context, *, album): albumname = npd["album"] artistname = npd["artist"] if None in [albumname, artistname]: - raise exceptions.CommandWarning("Could not get currently playing album!") + raise exceptions.CommandWarning( + "Could not get currently playing album!" + ) else: try: albumname, artistname = [x.strip() for x in album.split("|")] if "" in (albumname, artistname): raise ValueError except ValueError: - raise exceptions.CommandWarning("Incorrect format! use `album | artist`") + raise exceptions.CommandWarning( + "Incorrect format! use `album | artist`" + ) listeners = [] tasks = [] @@ -1846,7 +1975,9 @@ async def whoknowsalbum(self, ctx: commands.Context, *, album): if member is None: continue - tasks.append(self.get_playcount_album(artistname, albumname, lastfm_username, member)) + tasks.append( + self.get_playcount_album(artistname, albumname, lastfm_username, member) + ) if tasks: data = await asyncio.gather(*tasks) @@ -1855,7 +1986,9 @@ async def whoknowsalbum(self, ctx: commands.Context, *, album): if playcount > 0: listeners.append((playcount, user)) else: - return await ctx.send("Nobody on this server has connected their last.fm account yet!") + return await ctx.send( + "Nobody on this server has connected their last.fm account yet!" + ) artistname = discord.utils.escape_markdown(artistname) albumname = discord.utils.escape_markdown(albumname) @@ -1931,7 +2064,9 @@ async def lyrics(self, ctx: commands.Context, *, query): trackname = npd["track"] artistname = npd["artist"] if None in [trackname, artistname]: - return await ctx.send(":warning: Could not get currently playing track!") + return await ctx.send( + ":warning: Could not get currently playing track!" + ) query = artistname + " " + trackname genius = Genius(self.bot) @@ -1956,7 +2091,7 @@ def check(message): except ValueError: return False else: - return num <= len(results) and num > 0 + return len(results) <= num > 0 else: return False @@ -2051,7 +2186,6 @@ async def get_userinfo_embed(self, username): async def listening_report(self, ctx: commands.Context, timeframe): current_day_floor = arrow.utcnow().floor("day") week = [] - # for i in range(7, 0, -1): for i in range(1, 8): dt = current_day_floor.shift(days=-i) week.append( @@ -2108,7 +2242,6 @@ async def listening_report(self, ctx: commands.Context, timeframe): value=f"{scrobbles_average} Scrobbles", inline=False, ) - # content.add_field(name="Listening time", value=listening_time) await ctx.send(embed=content) async def get_artist_image(self, artist): @@ -2160,7 +2293,9 @@ async def api_request(self, params, ignore_errors=False): if ignore_errors: return None text = await response.text() - raise exceptions.LastFMError(error_code=response.status, message=text) + raise exceptions.LastFMError( + error_code=response.status, message=text + ) if content is None: raise exceptions.LastFMError( @@ -2181,7 +2316,7 @@ async def api_request(self, params, ignore_errors=False): message=content.get("message"), ) - async def custom_period(self, user, group_by, shift_hours=24): + async def custom_period(self, user, group_by, shift_hours=24, limit=None): """Parse recent tracks to get custom duration data (24 hour)""" limit_timestamp = arrow.utcnow().shift(hours=-shift_hours) data = await self.api_request( @@ -2189,7 +2324,7 @@ async def custom_period(self, user, group_by, shift_hours=24): "user": user, "method": "user.getrecenttracks", "from": limit_timestamp.int_timestamp, - "limit": 200, + "limit": 300, } ) loops = int(data["recenttracks"]["@attr"]["totalPages"]) @@ -2200,7 +2335,7 @@ async def custom_period(self, user, group_by, shift_hours=24): "user": user, "method": "user.getrecenttracks", "from": limit_timestamp.int_timestamp, - "limit": 200, + "limit": 300, "page": i, } ) @@ -2221,7 +2356,11 @@ async def custom_period(self, user, group_by, shift_hours=24): "image": track["image"], } - albumsdata = sorted(formatted_data.values(), key=lambda x: x["playcount"], reverse=True) + albumsdata = sorted( + formatted_data.values(), key=lambda x: x["playcount"], reverse=True + ) + if limit: + albumsdata = albumsdata[:limit] return { "topalbums": { "album": albumsdata, @@ -2246,7 +2385,11 @@ async def custom_period(self, user, group_by, shift_hours=24): "image": track["image"], } - tracksdata = sorted(formatted_data.values(), key=lambda x: x["playcount"], reverse=True) + tracksdata = sorted( + formatted_data.values(), key=lambda x: x["playcount"], reverse=True + ) + if limit: + tracksdata = tracksdata[:limit] return { "toptracks": { "track": tracksdata, @@ -2269,7 +2412,11 @@ async def custom_period(self, user, group_by, shift_hours=24): "image": track["image"], } - artistdata = sorted(formatted_data.values(), key=lambda x: x["playcount"], reverse=True) + artistdata = sorted( + formatted_data.values(), key=lambda x: x["playcount"], reverse=True + ) + if limit: + artistdata = artistdata[:limit] return { "topartists": { "artist": artistdata, @@ -2289,7 +2436,11 @@ async def get_np(self, username, ref): if data is not None: try: tracks = data["recenttracks"]["track"] - if tracks and "@attr" in tracks[0] and "nowplaying" in tracks[0]["@attr"]: + if ( + tracks + and "@attr" in tracks[0] + and "nowplaying" in tracks[0]["@attr"] + ): song = { "artist": tracks[0]["artist"]["#text"], "name": tracks[0]["name"], @@ -2423,7 +2574,9 @@ async def get_playcount(self, artist, username, reference=None): return count, reference, name async def scrape_artist_image(self, artist): - url = f"https://www.last.fm/music/{urllib.parse.quote_plus(str(artist))}/+images" + url = ( + f"https://www.last.fm/music/{urllib.parse.quote_plus(str(artist))}/+images" + ) data, error = await fetch_html(self.bot, url) if error: return None @@ -2457,9 +2610,11 @@ async def scrape_artists_for_chart(self, username, period, amount): raise exceptions.LastFMError(0, error) soup = BeautifulSoup(data, "lxml") - imagedivs = soup.findAll("td", {"class": "chartlist-image"}) + tbody = soup.findAll("tbody", {"data-playlisting-add-entries": ""})[-2] + imagedivs = tbody.findAll("td", {"class": "chartlist-image"}) images += [ - div.find("img")["src"].replace("/avatar70s/", "/300x300/") for div in imagedivs + div.find("img")["src"].replace("/avatar70s/", "/300x300/") + for div in imagedivs ] return images @@ -2597,7 +2752,7 @@ async def fetch_html(bot: MisoBot, url: str, params: Optional[dict] = None): """Returns tuple of (data, error)""" headers = headers = { "Host": "www.last.fm", - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:106.0) Gecko/20100101 Firefox/106.0", + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/114.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "fi,en;q=0.7,en-US;q=0.3", "Accept-Encoding": "gzip, deflate, br", @@ -2646,7 +2801,8 @@ async def username_to_ctx(ctx: commands.Context): ctx.usertarget.id, ) if not ctx.username and ( - not ctx.invoked_subcommand or ctx.invoked_subcommand.name not in ["set", "blacklist"] + not ctx.invoked_subcommand + or ctx.invoked_subcommand.name not in ["set", "blacklist"] ): if not ctx.foreign_target: msg = f"No last.fm username saved! Please use `{ctx.prefix}fm set` to save your username (last.fm account required)" @@ -2664,7 +2820,7 @@ def remove_mentions(text): def get_list_contents(soup): """Scrape lastfm for listing pages""" try: - chartlist = soup.find("tbody", {"data-playlisting-add-entries": ""}) + chartlist = soup.findAll("tbody", {"data-playlisting-add-entries": ""})[-2] except ValueError: return [] diff --git a/cogs/media.py b/cogs/media.py index 0747bda..a59f01f 100644 --- a/cogs/media.py +++ b/cogs/media.py @@ -4,16 +4,21 @@ import asyncio import random -from typing import Literal, Optional +from typing import Literal import discord import orjson from bs4 import BeautifulSoup from discord.ext import commands +from modules.media_embedders import ( + BaseEmbedder, + InstagramEmbedder, + TikTokEmbedder, + TwitterEmbedder, +) +from modules.misobot import MisoBot from modules import emojis, exceptions, util -from modules.media_embedders import InstagramEmbedder, TikTokEmbedder, TwitterEmbedder -from modules.misobot import MisoBot class Media(commands.Cog): @@ -21,7 +26,7 @@ class Media(commands.Cog): def __init__(self, bot): self.bot: MisoBot = bot - self.icon = "🌐" + self.icon = "🖼️" @commands.command(aliases=["yt"]) async def youtube(self, ctx: commands.Context, *, query): @@ -45,85 +50,182 @@ async def youtube(self, ctx: commands.Context, *, query): await util.paginate_list( ctx, - [f"https://youtube.com/watch?v={item['id']['videoId']}" for item in data.get("items")], + [ + f"https://youtube.com/watch?v={item['id']['videoId']}" + for item in data.get("items") + ], use_locking=True, only_author=True, index_entries=True, ) - @commands.command() + @util.patrons_only() + @commands.group() async def autoembedder( - self, - ctx: commands.Context, - provider: Literal["instagram", "tiktok"], - state: Optional[bool] = None, + self, ctx: commands.Context, provider: Literal["instagram", "tiktok"] ): """Set up automatic embeds for various media sources The links will be expanded automatically when detected in chat, - without requiring the use use the corresponding command + without requiring the use of the corresponding command """ if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") - if state is None: - # show current state - data = await self.bot.db.fetch_value( + if ctx.invoked_subcommand is None: + enabled = await self.bot.db.fetch_value( f""" - SELECT {provider} FROM media_auto_embed_settings WHERE guild_id = %s + SELECT {provider} FROM media_auto_embed_enabled WHERE guild_id = %s """, ctx.guild.id, ) - if data is None: - current_state = "Not configured" - elif data: - current_state = "ON" + options_data = await self.bot.db.fetch_row( + """ + SELECT options, reply FROM media_auto_embed_options + WHERE guild_id = %s AND provider = %s + """, + ctx.guild.id, + provider, + ) + if options_data: + options, reply = options_data else: - current_state = "OFF" + options, reply = None, None - return await ctx.send( + await ctx.send( embed=discord.Embed( - description=f"{provider.capitalize()} automatic embeds are currently **{current_state}** for this server" + title=f"{provider.capitalize()} autoembedder", + description=( + f"ENABLED: {':white_check_mark:' if enabled else ':x:'}\n" + f"OPTIONS: `{options}`\n" + f"REPLIES: {':white_check_mark:' if reply else ':x:'}" + ), ) ) + else: + ctx.provider = provider - # set new state - if state: - # check for donation status if trying to turn on - await util.patron_check(ctx) - + @autoembedder.command(name="toggle") + async def autoembedder_toggle(self, ctx: commands.Context): + """Toggle the autoembedder on or off for given media provider""" + data = await self.bot.db.fetch_value( + f""" + SELECT {ctx.provider} FROM media_auto_embed_enabled WHERE guild_id = %s + """, + ctx.guild.id, + ) await self.bot.db.execute( f""" - INSERT INTO media_auto_embed_settings (guild_id, {provider}) + INSERT INTO media_auto_embed_enabled (guild_id, {ctx.provider}) VALUES (%s, %s) ON DUPLICATE KEY UPDATE - {provider} = %s + {ctx.provider} = %s """, ctx.guild.id, - state, - state, + not data, + not data, ) await self.bot.cache.cache_auto_embedders() await util.send_success( ctx, - f"{provider.capitalize()} automatic embeds are now **{'ON' if state else 'OFF'}** for this server", + f"{ctx.provider.capitalize()} automatic embeds are now " + f"**{'OFF' if data else 'ON'}** for this server", + ) + + @autoembedder.command(name="options") + async def autoembedder_options(self, ctx: commands.Context, *, options: str): + """Set options to be applied to an automatic embed + + Refer to the help of the embedder commands for list of options. + """ + parsed_options = BaseEmbedder.get_options(options) + options = parsed_options.sanitized_string + + await self.bot.db.execute( + """ + INSERT INTO media_auto_embed_options (guild_id, provider, options) + VALUES (%s, %s, %s) + ON DUPLICATE KEY UPDATE + options = %s + """, + ctx.guild.id, + ctx.provider, + options, + options, + ) + + await util.send_success( + ctx, + f"{ctx.provider.capitalize()} automatic embed OPTIONS are now:\n" + f"```yml\nCAPTIONS: {parsed_options.captions}\n" + f"DELETE_AFTER: {parsed_options.delete_after}\n" + f"SPOILER: {parsed_options.spoiler}```\n", ) - @commands.command(aliases=["ig", "insta"]) + @autoembedder.command(name="reply", usage="") + async def autoembedder_reply(self, ctx: commands.Context, on_or_off: bool): + """Should the automatic embed be a reply to the invoking message""" + await self.bot.db.execute( + """ + INSERT INTO media_auto_embed_options (guild_id, provider, reply) + VALUES (%s, %s, %s) + ON DUPLICATE KEY UPDATE + reply = %s + """, + ctx.guild.id, + ctx.provider, + on_or_off, + on_or_off, + ) + + await util.send_success( + ctx, + f"{ctx.provider.capitalize()} automatic embed is now " + f"{'' if on_or_off else 'NOT '}a reply", + ) + + @commands.command( + aliases=["ig", "insta"], + usage="[OPTIONS] ", + ) async def instagram(self, ctx: commands.Context, *, links: str): - """Retrieve media from Instagram post, reel or story""" + """Retrieve media from Instagram post, reel or story + + OPTIONS + `-c`, `--caption` : also include the caption/text of the media + `-s`, `--spoiler` : spoiler the uploaded images and text + `-d`, `--delete` : delete your message when the media is done embedding + """ await InstagramEmbedder(self.bot).process(ctx, links) - @commands.command(aliases=["twt"]) + @commands.command( + aliases=["twt", "x"], + usage="[OPTIONS] ", + ) async def twitter(self, ctx: commands.Context, *, links: str): - """Retrieve media from a tweet""" + """Retrieve media from a tweet + + OPTIONS + `-c`, `--caption` : also include the caption/text of the media + `-s`, `--spoiler` : spoiler the uploaded images and text + `-d`, `--delete` : delete your message when the media is done embedding + """ await TwitterEmbedder(self.bot).process(ctx, links) - @commands.command(aliases=["tik", "tok", "tt"]) + @commands.command( + aliases=["tik", "tok", "tt"], + usage="[OPTIONS] ", + ) async def tiktok(self, ctx: commands.Context, *, links: str): - """Retrieve video without watermark from a TikTok""" + """Retrieve video without watermark from a TikTok + + OPTIONS + `-c`, `--caption` : also include the caption/text of the media + `-s`, `--spoiler` : spoiler the uploaded images and text + `-d`, `--delete` : delete your message when the media is done embedding + """ await TikTokEmbedder(self.bot).process(ctx, links) @commands.command(aliases=["gif", "gfy"]) @@ -139,9 +241,15 @@ async def extract_scripts(session, url): scripts = [] tasks = [] if len(query.split(" ")) == 1: - tasks.append(extract_scripts(self.bot.session, f"https://gfycat.com/gifs/tag/{query}")) + tasks.append( + extract_scripts( + self.bot.session, f"https://gfycat.com/gifs/tag/{query}" + ) + ) - tasks.append(extract_scripts(self.bot.session, f"https://gfycat.com/gifs/search/{query}")) + tasks.append( + extract_scripts(self.bot.session, f"https://gfycat.com/gifs/search/{query}") + ) scripts = sum(await asyncio.gather(*tasks), []) urls = [] @@ -190,7 +298,9 @@ async def melon(self, ctx: commands.Context, timeframe): url = f"https://www.melon.com/chart/{timeframe}/index.htm" headers = { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0", + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0" + ), } async with self.bot.session.get(url, headers=headers) as response: text = await response.text() @@ -206,7 +316,9 @@ async def melon(self, ctx: commands.Context, timeframe): if not title or not artist: raise exceptions.CommandError("Failure parsing Melon page") - rows.append(f"`#{i:2}` **{artist.attrs['title']}** — ***{title.attrs['title']}***") + rows.append( + f"`#{i:2}` **{artist.attrs['title']}** — ***{title.attrs['title']}***" + ) content = discord.Embed(color=discord.Color.from_rgb(0, 205, 60)) content.set_author( @@ -229,7 +341,9 @@ async def xkcd(self, ctx: commands.Context, comic_id=None): "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Connection": "keep-alive", "Referer": "https://xkcd.com/", - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0", + "User-Agent": ( + "Mozilla/5.0 (X11; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0" + ), } async with self.bot.session.get(url, headers=headers) as response: location = str(response.url) @@ -252,26 +366,26 @@ async def run(self, ctx: commands.Context): self.message = await ctx.send(random.choice(self.gifs)["url"], view=self) @discord.ui.button(emoji=emojis.REMOVE, style=discord.ButtonStyle.danger) - async def toggle(self, interaction: discord.Interaction, _button: discord.ui.Button): + async def toggle( + self, interaction: discord.Interaction, _button: discord.ui.Button + ): await interaction.response.defer() await self.message.delete() @discord.ui.button(emoji=emojis.REPEAT, style=discord.ButtonStyle.primary) - async def randomize(self, interaction: discord.Interaction, _button: discord.ui.Button): + async def randomize( + self, interaction: discord.Interaction, _button: discord.ui.Button + ): await interaction.response.defer() await self.message.edit(content=random.choice(self.gifs)["url"]) @discord.ui.button(emoji=emojis.CONFIRM, style=discord.ButtonStyle.secondary) - async def confirm(self, interaction: discord.Interaction, _button: discord.ui.Button): + async def confirm( + self, interaction: discord.Interaction, _button: discord.ui.Button + ): await interaction.response.defer() await self.remove_ui() - # @discord.ui.button( - # label="Powered by GIPHY", style=discord.ButtonStyle.secondary, disabled=True - # ) - # async def giphy_label(self, _, __): - # pass - async def remove_ui(self): for item in self.children: item.disabled = True # type: ignore diff --git a/cogs/misc.py b/cogs/misc.py index 7258dd1..4452e85 100644 --- a/cogs/misc.py +++ b/cogs/misc.py @@ -16,10 +16,10 @@ from aiohttp import ClientResponseError from bs4 import BeautifulSoup from discord.ext import commands +from modules.misobot import MisoBot from PIL import Image, ImageDraw, ImageFont, UnidentifiedImageError from modules import emoji_literals, exceptions, util -from modules.misobot import MisoBot EMOJIFIER_HOST = os.environ.get("EMOJIFIER_HOST") @@ -116,7 +116,9 @@ async def rng(self, ctx: commands.Context, *, number_range): try: values = [int(x) for x in number_range.split("-")] except ValueError: - return await ctx.send(":warning: Please give a valid number range to choose from") + return await ctx.send( + ":warning: Please give a valid number range to choose from" + ) if len(values) == 2: start, end = values else: @@ -162,7 +164,9 @@ async def joke(self, ctx: commands.Context): @commands.command(aliases=["imbored"]) async def iambored(self, ctx: commands.Context): """Get something to do""" - async with self.bot.session.get("http://www.boredapi.com/api/activity/") as response: + async with self.bot.session.get( + "http://www.boredapi.com/api/activity/" + ) as response: data = await response.json(loads=orjson.loads) # https://www.boredapi.com/documentation @@ -258,7 +262,9 @@ async def ship(self, ctx: commands.Context, *, names): lovenums = newnums it = 0 - maxit = 100 # Maximum iterations allowed in below algorithm to attempt convergence + maxit = ( + 100 # Maximum iterations allowed in below algorithm to attempt convergence + ) maxlen = 100 # Maximum length of generated list allowed (some cases grow list infinitely) while len(lovenums) > 2 and it < maxit and len(lovenums) < maxlen: newnums = [] @@ -271,21 +277,50 @@ async def ship(self, ctx: commands.Context, *, names): newnums.extend((1, pairsum % 10)) lovenums = newnums - # This if-else matches with original site alg handling of non-convergent result. (i.e. defaulting to 1%) - # Technically, you can leave this section as it was previously and still get a non-trivial outputtable result since the length is always at least 2. + # This if-else matches with original site alg handling of + # non-convergent result. (i.e. defaulting to 1%) + # Technically, you can leave this section as it was previously + # and still get a non-trivial outputtable result since the length is always at least 2. percentage = lovenums[0] * 10 + lovenums[1] if len(lovenums) == 2 else 1 if percentage < 25: emoji = ":broken_heart:" - text = f"Dr. Love thinks a relationship might work out between {nameslist[0]} and {nameslist[1]}, but the chance is very small. A successful relationship is possible, but you both have to work on it. Do not sit back and think that it will all work out fine, because it might not be working out the way you wanted it to. Spend as much time with each other as possible. Again, the chance of this relationship working out is very small, so even when you do work hard on it, it still might not work out." + text = ( + f"Dr. Love thinks a relationship might work out between {nameslist[0]} " + f"and {nameslist[1]}, but the chance is very small. " + "A successful relationship is possible, but you both have to work on it. " + "Do not sit back and think that it will all work out fine, " + "because it might not be working out the way you wanted it to. " + "Spend as much time with each other as possible. Again, the chance of this " + "relationship working out is very small, so even when you do work hard on it, " + "it still might not work out." + ) elif percentage < 50: emoji = ":heart:" - text = f"The chance of a relationship working out between {nameslist[0]} and {nameslist[1]} is not very big, but a relationship is very well possible, if the two of you really want it to, and are prepared to make some sacrifices for it. You'll have to spend a lot of quality time together. You must be aware of the fact that this relationship might not work out at all, no matter how much time you invest in it." + text = ( + f"The chance of a relationship working out between {nameslist[0]} and " + f"{nameslist[1]} is not very big, but a relationship is very well possible, " + "if the two of you really want it to, and are prepared to make some sacrifices for " + "it. You'll have to spend a lot of quality time together. You must be aware of the " + "fact that this relationship might not work out at all, " + "no matter how much time you invest in it." + ) elif percentage < 75: emoji = ":heart:" - text = f"Dr. Love thinks that a relationship between {nameslist[0]} and {nameslist[1]} has a reasonable chance of working out, but on the other hand, it might not. Your relationship may suffer good and bad times. If things might not be working out as you would like them to, do not hesitate to talk about it with the person involved. Spend time together, talk with each other." + text = ( + f"Dr. Love thinks that a relationship between {nameslist[0]} and {nameslist[1]} " + "has a reasonable chance of working out, but on the other hand, it might not. " + "Your relationship may suffer good and bad times. If things might not be working " + "out as you would like them to, do not hesitate to talk about it with the person " + "involved. Spend time together, talk with each other." + ) else: emoji = ":sparkling_heart:" - text = f"Dr. Love thinks that a relationship between {nameslist[0]} and {nameslist[1]} has a very good chance of being successful, but this doesn't mean that you don't have to work on the relationship. Remember that every relationship needs spending time together, talking with each other etc." + text = ( + f"Dr. Love thinks that a relationship between {nameslist[0]} and {nameslist[1]} " + "has a very good chance of being successful, but this doesn't mean that you don't " + "have to work on the relationship. Remember that every relationship needs spending " + "time together, talking with each other etc." + ) content = discord.Embed( title=f"{nameslist[0]} {emoji} {nameslist[1]} - {percentage}%", @@ -410,7 +445,9 @@ async def horoscope_monthly(self, ctx: commands.Context): async def send_hs( self, ctx: commands.Context, - variant: Literal["daily-yesterday", "daily-today", "daily-tomorrow", "weekly", "monthly"], + variant: Literal[ + "daily-yesterday", "daily-today", "daily-tomorrow", "weekly", "monthly" + ], ): sunsign = await self.bot.db.fetch_value( "SELECT sunsign FROM user_settings WHERE user_id = %s", @@ -433,7 +470,9 @@ async def send_hs( paragraph = soup.select_one("p") if paragraph is None: - raise exceptions.CommandError("Something went wrong trying to get horoscope text") + raise exceptions.CommandError( + "Something went wrong trying to get horoscope text" + ) date_node = paragraph.find("strong") if date_node is not None: @@ -496,7 +535,9 @@ async def horoscope_set(self, ctx: commands.Context, sign): ctx.author.id, sign, ) - await ctx.send(f"Zodiac saved as **{sign.capitalize()}** {self.hs[sign]['emoji']}") + await ctx.send( + f"Zodiac saved as **{sign.capitalize()}** {self.hs[sign]['emoji']}" + ) @horoscope.command(name="list") async def horoscope_list(self, ctx: commands.Context): @@ -511,7 +552,9 @@ async def horoscope_list(self, ctx: commands.Context): ) return await ctx.send(embed=content) - @commands.command(aliases=["colour"], usage=" ...") + @commands.command( + aliases=["colour"], usage=" ..." + ) async def color( self, ctx: commands.Context, @@ -542,7 +585,9 @@ async def color( if next_is_random_count and isinstance(source, int): slots = 50 - len(colors) amount = min(source, slots) - colors += ["{:06x}".format(random.randint(0, 0xFFFFFF)) for _ in range(amount)] + colors += [ + "{:06x}".format(random.randint(0, 0xFFFFFF)) for _ in range(amount) + ] next_is_random_count = False # member or role color elif isinstance(source, (discord.Member, discord.Role)): @@ -734,7 +779,9 @@ async def stealsticker(self, ctx: commands.Context): fetched_sticker = await sticker.fetch() if not isinstance(fetched_sticker, discord.GuildSticker): - raise exceptions.CommandWarning("I cannot steal default discord stickers!") + raise exceptions.CommandWarning( + "I cannot steal default discord stickers!" + ) sticker_file = await fetched_sticker.to_file() @@ -755,7 +802,9 @@ async def stealsticker(self, ctx: commands.Context): await ctx.send(embed=content) @commands.command() - async def emojify(self, ctx: commands.Context, *, text: Union[discord.Message, str]): + async def emojify( + self, ctx: commands.Context, *, text: Union[discord.Message, str] + ): """Emojify your message Usage: @@ -782,7 +831,9 @@ async def emojify(self, ctx: commands.Context, *, text: Union[discord.Message, s try: await ctx.send(result) except discord.errors.HTTPException: - raise exceptions.CommandWarning("Your text once emojified is too long to send!") + raise exceptions.CommandWarning( + "Your text once emojified is too long to send!" + ) @commands.command() async def meme(self, ctx: commands.Context, template: str, *, content): @@ -895,14 +946,14 @@ def parse_emoji(self, emoji_str: str | int): elif custom_emoji_match := re.search(r"<(a?)?:(\w+):(\d+)>", emoji_str): # is a custom emoji animated, emoji_name, emoji_id = custom_emoji_match.groups() - my_emoji.url = ( - f"https://cdn.discordapp.com/emojis/{emoji_id}.{'gif' if animated else 'png'}" - ) + my_emoji.url = f"https://cdn.discordapp.com/emojis/{emoji_id}.{'gif' if animated else 'png'}" my_emoji.name = emoji_name my_emoji.animated = animated == "a" my_emoji.id = int(emoji_id) elif emoji_name := emoji_literals.UNICODE_TO_NAME.get(emoji_str): - codepoint = "-".join(f"{ord(e):x}" for e in emoji_literals.NAME_TO_UNICODE[emoji_name]) + codepoint = "-".join( + f"{ord(e):x}" for e in emoji_literals.NAME_TO_UNICODE[emoji_name] + ) my_emoji.name = emoji_name.strip(":") my_emoji.url = f"https://twemoji.maxcdn.com/v/13.0.1/72x72/{codepoint}.png" else: @@ -933,7 +984,7 @@ def __init__(self, filename): def get_text_size(self, font_size, text): font = ImageFont.truetype(self.font, font_size) - return font.getsize(text) + return font.getbbox(text)[2:4] def save(self, filename=None): self.image.save(filename or self.filename) @@ -965,7 +1016,8 @@ def write_box(self, x, y, width, height, color, text, angle=0): lines.append(line) if len(word.split("\n")) > 2: lines.extend( - [newline_words[i]] for i in range(1, len(word.split("\n")) - 1) + [newline_words[i]] + for i in range(1, len(word.split("\n")) - 1) ) line = [newline_words[-1]] else: diff --git a/cogs/mod.py b/cogs/mod.py index 8bfdfb6..a9393ef 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -8,9 +8,9 @@ import discord from discord.ext import commands, tasks from loguru import logger +from modules.misobot import MisoBot from modules import exceptions, util -from modules.misobot import MisoBot class Mod(commands.Cog): @@ -44,7 +44,10 @@ async def check_mutes(self): if self.cache_needs_refreshing: self.cache_needs_refreshing = False self.unmute_list = await self.bot.db.fetch( - "SELECT user_id, guild_id, channel_id, unmute_on FROM muted_user WHERE unmute_on IS NOT NULL" + """ + SELECT user_id, guild_id, channel_id, unmute_on + FROM muted_user WHERE unmute_on IS NOT NULL + """ ) if not self.unmute_list: @@ -56,42 +59,6 @@ async def check_mutes(self): if unmute_ts > now_ts: continue - guild = self.bot.get_guild(guild_id) - if guild is None: - continue - await util.require_chunked(guild) - if user := guild.get_member(user_id): - mute_role_id = await self.bot.db.fetch_value( - """ - SELECT mute_role_id FROM guild_settings WHERE guild_id = %s - """, - guild.id, - ) - mute_role = guild.get_role(mute_role_id) if mute_role_id else None - if not mute_role: - return logger.warning("Mute role not set in unmuting loop") - channel = self.bot.get_partial_messageable(channel_id, guild_id=guild.id) - if channel is not None: - try: - await user.remove_roles(mute_role) - except discord.errors.Forbidden: - pass - try: - await channel.send( - embed=discord.Embed( - description=f":stopwatch: Unmuted {user.mention} (mute duration passed)", - color=int("66757f", 16), - ) - ) - except discord.errors.Forbidden: - logger.warning( - "Unable to send unmuting message due to missing permissions!" - ) - else: - logger.info( - f"Deleted expired mute of unknown user {user_id} or unknown guild {guild_id}" - ) - await self.bot.db.execute( """ DELETE FROM muted_user @@ -102,6 +69,48 @@ async def check_mutes(self): ) self.cache_needs_refreshing = True + guild = self.bot.get_guild(guild_id) + if guild is None: + logger.info(f"Deleted expired mute in unknown guild {guild_id}") + continue + + await util.require_chunked(guild) + user = guild.get_member(user_id) + if not user: + logger.info(f"Deleted expired mute of unknown user {user_id}") + continue + + mute_role_id = await self.bot.db.fetch_value( + """ + SELECT mute_role_id FROM guild_settings WHERE guild_id = %s + """, + guild.id, + ) + mute_role = guild.get_role(mute_role_id) if mute_role_id else None + if not mute_role: + logger.warning(f"Mute role not set in unmuting loop for {guild}") + continue + + try: + await user.remove_roles(mute_role) + except discord.errors.Forbidden: + pass + channel = self.bot.get_partial_messageable(channel_id, guild_id=guild.id) + if channel is None: + continue + + try: + await channel.send( + embed=discord.Embed( + description=f":stopwatch: Unmuted {user.mention} (mute duration passed)", + color=int("66757f", 16), + ) + ) + except discord.errors.Forbidden: + logger.warning( + "Unable to send unmuting message due to missing permissions!" + ) + @commands.command(aliases=["clean"], usage=" [@mentions...]") @commands.guild_only() @commands.has_permissions(manage_messages=True) @@ -118,7 +127,9 @@ async def purge(self, ctx: commands.Context, amount: int): raise exceptions.CommandWarning("This command cannot be used here.") if amount > 100: - raise exceptions.CommandWarning("You cannot delete more than 100 messages at a time.") + raise exceptions.CommandWarning( + "You cannot delete more than 100 messages at a time." + ) await ctx.message.delete() @@ -147,7 +158,10 @@ async def purge(self, ctx: commands.Context, amount: int): @commands.guild_only() @commands.has_permissions(manage_roles=True) async def giverole( - self, ctx: commands.Context, role: discord.Role, members: commands.Greedy[discord.Member] + self, + ctx: commands.Context, + role: discord.Role, + members: commands.Greedy[discord.Member], ): """Give a role to multiple people""" success = [] @@ -165,31 +179,39 @@ async def giverole( @commands.command() @commands.guild_only() @commands.has_permissions(moderate_members=True) - async def timeout(self, ctx: commands.Context, member: discord.Member, *, duration="1 hour"): + async def timeout( + self, ctx: commands.Context, member: discord.Member, *, duration="1 hour" + ): """Timeout user. Pass 'remove' as the duration to remove""" if member.is_timed_out(): if duration and duration.strip().lower() == "remove": await member.timeout(None) - return await util.send_success(ctx, f"Removed timeout from {member.mention}") + return await util.send_success( + ctx, f"Removed timeout from {member.mention}" + ) + seconds = member.timeout.timestamp() - arrow.now().int_timestamp raise exceptions.CommandInfo( - f"{member.mention} is already timed out (**{util.stringfromtime(seconds)}** remaining)", + f"{member.mention} is already timed out " + f"(**{util.stringfromtime(seconds)}** remaining)", ) - else: - seconds = util.timefromstring(duration) - if seconds is None: - raise exceptions.CommandWarning(f"Invalid duration `{duration}`") - until = arrow.now().shift(seconds=+seconds).datetime - await member.timeout(until) - await util.send_success( - ctx, f"Timed out {member.mention} for **{util.stringfromtime(seconds)}**" - ) + seconds = util.timefromstring(duration) + if seconds is None: + raise exceptions.CommandWarning(f"Invalid duration `{duration}`") + until = arrow.now().shift(seconds=+seconds).datetime + + await member.timeout(until) + await util.send_success( + ctx, f"Timed out {member.mention} for **{util.stringfromtime(seconds)}**" + ) @commands.command() @commands.guild_only() @commands.has_permissions(manage_roles=True) - async def mute(self, ctx: commands.Context, member: discord.Member, *, duration=None): + async def mute( + self, ctx: commands.Context, member: discord.Member, *, duration=None + ): """Mute user""" if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") @@ -218,10 +240,14 @@ async def mute(self, ctx: commands.Context, member: discord.Member, *, duration= raise exceptions.CommandWarning(f'Invalid mute duration "{duration}"') if seconds < 60: - raise exceptions.CommandInfo("The minimum duration of a mute is **1 minute**") + raise exceptions.CommandInfo( + "The minimum duration of a mute is **1 minute**" + ) if seconds > 604800: - raise exceptions.CommandInfo("The maximum duration of a mute is **1 week**") + raise exceptions.CommandInfo( + "The maximum duration of a mute is **1 week**" + ) try: await member.add_roles(mute_role) @@ -233,7 +259,11 @@ async def mute(self, ctx: commands.Context, member: discord.Member, *, duration= await util.send_success( ctx, f"Muted {member.mention}" - + (f" for **{util.stringfromtime(seconds)}**" if seconds is not None else ""), + + ( + f" for **{util.stringfromtime(seconds)}**" + if seconds is not None + else "" + ), ) if seconds is not None: @@ -355,7 +385,10 @@ async def fastban(self, ctx: commands.Context, *discord_users): success.append(f"`{user}` Banned :hammer:") await util.send_tasks_result_list( - ctx, success, failure, f":hammer: Attempting to ban {len(discord_users)} users..." + ctx, + success, + failure, + f":hammer: Attempting to ban {len(discord_users)} users...", ) @commands.command() @@ -370,7 +403,8 @@ async def ban(self, ctx: commands.Context, *discord_users): if len(discord_users) > 4: raise exceptions.CommandInfo( - f"It seems you are trying to ban a lot of users at once.\nPlease use `{ctx.prefix}massban ...` instead" + f"It seems you are trying to ban a lot of users at once.\n" + f"Please use `{ctx.prefix}massban ...` instead" ) for discord_user in discord_users: @@ -400,7 +434,10 @@ async def ban(self, ctx: commands.Context, *discord_users): except discord.errors.Forbidden: await ctx.send( embed=discord.Embed( - description=f":no_entry: It seems I don't have the permission to ban **{user}**", + description=( + ":no_entry: It seems I don't have the " + f"permission to ban **{user}**" + ), color=int("be1931", 16), ) ) @@ -422,7 +459,9 @@ async def ban(self, ctx: commands.Context, *discord_users): @staticmethod async def send_ban_confirmation(ctx: commands.Context, user): content = discord.Embed(title=":hammer: Ban user?", color=int("f4900c", 16)) - content.description = f"{user.mention}\n**{user.name}#{user.discriminator}**\n{user.id}" + content.description = ( + f"{user.mention}\n**{user.name}#{user.discriminator}**\n{user.id}" + ) msg = await ctx.send(embed=content) async def confirm_ban(): @@ -432,7 +471,10 @@ async def confirm_ban(): content.title = ":white_check_mark: Banned user" except discord.errors.Forbidden: content.title = None - content.description = f":no_entry: It seems I don't have the permission to ban **{user}** {user.mention}" + content.description = ( + ":no_entry: It seems I don't have the permission to " + f"ban **{user}** {user.mention}" + ) content.colour = int("be1931", 16) await msg.edit(embed=content) @@ -442,7 +484,9 @@ async def cancel_ban(): functions = {"✅": confirm_ban, "❌": cancel_ban} asyncio.ensure_future( - util.reaction_buttons(ctx, msg, functions, only_author=True, single_use=True) + util.reaction_buttons( + ctx, msg, functions, only_author=True, single_use=True + ) ) @commands.command() @@ -469,9 +513,13 @@ async def unban(self, ctx: commands.Context, *discord_users): f"It seems I don't have the permission to unban **{user}** {user.mention}" ) except discord.errors.NotFound: - raise exceptions.CommandWarning(f"Unable to unban. **{user}** is not banned") + raise exceptions.CommandWarning( + f"Unable to unban. **{user}** is not banned" + ) else: - return await util.send_success(ctx, f"Unbanned **{user}** {user.mention}") + return await util.send_success( + ctx, f"Unbanned **{user}** {user.mention}" + ) success = [] failure = [] @@ -494,7 +542,10 @@ async def unban(self, ctx: commands.Context, *discord_users): success.append(f"`{user}` Unbanned") await util.send_tasks_result_list( - ctx, success, failure, f":memo: Attempting to unban {len(discord_users)} users..." + ctx, + success, + failure, + f":memo: Attempting to unban {len(discord_users)} users...", ) diff --git a/cogs/notifications.py b/cogs/notifications.py index 87bd8d9..80787b2 100644 --- a/cogs/notifications.py +++ b/cogs/notifications.py @@ -9,9 +9,9 @@ import regex from discord.ext import commands from loguru import logger +from modules.misobot import MisoBot from modules import emojis, exceptions, queries, util -from modules.misobot import MisoBot class Notifications(commands.Cog): @@ -54,9 +54,15 @@ async def send_notification( return content = discord.Embed(color=message.author.color) - content.set_author(name=f"{message.author}", icon_url=message.author.display_avatar.url) - pattern = regex.compile(self.keyword_regex, words=keywords, flags=regex.IGNORECASE) - highlighted_text = regex.sub(pattern, lambda x: f"**{x.group(0)}**", message.content) + content.set_author( + name=f"{message.author}", icon_url=message.author.display_avatar.url + ) + pattern = regex.compile( + self.keyword_regex, words=keywords, flags=regex.IGNORECASE + ) + highlighted_text = regex.sub( + pattern, lambda x: f"**{x.group(0)}**", message.content + ) content.description = highlighted_text[:2047] content.add_field( @@ -134,7 +140,12 @@ async def on_message(self, message: discord.Message): f"User {user_id} not found, deleting their notification for {users_words}" ) await self.bot.db.execute( - """DELETE FROM notification WHERE guild_id = %s AND user_id = %s AND keyword IN %s""", + """ + DELETE FROM notification + WHERE guild_id = %s + AND user_id = %s + AND keyword IN %s + """, message.guild.id, user_id, users_words, @@ -142,8 +153,13 @@ async def on_message(self, message: discord.Message): await self.create_cache() continue - if member is not None and message.channel.permissions_for(member).read_messages: - asyncio.ensure_future(self.send_notification(member, message, users_words)) + if ( + member is not None + and message.channel.permissions_for(member).read_messages + ): + asyncio.ensure_future( + self.send_notification(member, message, users_words) + ) @commands.group(case_insensitive=True, aliases=["noti", "notif", "notifications"]) async def notification(self, ctx: commands.Context): @@ -165,7 +181,8 @@ async def notification_add(self, ctx: commands.Context, *, keyword: str): ) if amount and amount >= 25: raise exceptions.CommandWarning( - f"You can only have a maximum of **25** notifications. You have **{amount}** (Become a [donator](https://misobot.xyz/donate) for unlimited notifications)" + f"You can only have a maximum of **25** notifications. You have **{amount}** " + "(Become a [donator](https://misobot.xyz/donate) for unlimited notifications)" ) try: @@ -208,7 +225,9 @@ async def notification_add(self, ctx: commands.Context, *, keyword: str): # remake notification cache await self.create_cache() - await util.send_success(ctx, f"New notification set! Check your DM {emojis.VIVISMIRK}") + await util.send_success( + ctx, f"New notification set! Check your DM {emojis.VIVISMIRK}" + ) @notification.command(name="remove") async def notification_remove(self, ctx: commands.Context, *, keyword: str): @@ -239,7 +258,8 @@ async def notification_remove(self, ctx: commands.Context, *, keyword: str): try: await util.send_success( ctx.author, - f"The keyword notification for `{keyword}` that you set in **{ctx.guild.name}** has been removed.", + f"The keyword notification for `{keyword}` that you set in " + f"**{ctx.guild.name}** has been removed.", ) except discord.errors.Forbidden: raise exceptions.CommandWarning( @@ -257,14 +277,18 @@ async def notification_remove(self, ctx: commands.Context, *, keyword: str): # remake notification cache await self.create_cache() - await util.send_success(ctx, f"Removed a notification! Check your DM {emojis.VIVISMIRK}") + await util.send_success( + ctx, f"Removed a notification! Check your DM {emojis.VIVISMIRK}" + ) @notification.command(name="list") async def notification_list(self, ctx: commands.Context): """List your current notifications""" words = await self.bot.db.fetch( """ - SELECT guild_id, keyword, times_triggered FROM notification WHERE user_id = %s ORDER BY keyword + SELECT guild_id, keyword, times_triggered + FROM notification WHERE user_id = %s + ORDER BY keyword """, ctx.author.id, ) @@ -283,17 +307,23 @@ async def notification_list(self, ctx: commands.Context): if guild is None: guild = f"[Unknown server `{guild_id}`]" - rows.append(f"**{guild}** : `{keyword}` - Triggered **{times_triggered}** times") + rows.append( + f"**{guild}** : `{keyword}` - Triggered **{times_triggered}** times" + ) try: - await util.send_as_pages(ctx, content, rows, maxpages=1, maxrows=50, send_to=ctx.author) + await util.send_as_pages( + ctx, content, rows, maxpages=1, maxrows=50, send_to=ctx.author + ) except discord.errors.Forbidden: raise exceptions.CommandWarning( "I was unable to send you a DM! Please change your settings." ) if ctx.guild is not None: - await util.send_success(ctx, f"Notification list sent to your DM {emojis.VIVISMIRK}") + await util.send_success( + ctx, f"Notification list sent to your DM {emojis.VIVISMIRK}" + ) @notification.command(name="clear") async def notification_clear(self, ctx: commands.Context): @@ -308,7 +338,9 @@ async def notification_clear(self, ctx: commands.Context): """, ctx.author.id, ) - await util.send_success(ctx, "Cleared all of your notifications in all servers!") + await util.send_success( + ctx, "Cleared all of your notifications in all servers!" + ) else: await self.bot.db.execute( """ @@ -317,7 +349,9 @@ async def notification_clear(self, ctx: commands.Context): ctx.author.id, ctx.guild.id, ) - await util.send_success(ctx, "Cleared all of your notifications in this server!") + await util.send_success( + ctx, "Cleared all of your notifications in this server!" + ) # remake notification cache await self.create_cache() @@ -352,7 +386,9 @@ async def notification_test( ctx.author.id, ) - pattern = regex.compile(self.keyword_regex, words=keywords, flags=regex.IGNORECASE) + pattern = regex.compile( + self.keyword_regex, words=keywords, flags=regex.IGNORECASE + ) if finds := pattern.findall(message.content): keywords = list(set(finds)) diff --git a/cogs/owner.py b/cogs/owner.py index 5529eb7..e9a651e 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -8,9 +8,9 @@ import discord from discord.ext import commands from loguru import logger +from modules.misobot import MisoBot from modules import util -from modules.misobot import MisoBot class Owner(commands.Cog): @@ -34,7 +34,9 @@ async def say( ): """Makes the bot say something in the given channel""" channel = self.bot.get_partial_messageable(channel_id) - await ctx.send(f"Sending message to **{channel.guild}** <#{channel.id}>\n> {message}") + await ctx.send( + f"Sending message to **{channel.guild}** <#{channel.id}>\n> {message}" + ) await channel.send(message) @commands.command(rest_is_raw=True) @@ -58,7 +60,9 @@ async def guilds(self, ctx: commands.Context): rows = [ f"[`{guild.id}`] **{guild.member_count}** members : **{guild.name}**" - for guild in sorted(self.bot.guilds, key=lambda x: x.member_count or 0, reverse=True) + for guild in sorted( + self.bot.guilds, key=lambda x: x.member_count or 0, reverse=True + ) ] await util.send_as_pages(ctx, content, rows) @@ -67,22 +71,32 @@ async def findguild(self, ctx: commands.Context, *, search_term): """Find a guild by name""" rows = [ f"[`{guild.id}`] **{guild.member_count}** members : **{guild.name}**" - for guild in sorted(self.bot.guilds, key=lambda x: x.member_count or 0, reverse=True) + for guild in sorted( + self.bot.guilds, key=lambda x: x.member_count or 0, reverse=True + ) if search_term.lower() in guild.name.lower() ] - content = discord.Embed(title=f"Found **{len(rows)}** guilds matching search term") + content = discord.Embed( + title=f"Found **{len(rows)}** guilds matching search term" + ) await util.send_as_pages(ctx, content, rows) @commands.command() async def userguilds(self, ctx: commands.Context, user: discord.User): """Get all guilds user is part of""" rows = [] - for guild in sorted(self.bot.guilds, key=lambda x: x.member_count or 0, reverse=True): + for guild in sorted( + self.bot.guilds, key=lambda x: x.member_count or 0, reverse=True + ): guildmember = guild.get_member(user.id) if guildmember is not None: - rows.append(f"[`{guild.id}`] **{guild.member_count}** members : **{guild.name}**") + rows.append( + f"[`{guild.id}`] **{guild.member_count}** members : **{guild.name}**" + ) - content = discord.Embed(title=f"User **{user}** found in **{len(rows)}** guilds") + content = discord.Embed( + title=f"User **{user}** found in **{len(rows)}** guilds" + ) await util.send_as_pages(ctx, content, rows) @commands.command() @@ -109,7 +123,12 @@ async def donator(self, ctx: commands.Context): @donator.command(name="addsingle") async def donator_addsingle( - self, ctx: commands.Context, user: discord.User, platform, amount: float, ts=None + self, + ctx: commands.Context, + user: discord.User, + platform, + amount: float, + ts=None, ): """Add a new single time donation""" ts = arrow.utcnow().datetime if ts is None else arrow.get(ts).datetime @@ -127,7 +146,14 @@ async def donator_addsingle( @donator.command(name="add") async def donator_add( - self, ctx, user: discord.User, username, platform, tier: int, amount: int, since_ts=None + self, + ctx, + user: discord.User, + username, + platform, + tier: int, + amount: int, + since_ts=None, ): """Add a new monthly donator""" if since_ts is None: @@ -137,7 +163,8 @@ async def donator_add( await self.bot.db.execute( """ - INSERT INTO donator (user_id, platform, external_username, donation_tier, donating_since, amount) + INSERT INTO donator (user_id, platform, external_username, + donation_tier, donating_since, amount) VALUES (%s, %s, %s, %s, %s, %s) """, user.id, @@ -175,7 +202,9 @@ async def donator_toggle(self, ctx: commands.Context, user: discord.User): await util.send_success(ctx, f"**{user}** donator status changed.") @donator.command(name="tier") - async def donator_tier(self, ctx: commands.Context, user: discord.User, new_tier: int): + async def donator_tier( + self, ctx: commands.Context, user: discord.User, new_tier: int + ): """Change user's donation tier""" await self.bot.db.execute( """ @@ -184,7 +213,9 @@ async def donator_tier(self, ctx: commands.Context, user: discord.User, new_tier new_tier, user.id, ) - await util.send_success(ctx, f"**{user}** donation changed to **Tier {new_tier}**") + await util.send_success( + ctx, f"**{user}** donation changed to **Tier {new_tier}**" + ) @commands.command(name="db", aliases=["dbe", "dbq"]) @commands.is_owner() diff --git a/cogs/roles.py b/cogs/roles.py index c27a116..5b1d01c 100644 --- a/cogs/roles.py +++ b/cogs/roles.py @@ -5,11 +5,11 @@ import asyncio import discord +from cogs.errorhandler import ErrorHandler from discord.ext import commands +from modules.misobot import MisoBot -from cogs.errorhandler import ErrorHander from modules import emojis, exceptions, queries, util -from modules.misobot import MisoBot class Roles(commands.Cog): @@ -98,7 +98,9 @@ async def confirm(): ctx.guild.id, # type: ignore ) - content.title = f":white_check_mark: Deleted all {len(matching_roles)} color roles" + content.title = ( + f":white_check_mark: Deleted all {len(matching_roles)} color roles" + ) content.description = "" content.color = int("77b255", 16) await msg.edit(content=None, embed=content) @@ -123,14 +125,15 @@ async def cancel(): async def baserole(self, ctx: commands.Context, role: discord.Role): """Set the base role to inherit permissions and position from - You should set this to something lower than the bot but high enough for the color to show up. + You should set this to something lower than the bot + but high enough for the color to show up. """ if ctx.guild is None or isinstance(ctx.author, discord.User): raise exceptions.CommandError("Unable to get current guild") if role > ctx.author.top_role: raise exceptions.CommandWarning( - "You cannot set the colorizer baserole to something higher than you in the hierarchy" + "You cannot set the colorizer baserole to a role higher than you in the hierarchy" ) await self.bot.db.execute( @@ -144,7 +147,8 @@ async def baserole(self, ctx: commands.Context, role: discord.Role): role.id, ) await util.send_success( - ctx, f"New color roles will now inherit permissions and position from {role.mention}" + ctx, + f"New color roles will now inherit permissions and position from {role.mention}", ) @commands.guild_only() @@ -198,10 +202,16 @@ async def colorme(self, ctx: commands.Context, hex_color: str): color_role = None if existing_roles is not None: existing_role_id: int | None = dict(existing_roles).get(str(color)) - color_role = ctx.guild.get_role(existing_role_id) if existing_role_id else None + color_role = ( + ctx.guild.get_role(existing_role_id) if existing_role_id else None + ) - if old_roles := list(filter(lambda r: r.id in existing_roles_ids, ctx.author.roles)): - await ctx.author.remove_roles(*old_roles, atomic=True, reason="Changed color") + if old_roles := list( + filter(lambda r: r.id in existing_roles_ids, ctx.author.roles) + ): + await ctx.author.remove_roles( + *old_roles, atomic=True, reason="Changed color" + ) # remove manually deleted roles for role_id in existing_roles_ids: @@ -251,7 +261,11 @@ async def colorme(self, ctx: commands.Context, hex_color: str): final = before_colors + colors + acquired payload = [{"id": role.id, "position": i} for i, role in enumerate(final)] - await self.bot.http.move_role_position(ctx.guild.id, payload, reason="movin") # type: ignore + await self.bot.http.move_role_position( + ctx.guild.id, + payload, + reason="Colorizer action", + ) # type: ignore # color the user await ctx.author.add_roles(color_role) @@ -265,7 +279,8 @@ async def colorme(self, ctx: commands.Context, hex_color: str): # clean up any roles that are left with 0 users unused_roles = filter( - lambda r: r.id in [x[1] for x in existing_roles or []] and len(r.members) == 0, + lambda r: r.id in [x[1] for x in existing_roles or []] + and len(r.members) == 0, ctx.guild.roles, ) for role in unused_roles: @@ -331,9 +346,13 @@ async def rolepicker_remove(self, ctx: commands.Context, *, name): ) @rolepicker.command(name="channel") - async def rolepicker_channel(self, ctx: commands.Context, channel: discord.TextChannel): + async def rolepicker_channel( + self, ctx: commands.Context, channel: discord.TextChannel + ): """Set the channel you want to add and remove roles in""" - await queries.update_setting(ctx, "rolepicker_settings", "channel_id", channel.id) + await queries.update_setting( + ctx, "rolepicker_settings", "channel_id", channel.id + ) self.bot.cache.rolepickers.add(channel.id) await util.send_success( ctx, @@ -361,7 +380,9 @@ async def rolepicker_list(self, ctx: commands.Context): title=f":scroll: Available roles in {ctx.guild.name}", color=int("ffd983", 16), ) - if rows := [f"`{role_name}` : <@&{role_id}>" for role_name, role_id in sorted(data)]: + if rows := [ + f"`{role_name}` : <@&{role_id}>" for role_name, role_id in sorted(data) + ]: await util.send_as_pages(ctx, content, rows) else: content.description = "Nothing yet!" @@ -371,7 +392,9 @@ async def rolepicker_list(self, ctx: commands.Context): async def rolepicker_enabled(self, ctx: commands.Context, value: bool): """Enable or disable the rolepicker""" await queries.update_setting(ctx, "rolepicker_settings", "is_enabled", value) - await util.send_success(ctx, f"Rolepicker is now **{'enabled' if value else 'disabled'}**") + await util.send_success( + ctx, f"Rolepicker is now **{'enabled' if value else 'disabled'}**" + ) @commands.Cog.listener() async def on_message(self, message: discord.Message): @@ -405,8 +428,8 @@ async def on_message(self, message: discord.Message): command = message.content[0] rolename = message.content[1:].strip() - errorhandler = self.bot.get_cog("ErrorHander") - if not isinstance(errorhandler, ErrorHander): + errorhandler = self.bot.get_cog("ErrorHandler") + if not isinstance(errorhandler, ErrorHandler): return message.channel.send("Internal Error: Could not get ErrorHandler") if command in ["+", "-"]: @@ -425,7 +448,9 @@ async def on_message(self, message: discord.Message): try: await message.author.add_roles(role) except discord.errors.Forbidden: - await message.reply(":warning: I don't have permission to give you this role!") + await message.reply( + ":warning: I don't have permission to give you this role!" + ) else: await message.channel.send( embed=discord.Embed( @@ -449,7 +474,8 @@ async def on_message(self, message: discord.Message): ) else: await message.reply( - f":warning: Unknown action `{command}`. Use `+name` to add roles and `-name` to remove them." + f":warning: Unknown action `{command}`. " + "Use `+name` to add roles and `-name` to remove them." ) await asyncio.sleep(5) diff --git a/cogs/typings.py b/cogs/typings.py index 8a63440..4350557 100644 --- a/cogs/typings.py +++ b/cogs/typings.py @@ -10,9 +10,9 @@ import arrow import discord from discord.ext import commands +from modules.misobot import MisoBot from modules import exceptions, util -from modules.misobot import MisoBot class Typings(commands.Cog): @@ -33,7 +33,9 @@ def obfuscate(self, text): return "".join(letter_dict.get(letter, letter) for letter in text) def anticheat(self, message): - remainder = "".join(set(message.content).intersection(self.font + "".join(self.separators))) + remainder = "".join( + set(message.content).intersection(self.font + "".join(self.separators)) + ) return remainder != "" @commands.group() @@ -42,7 +44,9 @@ async def typing(self, ctx: commands.Context): await util.command_group_help(ctx) @typing.command(name="test") - async def typing_test(self, ctx: commands.Context, language=None, wordcount: int = 25): + async def typing_test( + self, ctx: commands.Context, language=None, wordcount: int = 25 + ): """Take a typing test""" if language is None: language = wordcount @@ -65,7 +69,9 @@ async def typing_test(self, ctx: commands.Context, language=None, wordcount: int f"Currently supported languages are:\n>>> {langs}" ) - words_message = await ctx.reply(f"```\n{self.obfuscate(' '.join(wordlist))}\n```") + words_message = await ctx.reply( + f"```\n{self.obfuscate(' '.join(wordlist))}\n```" + ) def check(_message): return _message.author == ctx.author and _message.channel == ctx.channel @@ -76,7 +82,9 @@ def check(_message): return await ctx.send(f"{ctx.author.mention} Too slow.") else: - wpm, accuracy, not_long_enough = calculate_entry(message, words_message, wordlist) + wpm, accuracy, not_long_enough = calculate_entry( + message, words_message, wordlist + ) if self.anticheat(message) or wpm > 300: return await message.reply("Stop cheating >:(") @@ -91,7 +99,9 @@ def check(_message): ) @typing.command(name="race") - async def typing_race(self, ctx: commands.Context, language=None, wordcount: int = 25): + async def typing_race( + self, ctx: commands.Context, language=None, wordcount: int = 25 + ): """Challenge your friends into a typing race""" if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") @@ -126,7 +136,9 @@ async def typing_race(self, ctx: commands.Context, language=None, wordcount: int "React with :white_check_mark: to start the race." ) - content.add_field(name="Participants", value=f"**{util.displayname(ctx.author)}**") + content.add_field( + name="Participants", value=f"**{util.displayname(ctx.author)}**" + ) enter_message = await ctx.send(embed=content) note_emoji = "🗒" @@ -147,11 +159,15 @@ def check(_reaction, _user): while not race_in_progress: try: - reaction, user = await ctx.bot.wait_for("reaction_add", timeout=120.0, check=check) + reaction, user = await ctx.bot.wait_for( + "reaction_add", timeout=120.0, check=check + ) except asyncio.TimeoutError: try: for emoji in [note_emoji, check_emoji]: - asyncio.ensure_future(enter_message.remove_reaction(emoji, ctx.bot.user)) + asyncio.ensure_future( + enter_message.remove_reaction(emoji, ctx.bot.user) + ) except (discord.errors.NotFound, discord.errors.Forbidden): pass break @@ -171,13 +187,8 @@ def check(_reaction, _user): if len(players) < 2: cant_race_alone = await ctx.send("You can't race alone!") await asyncio.sleep(1) - try: - await cant_race_alone.delete() - await enter_message.remove_reaction(check_emoji, user) - except discord.errors.Forbidden: - await ctx.send( - "`error: i'm missing required discord permission [ manage messages ]`" - ) + await cant_race_alone.delete() + await enter_message.remove_reaction(check_emoji, user) else: race_in_progress = True else: @@ -199,7 +210,9 @@ def check(_reaction, _user): await asyncio.sleep(1) await words_message.delete() - words_message = await ctx.send(f"```\n{self.obfuscate(' '.join(wordlist))}\n```") + words_message = await ctx.send( + f"```\n{self.obfuscate(' '.join(wordlist))}\n```" + ) tasks = [] for player in players: @@ -211,16 +224,23 @@ def check(_reaction, _user): results = await asyncio.gather(*tasks) - content = discord.Embed(title=":checkered_flag: Race complete!", color=int("e1e8ed", 16)) + content = discord.Embed( + title=":checkered_flag: Race complete!", color=int("e1e8ed", 16) + ) rows = [] values = [] + player: discord.Member for i, (player, wpm, accuracy) in enumerate( sorted(results, key=itemgetter(1), reverse=True), start=1 ): values.append((ctx.guild.id, player.id, 1, 1 if i == 1 else 0)) rows.append( f"{f'`#{i}`' if i > 1 else ':trophy:'} **{util.displayname(player)}** — " - + (f"**{int(wpm)} WPM / {int(accuracy)}% Accuracy**" if wpm != 0 else ":x:") + + ( + f"**{int(wpm)} WPM / {int(accuracy)}% Accuracy**" + if wpm != 0 + else ":x:" + ) ) await self.bot.db.executemany( @@ -243,12 +263,16 @@ def progress_check(_message): return _message.author == player and _message.channel == ctx.channel try: - message = await self.bot.wait_for("message", timeout=300.0, check=progress_check) + message = await self.bot.wait_for( + "message", timeout=300.0, check=progress_check + ) except asyncio.TimeoutError: await ctx.send(f"{player.mention} too slow!") return player, 0, 0 else: - wpm, accuracy, not_long_enough = calculate_entry(message, words_message, wordlist) + wpm, accuracy, not_long_enough = calculate_entry( + message, words_message, wordlist + ) if self.anticheat(message) or wpm > 300: await message.reply("Stop cheating >:(") return player, 0, 0 @@ -259,7 +283,15 @@ def progress_check(_message): ) return player, 0, 0 await message.add_reaction("✅") - await self.save_wpm(message.author, ctx.guild, wpm, accuracy, wordcount, language, True) + await self.save_wpm( + message.author, + ctx.guild, + wpm, + accuracy, + wordcount, + language, + True, + ) return player, wpm, accuracy @typing.command(name="history") @@ -284,12 +316,13 @@ async def typing_history( ) content = discord.Embed( - title=f":stopwatch: {util.displayname(member)} Typing test history", + title=f":stopwatch: {member.display_name} Typing test history", color=int("dd2e44", 16), ) content.set_footer(text=f"Total {len(data)} typing tests taken") rows = [ - f"**{wpm}** WPM, **{int(accuracy)}%** ACC, **{word_count}** words, *{test_language}* ({arrow.get(test_date).to('utc').humanize()})" + f"**{wpm}** WPM, **{int(accuracy)}%** ACC, **{word_count}** words, " + f"*{test_language}* ({arrow.get(test_date).to('utc').humanize()})" for test_date, wpm, accuracy, word_count, test_language in data ] await util.send_as_pages(ctx, content, rows) @@ -297,10 +330,10 @@ async def typing_history( @typing.command(name="cleardata") async def typing_clear(self, ctx: commands.Context): """Clear your typing data""" - content = discord.Embed(title=":warning: Are you sure?", color=int("ffcc4d", 16)) - content.description = ( - "This action will delete *all* of your saved typing data and is **irreversible**." + content = discord.Embed( + title=":warning: Are you sure?", color=int("ffcc4d", 16) ) + content.description = "This action will delete *all* of your saved typing data and is **irreversible**." msg = await ctx.send(embed=content) async def confirm(): @@ -329,7 +362,9 @@ async def cancel(): functions = {"✅": confirm, "❌": cancel} asyncio.ensure_future( - util.reaction_buttons(ctx, msg, functions, only_author=True, single_use=True) + util.reaction_buttons( + ctx, msg, functions, only_author=True, single_use=True + ) ) @typing.command(name="stats") diff --git a/cogs/user.py b/cogs/user.py index 79cf027..d41d884 100644 --- a/cogs/user.py +++ b/cogs/user.py @@ -10,9 +10,9 @@ import discord import humanize from discord.ext import commands +from modules.misobot import MisoBot from modules import emojis, exceptions, queries, util -from modules.misobot import MisoBot class User(commands.Cog): @@ -28,7 +28,10 @@ def __init__(self, bot): @commands.command(aliases=["dp", "av", "pfp"]) async def avatar( - self, ctx: commands.Context, *, user: Union[discord.Member, discord.User, None] = None + self, + ctx: commands.Context, + *, + user: Union[discord.Member, discord.User, None] = None, ): """Get user's profile picture""" if ctx.guild is None: @@ -80,7 +83,10 @@ async def switch(): @commands.command(aliases=["uinfo"]) @commands.cooldown(3, 30, type=commands.BucketType.user) async def userinfo( - self, ctx: commands.Context, *, user: Union[discord.Member, discord.User, None] = None + self, + ctx: commands.Context, + *, + user: Union[discord.Member, discord.User, None] = None, ): """Get information about discord user""" if ctx.guild is None: @@ -102,7 +108,9 @@ async def userinfo( content.add_field(name="Badges", value=" ".join(user_badges + other_badges)) content.add_field(name="Mention", value=user.mention) - content.add_field(name="Account created", value=user.created_at.strftime("%d/%m/%Y %H:%M")) + content.add_field( + name="Account created", value=user.created_at.strftime("%d/%m/%Y %H:%M") + ) if isinstance(user, discord.Member): content.colour = user.color @@ -110,29 +118,41 @@ async def userinfo( member_number = 1 + sum( 1 for member in ctx.guild.members - if member.joined_at and user.joined_at and member.joined_at < user.joined_at + if member.joined_at + and user.joined_at + and member.joined_at < user.joined_at ) boosting_date = None if user.premium_since: - boosting_date = humanize.naturaldelta(discord.utils.utcnow() - user.premium_since) + boosting_date = humanize.naturaldelta( + discord.utils.utcnow() - user.premium_since + ) - content.add_field(name="Member", value=f"#{member_number} / {len(ctx.guild.members)}") + content.add_field( + name="Member", value=f"#{member_number} / {len(ctx.guild.members)}" + ) content.add_field( name="Boosting", value=f"For {boosting_date}" if boosting_date else "No" ) content.add_field( name="Joined server", - value=user.joined_at.strftime("%d/%m/%Y %H:%M") if user.joined_at else "Unknown", + value=user.joined_at.strftime("%d/%m/%Y %H:%M") + if user.joined_at + else "Unknown", ) if self.bot.intents.presences: activity_display = util.UserActivity(user.activities).display() status = "mobile" if user.is_on_mobile() else user.status.name - status_display = f"{emojis.Status[status].value} {user.status.name.capitalize()}" + status_display = ( + f"{emojis.Status[status].value} {user.status.name.capitalize()}" + ) content.add_field(name="Status", value=status_display) - content.add_field(name="Activity", value=activity_display or "Unavailable") + content.add_field( + name="Activity", value=activity_display or "Unavailable" + ) content.add_field( name="Roles", @@ -171,7 +191,9 @@ async def members(self, ctx: commands.Context): if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") - sorted_members = sorted(ctx.guild.members, key=lambda x: x.joined_at or 0, reverse=True) + sorted_members = sorted( + ctx.guild.members, key=lambda x: x.joined_at or 0, reverse=True + ) membercount = len(sorted_members) content = discord.Embed(title=f"{ctx.guild.name} members") rows = [] @@ -183,7 +205,9 @@ async def members(self, ctx: commands.Context): await util.send_as_pages(ctx, content, rows) @commands.command() - async def banner(self, ctx: commands.Context, *, user: Optional[discord.User] = None): + async def banner( + self, ctx: commands.Context, *, user: Optional[discord.User] = None + ): """Get user's banner""" # banners are not cached so an api call is required user = await self.bot.fetch_user(user.id if user else ctx.author.id) @@ -192,7 +216,9 @@ async def banner(self, ctx: commands.Context, *, user: Optional[discord.User] = if not user.banner: if not user.accent_color: - raise exceptions.CommandWarning(f"**{user}** has not set banner or accent color.") + raise exceptions.CommandWarning( + f"**{user}** has not set banner or accent color." + ) content.color = user.accent_color content.description = f":art: Solid color `{user.accent_color}`" @@ -201,7 +227,9 @@ async def banner(self, ctx: commands.Context, *, user: Optional[discord.User] = banner_url = util.asset_full_size(user.banner) - content.set_author(name=f"{user} Banner", url=banner_url, icon_url=user.display_avatar.url) + content.set_author( + name=f"{user} Banner", url=banner_url, icon_url=user.display_avatar.url + ) content.set_image(url=banner_url) stats = await util.image_info_from_url(self.bot.session, banner_url) color = await util.color_from_image_url( @@ -216,7 +244,9 @@ async def banner(self, ctx: commands.Context, *, user: Optional[discord.User] = await ctx.send(embed=content) @commands.command(aliases=["sbanner", "guildbanner"]) - async def serverbanner(self, ctx: commands.Context, *, guild: Optional[discord.Guild] = None): + async def serverbanner( + self, ctx: commands.Context, *, guild: Optional[discord.Guild] = None + ): """Get server's banner""" if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") @@ -252,7 +282,9 @@ async def serverbanner(self, ctx: commands.Context, *, guild: Optional[discord.G await ctx.send(embed=content) @commands.command(aliases=["sinfo", "guildinfo"]) - async def serverinfo(self, ctx: commands.Context, *, guild: Optional[discord.Guild] = None): + async def serverinfo( + self, ctx: commands.Context, *, guild: Optional[discord.Guild] = None + ): """Get various information on server""" if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") @@ -281,14 +313,22 @@ async def serverinfo(self, ctx: commands.Context, *, guild: Optional[discord.Gui content.add_field(name="Members", value=str(guild.member_count)) content.add_field( name="Channels", - value=(f"{len(guild.text_channels)} Text, {len(guild.voice_channels)} Voice"), + value=( + f"{len(guild.text_channels)} Text, {len(guild.voice_channels)} Voice" + ), ) content.add_field(name="Roles", value=str(len(guild.roles))) content.add_field(name="Threads", value=str(len(guild.threads))) content.add_field(name="NSFW filter", value=guild.explicit_content_filter.name) - content.add_field(name="Emojis", value=f"{len(guild.emojis)} / {guild.emoji_limit}") - content.add_field(name="Stickers", value=f"{len(guild.stickers)} / {guild.sticker_limit}") - content.add_field(name="Created at", value=guild.created_at.strftime("%d/%m/%Y %H:%M")) + content.add_field( + name="Emojis", value=f"{len(guild.emojis)} / {guild.emoji_limit}" + ) + content.add_field( + name="Stickers", value=f"{len(guild.stickers)} / {guild.sticker_limit}" + ) + content.add_field( + name="Created at", value=guild.created_at.strftime("%d/%m/%Y %H:%M") + ) if guild.features: content.add_field( name="Features", @@ -306,7 +346,8 @@ async def roleslist(self, ctx: commands.Context): content = discord.Embed(title=f"Roles in {ctx.guild.name}") rows = [ - f"[`{role.id} | {str(role.color)}`] **x{len(role.members)}**{':warning:' if len(role.members) == 0 else ''}: {role.mention}" + f"[`{role.id} | {str(role.color)}`] **x{len(role.members)}**" + f"{':warning:' if len(role.members) == 0 else ''}: {role.mention}" for role in reversed(ctx.guild.roles) ] await util.send_as_pages(ctx, content, rows) @@ -316,9 +357,50 @@ async def leaderboard(self, ctx: commands.Context): """Show various leaderboards""" await util.command_group_help(ctx) + @leaderboard.command(name="fishygifted") + async def leaderboard_fishy_gifted(self, ctx: commands.Context, scope=""): + """Most altruistic fishers leaderboard""" + if ctx.guild is None: + raise exceptions.CommandError("Unable to get current guild") + + global_data = scope.lower() == "global" + data = await self.bot.db.fetch( + "SELECT user_id, fishy_gifted_count FROM fishy ORDER BY fishy_gifted_count DESC" + ) + + rows = [] + if data: + medal_emoji = [":first_place:", ":second_place:", ":third_place:"] + i = 1 + for user_id, fishy_count in data: + if global_data: + user = self.bot.get_user(user_id) + else: + user = ctx.guild.get_member(user_id) + + if user is None or user.bot or fishy_count == 0: + continue + + ranking = medal_emoji[i - 1] if i <= len(medal_emoji) else f"`#{i:2}`" + rows.append( + f"{ranking} **{util.displayname(user)}** — **{fishy_count}** fishy gifted" + ) + i += 1 + if not rows: + raise exceptions.CommandInfo("Nobody has gifted fish yet!") + + content = discord.Embed( + title=( + f":fish: {'Global' if global_data else ctx.guild.name} " + "gifted fishy leaderboard" + ), + color=int("55acee", 16), + ) + await util.send_as_pages(ctx, content, rows) + @leaderboard.command(name="fishy") async def leaderboard_fishy(self, ctx: commands.Context, scope=""): - """Fishy leaderboard""" + """Fishers leaderboard""" if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") @@ -341,7 +423,9 @@ async def leaderboard_fishy(self, ctx: commands.Context, scope=""): continue ranking = medal_emoji[i - 1] if i <= len(medal_emoji) else f"`#{i:2}`" - rows.append(f"{ranking} **{util.displayname(user)}** — **{fishy_count}** fishy") + rows.append( + f"{ranking} **{util.displayname(user)}** — **{fishy_count}** fishy" + ) i += 1 if not rows: raise exceptions.CommandInfo("Nobody has any fish yet!") @@ -371,7 +455,11 @@ async def leaderboard_wpm(self, ctx: commands.Context, scope=""): if data: i = 1 for userid, wpm, test_date, word_count in data: - user = self.bot.get_user(userid) if _global_ else ctx.guild.get_member(userid) + user = ( + self.bot.get_user(userid) + if _global_ + else ctx.guild.get_member(userid) + ) if user is None or user.bot: continue @@ -381,7 +469,8 @@ async def leaderboard_wpm(self, ctx: commands.Context, scope=""): ranking = f"`#{i:2}`" rows.append( - f"{ranking} **{util.displayname(user)}** — **{int(wpm)}** WPM ({word_count} words, {arrow.get(test_date).to('utc').humanize()})" + f"{ranking} **{util.displayname(user)}** — **{int(wpm)}** " + f"WPM ({word_count} words, {arrow.get(test_date).to('utc').humanize()})" ) i += 1 @@ -419,7 +508,9 @@ async def leaderboard_crowns(self, ctx: commands.Context): else: ranking = f"`#{i:2}`" - rows.append(f"{ranking} **{util.displayname(user)}** — **{amount}** crowns") + rows.append( + f"{ranking} **{util.displayname(user)}** — **{amount}** crowns" + ) if not rows: rows = ["No data."] @@ -432,7 +523,9 @@ async def leaderboard_crowns(self, ctx: commands.Context): @commands.command(enabled=False) async def profile( - self, ctx: commands.Context, user: Union[discord.Member, discord.User, None] = None + self, + ctx: commands.Context, + user: Union[discord.Member, discord.User, None] = None, ): """Your personal customizable user profile""" if user is None: @@ -469,7 +562,10 @@ def make_badge(classname): badges.append(make_badge(badge_classes["patreon"])) user_settings = await self.bot.db.fetch_row( - "SELECT lastfm_username, sunsign, location_string FROM user_settings WHERE user_id = %s", + """ + SELECT lastfm_username, sunsign, location_string + FROM user_settings WHERE user_id = %s + """, user.id, ) if user_settings: @@ -552,7 +648,9 @@ def make_badge(classname): "imageFormat": "png", } buffer = await util.render_html(self.bot, payload) - await ctx.send(file=discord.File(fp=buffer, filename=f"profile_{user.name}.png")) + await ctx.send( + file=discord.File(fp=buffer, filename=f"profile_{user.name}.png") + ) @commands.command() async def marry(self, ctx: commands.Context, user: discord.Member): @@ -575,12 +673,19 @@ async def marry(self, ctx: commands.Context, user: discord.Member): partner = ctx.guild.get_member(pair[0]) or await util.find_user( self.bot, pair[0] ) + if partner is None: + return await ctx.send( + ":confused: You are already married to someone but I don't know who it is" + "... Please divorce before marrying someone else!" + ) return await ctx.send( - f":confused: You are already married to **{util.displayname(partner)}**! You must divorce before marrying someone else..." + f":confused: You are already married to **{util.displayname(partner)}**! " + "You must divorce before marrying someone else..." ) if user.id in el: return await ctx.send( - f":grimacing: **{user}** is already married to someone else, sorry!" + f":grimacing: **{util.displayname(member=user)}** " + "is already married to someone else, sorry!" ) if (user.id, ctx.author.id) in self.proposals: @@ -594,10 +699,15 @@ async def marry(self, ctx: commands.Context, user: discord.Member): await ctx.send( embed=discord.Embed( color=int("dd2e44", 16), - description=f":revolving_hearts: **{util.displayname(user)}** and **{util.displayname(ctx.author)}** are now married :wedding:", + description=( + f":revolving_hearts: **{util.displayname(member=user)}** and " + f"**{ctx.author.display_name}** are now married :wedding:" + ), ) ) - new_proposals = {el for el in self.proposals if el[0] not in [user.id, ctx.author.id]} + new_proposals = { + el for el in self.proposals if el[0] not in [user.id, ctx.author.id] + } self.proposals = new_proposals else: self.proposals.add((ctx.author.id, user.id)) @@ -614,26 +724,27 @@ async def divorce(self, ctx: commands.Context): if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") - partner = "" + partner = None to_remove = [] for el in self.bot.cache.marriages: if ctx.author.id in el: to_remove.append(el) pair = list(el) if ctx.author.id == pair[0]: - partner = ctx.guild.get_member(pair[1]) or await util.find_user( - self.bot, pair[1] - ) + partner = pair[1] else: - partner = ctx.guild.get_member(pair[0]) or await util.find_user( - self.bot, pair[0] - ) + partner = pair[0] - if partner == "": + if partner is None: return await ctx.send(":thinking: You are not married!") + partner = ctx.guild.get_member(partner) or await util.find_user( + self.bot, partner + ) + content = discord.Embed( - description=f":broken_heart: Divorce **{util.displayname(partner)}**?", + description=":broken_heart:" + + (f"Divorce **{util.displayname(partner)}**?" if partner else "Divorce?"), color=int("dd2e44", 16), ) msg = await ctx.send(embed=content) @@ -649,7 +760,11 @@ async def confirm(): await ctx.send( embed=discord.Embed( color=int("ffcc4d", 16), - description=f":pensive: You and **{util.displayname(partner)}** are now divorced...", + description=( + ":pensive: You " + + (f"and **{util.displayname(partner)}** " if partner else "") + + "are now divorced..." + ), ) ) @@ -658,7 +773,9 @@ async def cancel(): functions = {"✅": confirm, "❌": cancel} asyncio.ensure_future( - util.reaction_buttons(ctx, msg, functions, only_author=True, single_use=True) + util.reaction_buttons( + ctx, msg, functions, only_author=True, single_use=True + ) ) @commands.command() @@ -682,9 +799,13 @@ async def marriage( ) if data: if data[0] == member.id: - partner = ctx.guild.get_member(data[1]) or await util.find_user(self.bot, data[1]) + partner = ctx.guild.get_member(data[1]) or await util.find_user( + self.bot, data[1] + ) else: - partner = ctx.guild.get_member(data[0]) or await util.find_user(self.bot, data[0]) + partner = ctx.guild.get_member(data[0]) or await util.find_user( + self.bot, data[0] + ) marriage_date = data[2] length = humanize.naturaldelta( arrow.utcnow().timestamp() - marriage_date.timestamp(), months=False @@ -694,7 +815,9 @@ async def marriage( color=int("f4abba", 16), description=( f":wedding: {'You have' if member == ctx.author else f'**{member}** has'} " - f"been married to **{util.displayname(partner)}** for **{length}**" + "been married to " + + (f"**{partner.display_name}**" if partner else "someone") + + f" for **{length}**" ), ) ) diff --git a/cogs/utility.py b/cogs/utility.py index c425b45..551d806 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -15,11 +15,11 @@ import orjson from discord.ext import commands, tasks from loguru import logger - -from modules import emojis, exceptions, queries, util from modules.misobot import MisoBot from modules.shazam import Shazam -from modules.ui import BaseButtonPaginator, Compliance, RowPaginator +from modules.ui import BaseButtonPaginator, Compliance + +from modules import emojis, exceptions, queries, util papago_pairs = [ "ko/en", @@ -136,8 +136,9 @@ async def check_reminders(self): date = arrow.get(created_on) if now_ts - reminder_ts > 21600: + date_fmt = date.format("DD/MM/YYYY HH:mm:ss") logger.info( - f"Deleting reminder set for {date.format('DD/MM/YYYY HH:mm:ss')} for being over 6 hours late" + f"Deleting reminder set for {date_fmt} for being over 6 hours late" ) else: embed = discord.Embed( @@ -156,7 +157,9 @@ async def check_reminders(self): await user.send(embed=embed) logger.info(f'Reminded {user} to "{content}"') except discord.errors.Forbidden: - logger.warning(f"Unable to remind {user}, missing DM permissions!") + logger.warning( + f"Unable to remind {user}, missing DM permissions!" + ) else: logger.info(f"Deleted expired reminder by unknown user {user_id}") @@ -175,9 +178,9 @@ async def check_reminders(self): async def on_command_error(self, ctx: commands.Context, error): """only for CommandNotFound""" error = getattr(error, "original", error) - if isinstance(error, commands.CommandNotFound) and ctx.message.content.startswith( - f"{ctx.prefix}!" - ): + if isinstance( + error, commands.CommandNotFound + ) and ctx.message.content.startswith(f"{ctx.prefix}!"): # type ignores everywhere because this is so hacky ctx.timer = time() # type: ignore ctx.iscallback = True # type: ignore @@ -245,7 +248,9 @@ async def shazam(self, ctx: commands.Context, url_or_attachment: Optional[str]): and ctx.message.reference.message_id and isinstance(ctx.channel, (discord.Thread, discord.TextChannel)) ): - reply_message = await ctx.channel.fetch_message(ctx.message.reference.message_id) + reply_message = await ctx.channel.fetch_message( + ctx.message.reference.message_id + ) if not reply_message.attachments: raise exceptions.CommandWarning("Referenced message has no attachments") attachment = await reply_message.attachments[0].to_file() @@ -254,7 +259,9 @@ async def shazam(self, ctx: commands.Context, url_or_attachment: Optional[str]): return await util.send_command_help(ctx) if result is None: - raise exceptions.CommandWarning("I was unable to recognize any music from this") + raise exceptions.CommandWarning( + "I was unable to recognize any music from this" + ) metadata = "\n".join([f'`{m["title"]}:` {m["text"]}' for m in result.metadata]) content = discord.Embed( @@ -264,12 +271,17 @@ async def shazam(self, ctx: commands.Context, url_or_attachment: Optional[str]): content.set_author( name="Shazam result", url=result.url, - icon_url="https://upload.wikimedia.org/wikipedia/commons/thumb/c/c0/Shazam_icon.svg/84px-Shazam_icon.svg.png", + icon_url=( + "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c0/" + "Shazam_icon.svg/84px-Shazam_icon.svg.png" + ), ) content.set_thumbnail(url=result.cover_art) await ctx.send(embed=content) - @commands.command(usage="<'in' | 'on'>