From 7b99033f81e5219f031a5ffee9ca80593c686b19 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Tue, 13 Jun 2023 12:54:24 +0300 Subject: [PATCH 01/40] Fix most linter warnings --- cogs/configuration.py | 3 +- cogs/customcommands.py | 10 +++- cogs/errorhandler.py | 13 ++++-- cogs/events.py | 15 ++++-- cogs/information.py | 10 ++-- cogs/kpop.py | 3 +- cogs/lastfm.py | 6 ++- cogs/media.py | 14 ++++-- cogs/misc.py | 33 +++++++++++--- cogs/mod.py | 93 +++++++++++++++++++++----------------- cogs/notifications.py | 17 +++++-- cogs/owner.py | 3 +- cogs/roles.py | 14 ++++-- cogs/typings.py | 27 ++++++----- cogs/user.py | 50 +++++++++++++------- cogs/utility.py | 79 ++++++++++---------------------- modules/instagram.py | 3 +- modules/media_embedders.py | 6 ++- modules/tiktok.py | 6 ++- modules/util.py | 16 +++---- pyproject.toml | 1 + 21 files changed, 249 insertions(+), 173 deletions(-) diff --git a/cogs/configuration.py b/cogs/configuration.py index 3e755ce..5678889 100644 --- a/cogs/configuration.py +++ b/cogs/configuration.py @@ -309,7 +309,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: diff --git a/cogs/customcommands.py b/cogs/customcommands.py index ae33afb..69c00dc 100644 --- a/cogs/customcommands.py +++ b/cogs/customcommands.py @@ -161,7 +161,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( @@ -290,7 +293,10 @@ async def command_clear(self, ctx: commands.Context): 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.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(): diff --git a/cogs/errorhandler.py b/cogs/errorhandler.py index d43a094..6d1b47d 100644 --- a/cogs/errorhandler.py +++ b/cogs/errorhandler.py @@ -36,7 +36,10 @@ def __init__(self, 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)}" + 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: @@ -93,10 +96,14 @@ async def send_error( 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." diff --git a/cogs/events.py b/cogs/events.py index 402bd9b..81ab3a5 100644 --- a/cogs/events.py +++ b/cogs/events.py @@ -155,7 +155,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: @@ -212,7 +215,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: @@ -470,7 +476,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( """ diff --git a/cogs/information.py b/cogs/information.py index 5196a51..e9f6829 100644 --- a/cogs/information.py +++ b/cogs/information.py @@ -337,7 +337,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: @@ -379,7 +380,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: @@ -401,7 +403,7 @@ async def commandstats_single(self, ctx: commands.Context, command_name): 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 +478,7 @@ 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..a668d2f 100644 --- a/cogs/kpop.py +++ b/cogs/kpop.py @@ -67,7 +67,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)}") diff --git a/cogs/lastfm.py b/cogs/lastfm.py index d0c2603..a28a22c 100644 --- a/cogs/lastfm.py +++ b/cogs/lastfm.py @@ -211,7 +211,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"]) diff --git a/cogs/media.py b/cogs/media.py index 0747bda..c748d24 100644 --- a/cogs/media.py +++ b/cogs/media.py @@ -83,7 +83,10 @@ async def autoembedder( return await ctx.send( embed=discord.Embed( - description=f"{provider.capitalize()} automatic embeds are currently **{current_state}** for this server" + description=( + f"{provider.capitalize()} automatic embeds are " + f"currently **{current_state}** for this server" + ) ) ) @@ -108,7 +111,8 @@ async def autoembedder( await util.send_success( ctx, - f"{provider.capitalize()} automatic embeds are now **{'ON' if state else 'OFF'}** for this server", + f"{provider.capitalize()} automatic embeds are now " + f"**{'ON' if state else 'OFF'}** for this server", ) @commands.command(aliases=["ig", "insta"]) @@ -190,7 +194,8 @@ 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() @@ -229,7 +234,8 @@ 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) diff --git a/cogs/misc.py b/cogs/misc.py index 7258dd1..93b7e60 100644 --- a/cogs/misc.py +++ b/cogs/misc.py @@ -271,21 +271,42 @@ 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}%", diff --git a/cogs/mod.py b/cogs/mod.py index 8bfdfb6..96160d9 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -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,46 @@ 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) @@ -173,7 +180,8 @@ async def timeout(self, ctx: commands.Context, member: discord.Member, *, durati 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) @@ -370,7 +378,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 +409,8 @@ 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), ) ) @@ -432,7 +442,8 @@ 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) diff --git a/cogs/notifications.py b/cogs/notifications.py index 87bd8d9..602b0b6 100644 --- a/cogs/notifications.py +++ b/cogs/notifications.py @@ -134,7 +134,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, @@ -165,7 +170,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: @@ -239,7 +245,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( @@ -264,7 +271,9 @@ 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, ) diff --git a/cogs/owner.py b/cogs/owner.py index 5529eb7..8380719 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -137,7 +137,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, diff --git a/cogs/roles.py b/cogs/roles.py index c27a116..f94795e 100644 --- a/cogs/roles.py +++ b/cogs/roles.py @@ -123,14 +123,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( @@ -251,7 +252,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) @@ -449,7 +454,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..737f1d1 100644 --- a/cogs/typings.py +++ b/cogs/typings.py @@ -171,13 +171,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: @@ -214,12 +209,13 @@ def check(_reaction, _user): 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"{f'`#{i}`' if i > 1 else ':trophy:'} **{player.display_name}** — " + (f"**{int(wpm)} WPM / {int(accuracy)}% Accuracy**" if wpm != 0 else ":x:") ) @@ -259,7 +255,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 +288,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) diff --git a/cogs/user.py b/cogs/user.py index 79cf027..a138069 100644 --- a/cogs/user.py +++ b/cogs/user.py @@ -306,7 +306,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) @@ -381,7 +382,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 @@ -469,7 +471,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: @@ -575,12 +580,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,7 +606,8 @@ 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]} @@ -614,26 +627,25 @@ 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 +661,9 @@ 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...", ) ) @@ -694,7 +708,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..c3f7f09 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -19,7 +19,7 @@ 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 papago_pairs = [ "ko/en", @@ -136,9 +136,8 @@ async def check_reminders(self): date = arrow.get(created_on) if now_ts - reminder_ts > 21600: - logger.info( - f"Deleting reminder set for {date.format('DD/MM/YYYY HH:mm:ss')} for being over 6 hours late" - ) + date_fmt = date.format("DD/MM/YYYY HH:mm:ss") + logger.info(f"Deleting reminder set for {date_fmt} for being over 6 hours late") else: embed = discord.Embed( color=int("d3a940", 16), @@ -264,7 +263,8 @@ 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) @@ -310,7 +310,8 @@ async def remindme(self, ctx: commands.Context, pre, *, arguments): await self.bot.db.execute( """ - INSERT INTO reminder (user_id, guild_id, created_on, reminder_date, content, original_message_url) + INSERT INTO reminder (user_id, guild_id, created_on, reminder_date, + content, original_message_url) VALUES(%s, %s, %s, %s, %s, %s) """, ctx.author.id, @@ -414,9 +415,8 @@ async def weather_now(self, ctx: commands.Context, *, location: Optional[str] = precipitation_type = self.weather_constants["precipitation"][ str(values_today["precipitationType"]) ] - summary += ( - f", with {values_today['precipitationProbability']}% chance of {precipitation_type}" - ) + precipitation_chance = values_today["precipitationProbability"] + summary += f", with {precipitation_chance}% chance of {precipitation_type}" content = discord.Embed(color=int("e1e8ed", 16)) content.title = f":flag_{country_code.lower()}: {address}" @@ -424,9 +424,12 @@ async def weather_now(self, ctx: commands.Context, *, location: Optional[str] = def render(F: bool): information_rows = [ - f":thermometer: Currently **{temp(temperature, F)}**, feels like **{temp(temperature_apparent, F)}**", - f":calendar: Daily low **{temp(values_today['temperatureMin'], F)}**, high **{temp(values_today['temperatureMax'], F)}**", - f":dash: Wind speed **{values_current['windSpeed']} m/s** with gusts of **{values_current['windGust']} m/s**", + f":thermometer: Currently **{temp(temperature, F)}**, " + f"feels like **{temp(temperature_apparent, F)}**", + f":calendar: Daily low **{temp(values_today['temperatureMin'], F)}**, " + f"high **{temp(values_today['temperatureMax'], F)}**", + f":dash: Wind speed **{values_current['windSpeed']} m/s** " + f"with gusts of **{values_current['windGust']} m/s**", f":sunrise: Sunrise at **{sunrise}**, sunset at **{sunset}**", f":sweat_drops: Air humidity **{values_current['humidity']}%**", f":map: [See on map](https://www.google.com/maps/search/?api=1&query={lat},{lon})", @@ -750,48 +753,8 @@ async def wolfram(self, ctx: commands.Context, *, query): else: await ctx.send(":shrug:") - @commands.command(enabled=False) - async def mygifs(self, ctx: commands.Context): - """See the gifs you have uploaded""" - data = await self.bot.db.fetch( - """ - SELECT gif_id, source_url, ts FROM user_uploaded_gif WHERE user_id = %s - """, - ctx.author.id, - ) - if not data: - raise exceptions.CommandWarning("You have not uploaded any gifs yet!") - - async with self.bot.session.get( - "https://api.giphy.com/v1/gifs", - params={ - "api_key": self.bot.keychain.GIPHY_API_KEY, - "ids": ",".join(gif[0] for gif in data), - }, - ) as response: - response_data = await response.json() - - if not response_data["data"]: - raise exceptions.CommandWarning("You don't have any gifs :(") - - gif_sources = {gif[0]: gif[1] for gif in data} - - rows = [] - for gif in response_data["data"]: - source = gif_sources[gif["id"]] - ts = arrow.get(gif["import_datetime"]) - rows.append( - f"`{gif['id']}` [Source]({source}) | [Gif]({gif['url']}) " - ) - - await RowPaginator( - discord.Embed(color=int("8b3cff", 16)).set_author( - name="Your gifs", icon_url=ctx.author.display_avatar.url - ), - rows, - ).run(ctx) - - @commands.command(enabled=False) + @commands.is_owner() + @commands.command() async def creategif(self, ctx: commands.Context, media_url: str, *tags: str): """Create a gif and upload it to GIPHY""" has_seen_warning = await self.bot.db.fetch_value( @@ -865,7 +828,8 @@ async def creategif(self, ctx: commands.Context, media_url: str, *tags: str): await msg.delete() if response.status == 500: raise exceptions.CommandWarning( - "Gif creation failed! Most likely the url provided is not a valid video source." + "Gif creation failed! " + "Most likely the url provided is not a valid video source." ) response.raise_for_status() return @@ -1041,7 +1005,10 @@ async def tz_list(self, ctx: commands.Context): rows = [] user_ids = [user.id for user in ctx.guild.members] data = await self.bot.db.fetch( - "SELECT user_id, timezone FROM user_settings WHERE user_id IN %s AND timezone IS NOT NULL", + """ + SELECT user_id, timezone FROM user_settings + WHERE user_id IN %s AND timezone IS NOT NULL + """, user_ids, ) if not data: diff --git a/modules/instagram.py b/modules/instagram.py index 8052029..68da360 100644 --- a/modules/instagram.py +++ b/modules/instagram.py @@ -363,7 +363,8 @@ async def graphql_request(self, shortcode: str): "query_hash": "9f8827793ef34641b2fb195d4d41151c", "variables": '{"shortcode": "' + shortcode - + '", "child_comment_count": 3, "fetch_comment_count": 40, "parent_comment_count": 24, "has_threaded_comments": "true"}', + + '", "child_comment_count": 3, "fetch_comment_count": 40, ' + + '"parent_comment_count": 24, "has_threaded_comments": "true"}', } headers = { "Host": "www.instagram.com", diff --git a/modules/media_embedders.py b/modules/media_embedders.py index 2ce8e96..e21d806 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -173,7 +173,8 @@ class InstagramEmbedder(BaseEmbedder): @staticmethod def extract_links(text: str, include_shortcodes=True) -> list[InstagramPost | InstagramStory]: text = "\n".join(text.split()) - instagram_regex = r"(?:https?:\/\/)?(?:www.)?instagram.com\/?([a-zA-Z0-9\.\_\-]+)?\/([p]+)?([reel]+)?([tv]+)?([stories]+)?\/([a-zA-Z0-9\-\_\.]+)\/?([0-9]+)?" + instagram_regex = r"(?:https?:\/\/)?(?:www.)?instagram.com\/?([a-zA-Z0-9\.\_\-]+)?\/([p]+)?" + r"([reel]+)?([tv]+)?([stories]+)?\/([a-zA-Z0-9\-\_\.]+)\/?([0-9]+)?" results = [] for match in regex.finditer(instagram_regex, text): # group 1 for username @@ -259,7 +260,8 @@ def __init__(self, bot: "MisoBot"): @staticmethod def extract_links(text: str): text = "\n".join(text.split()) - pattern = r"\bhttps?:\/\/(?:m|www|vm)\.tiktok\.com\/.*\b(?:(?:usr|v|embed|user|video|t)\/|\?shareId=|\&item_id=)(\d+)\b" + pattern = r"\bhttps?:\/\/(?:m|www|vm)\.tiktok\.com\/.*\b" + r"(?:(?:usr|v|embed|user|video|t)\/|\?shareId=|\&item_id=)(\d+)\b" vm_pattern = r"\bhttps?:\/\/(?:vm|vt)\.tiktok\.com\/.*\b(\S+)\b" validated_urls = [ diff --git a/modules/tiktok.py b/modules/tiktok.py index 3a759a1..f1adddf 100644 --- a/modules/tiktok.py +++ b/modules/tiktok.py @@ -38,8 +38,10 @@ class TikTok: HEADERS: Dict[str, str] = { "Host": "musicaldown.com", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) " + "Gecko/20100101 Firefox/103.0", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9," + "image/avif,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.5", "DNT": "1", "Upgrade-Insecure-Requests": "1", diff --git a/modules/util.py b/modules/util.py index 43135ef..47f10f2 100644 --- a/modules/util.py +++ b/modules/util.py @@ -77,16 +77,12 @@ async def suppress(message: discord.Message): def displayname(member: Optional[discord.User | discord.Member], escape=True): if member is None: - return None - - name = member.name - if isinstance(member, discord.Member) and member.nick is not None: - name = member.nick + return "[unknown user]" if escape: - name = discord.utils.escape_markdown(name) + return discord.utils.escape_markdown(member.display_name) - return name + return member.display_name def displaychannel( @@ -869,7 +865,8 @@ async def send_donation_beg(channel: "discord.abc.MessageableChannel"): donate_link = "https://misobot.xyz/donate" content = discord.Embed( color=int("be1931", 16), - description=f":loudspeaker: Miso Bot is running solely on donations; Consider [donating]({donate_link}) if you like the bot!", + description=f":loudspeaker: Miso Bot is running solely on donations; " + f"Consider [donating]({donate_link}) if you like the bot!", ) await channel.send(embed=content, delete_after=15) @@ -903,7 +900,8 @@ async def require_chunked(guild: discord.Guild): start_time = time() await guild.chunk(cache=True) logger.info( - f"Chunked [{guild}] with {guild.member_count} members in {time() - start_time:.2f} seconds" + f"Chunked [{guild}] with {guild.member_count} members " + f"in {time() - start_time:.2f} seconds" ) diff --git a/pyproject.toml b/pyproject.toml index 1dd603b..b50b1bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ executionEnvironments = [ ] exclude = [ "**/__pycache__", + ".venv" ] venvPath = "." venv = "venv" From b53282f5afd8b7aa8d1c6b711ac54c0487308312 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Tue, 13 Jun 2023 13:04:11 +0300 Subject: [PATCH 02/40] Update dependencies (including discord.py 2.3) --- dev-requirements.txt | 8 ++++++-- requirements.txt | 36 ++++++------------------------------ update-deps | 5 +++++ 3 files changed, 17 insertions(+), 32 deletions(-) create mode 100755 update-deps diff --git a/dev-requirements.txt b/dev-requirements.txt index 264db72..8a41c87 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -18,7 +18,7 @@ dill==0.3.6 # via pylint distlib==0.3.6 # via virtualenv -filelock==3.12.1 +filelock==3.12.2 # via virtualenv identify==2.5.24 # via pre-commit @@ -33,7 +33,9 @@ mccabe==0.7.0 mypy-extensions==1.0.0 # via black nodeenv==1.8.0 - # via pre-commit + # via + # pre-commit + # pyright packaging==23.1 # via # -c requirements.txt @@ -49,6 +51,8 @@ pre-commit==3.3.2 # via -r dev-requirements.in pylint==2.17.4 # via -r dev-requirements.in +pyright==1.1.313 + # via -r dev-requirements.in pyyaml==6.0 # via pre-commit ruff==0.0.272 diff --git a/requirements.txt b/requirements.txt index 1902a7a..3fc5a93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,10 +15,9 @@ aiohttp==3.8.4 # async-cse # discord-py # shazamio - # tweepy aiohttp-cors==0.7.0 # via -r requirements.in -aiomysql==0.1.1 +aiomysql==0.2.0 # via -r requirements.in aiosignal==1.3.1 # via aiohttp @@ -30,8 +29,6 @@ astunparse==1.6.3 # via import-expression async-cse==0.3.0 # via -r requirements.in -async-lru==2.0.2 - # via tweepy async-timeout==4.0.2 # via aiohttp attrs==23.1.0 @@ -46,14 +43,10 @@ braceexpand==0.1.7 # via jishaku brotli==1.0.9 # via discord-py -certifi==2023.5.7 - # via requests cffi==1.15.1 # via pycares charset-normalizer==3.1.0 - # via - # aiohttp - # requests + # via aiohttp click==8.1.3 # via jishaku colorgram-py==1.2.0 @@ -64,13 +57,13 @@ cycler==0.11.0 # via matplotlib dataclass-factory==2.16 # via shazamio -discord-py[speed]==2.2.3 +discord-py[speed]==2.3.0 # via -r requirements.in dnspython==2.3.0 # via minestat durations-nlp==1.0.1 # via -r requirements.in -fonttools==4.39.4 +fonttools==4.40.0 # via matplotlib frozenlist==1.3.3 # via @@ -81,7 +74,6 @@ humanize==4.6.0 idna==3.4 # via # anyio - # requests # yarl import-expression==1.1.4 # via jishaku @@ -116,10 +108,6 @@ numpy==1.24.3 # matplotlib # scipy # shazamio -oauthlib==3.2.2 - # via - # requests-oauthlib - # tweepy orjson==3.9.1 # via discord-py packaging==23.1 @@ -151,7 +139,7 @@ pymysql==1.0.3 # via aiomysql pyparsing==3.0.9 # via matplotlib -pytest==7.3.1 +pytest==7.3.2 # via # pytest-asyncio # shazamio @@ -169,12 +157,6 @@ redis==4.5.5 # via -r requirements.in regex==2023.6.3 # via -r requirements.in -requests==2.31.0 - # via - # requests-oauthlib - # tweepy -requests-oauthlib==1.3.1 - # via tweepy scipy==1.10.1 # via -r requirements.in shazamio==0.4.0.1 @@ -189,14 +171,8 @@ sniffio==1.3.0 # via anyio soupsieve==2.4.1 # via beautifulsoup4 -tweepy[async]==4.14.0 - # via -r requirements.in typing-extensions==4.6.3 - # via - # async-lru - # pydantic -urllib3==2.0.3 - # via requests + # via pydantic uvloop==0.17.0 # via -r requirements.in webencodings==0.5.1 diff --git a/update-deps b/update-deps new file mode 100755 index 0000000..a30bd9d --- /dev/null +++ b/update-deps @@ -0,0 +1,5 @@ +#!/bin/sh + +pip-compile requirements.in > requirements.txt --resolver=backtracking +pip-compile dev-requirements.in > dev-requirements.txt --resolver=backtracking + From fb7107c5f519cc8fc0222779f0670938b08ef1b9 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Tue, 13 Jun 2023 14:12:14 +0300 Subject: [PATCH 03/40] Regex cannot be split into multiple lines it seems --- modules/media_embedders.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/modules/media_embedders.py b/modules/media_embedders.py index e21d806..c7f0655 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -173,8 +173,7 @@ class InstagramEmbedder(BaseEmbedder): @staticmethod def extract_links(text: str, include_shortcodes=True) -> list[InstagramPost | InstagramStory]: text = "\n".join(text.split()) - instagram_regex = r"(?:https?:\/\/)?(?:www.)?instagram.com\/?([a-zA-Z0-9\.\_\-]+)?\/([p]+)?" - r"([reel]+)?([tv]+)?([stories]+)?\/([a-zA-Z0-9\-\_\.]+)\/?([0-9]+)?" + instagram_regex = r"(?:https?:\/\/)?(?:www.)?instagram.com\/?([a-zA-Z0-9\.\_\-]+)?\/([p]+)? ([reel]+)?([tv]+)?([stories]+)?\/([a-zA-Z0-9\-\_\.]+)\/?([0-9]+)?" results = [] for match in regex.finditer(instagram_regex, text): # group 1 for username @@ -260,8 +259,7 @@ def __init__(self, bot: "MisoBot"): @staticmethod def extract_links(text: str): text = "\n".join(text.split()) - pattern = r"\bhttps?:\/\/(?:m|www|vm)\.tiktok\.com\/.*\b" - r"(?:(?:usr|v|embed|user|video|t)\/|\?shareId=|\&item_id=)(\d+)\b" + pattern = r"\bhttps?:\/\/(?:m|www|vm)\.tiktok\.com\/.*\b(?:(?:usr|v|embed|user|video|t)\/|\?shareId=|\&item_id=)(\d+)\b" vm_pattern = r"\bhttps?:\/\/(?:vm|vt)\.tiktok\.com\/.*\b(\S+)\b" validated_urls = [ From fa7511486b4805948c1c33519472224edf063397 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Tue, 13 Jun 2023 14:31:59 +0300 Subject: [PATCH 04/40] Fix strings that got cut off by accident --- cogs/media.py | 10 +++++---- cogs/misc.py | 54 +++++++++++++++++++++++++++-------------------- cogs/mod.py | 12 +++++++---- cogs/typings.py | 2 +- cogs/user.py | 14 +++++++----- cogs/utility.py | 26 +++++++++++++++-------- modules/tiktok.py | 10 +++++---- modules/util.py | 6 ++++-- 8 files changed, 82 insertions(+), 52 deletions(-) diff --git a/cogs/media.py b/cogs/media.py index c748d24..83376b6 100644 --- a/cogs/media.py +++ b/cogs/media.py @@ -194,8 +194,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() @@ -234,8 +235,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) diff --git a/cogs/misc.py b/cogs/misc.py index 93b7e60..30db1ac 100644 --- a/cogs/misc.py +++ b/cogs/misc.py @@ -278,35 +278,43 @@ async def ship(self, ctx: commands.Context, *, names): 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]} " - 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." + 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 " - 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." + 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}%", diff --git a/cogs/mod.py b/cogs/mod.py index 96160d9..12151c4 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -409,8 +409,10 @@ async def ban(self, ctx: commands.Context, *discord_users): except discord.errors.Forbidden: await ctx.send( embed=discord.Embed( - description=":no_entry: It seems I don't have the " - f"permission to ban **{user}**", + description=( + ":no_entry: It seems I don't have the " + f"permission to ban **{user}**" + ), color=int("be1931", 16), ) ) @@ -442,8 +444,10 @@ async def confirm_ban(): content.title = ":white_check_mark: Banned user" except discord.errors.Forbidden: content.title = None - content.description = ":no_entry: It seems I don't have the permission to " - f"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) diff --git a/cogs/typings.py b/cogs/typings.py index 737f1d1..7eb14e0 100644 --- a/cogs/typings.py +++ b/cogs/typings.py @@ -215,7 +215,7 @@ def check(_reaction, _user): ): values.append((ctx.guild.id, player.id, 1, 1 if i == 1 else 0)) rows.append( - f"{f'`#{i}`' if i > 1 else ':trophy:'} **{player.display_name}** — " + f"{f'`#{i}`' if i > 1 else ':trophy:'} **{util.displayname(player)}** — " + (f"**{int(wpm)} WPM / {int(accuracy)}% Accuracy**" if wpm != 0 else ":x:") ) diff --git a/cogs/user.py b/cogs/user.py index a138069..ca5780c 100644 --- a/cogs/user.py +++ b/cogs/user.py @@ -606,8 +606,10 @@ 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(member=user)}** and " - f"**{ctx.author.display_name}** 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]} @@ -661,9 +663,11 @@ async def confirm(): await ctx.send( embed=discord.Embed( color=int("ffcc4d", 16), - description=":pensive: You " - + (f"and **{util.displayname(partner)}** " if partner else "") - + "are now divorced...", + description=( + ":pensive: You " + + (f"and **{util.displayname(partner)}** " if partner else "") + + "are now divorced..." + ), ) ) diff --git a/cogs/utility.py b/cogs/utility.py index c3f7f09..b8959a9 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -263,8 +263,10 @@ 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) @@ -424,12 +426,18 @@ async def weather_now(self, ctx: commands.Context, *, location: Optional[str] = def render(F: bool): information_rows = [ - f":thermometer: Currently **{temp(temperature, F)}**, " - f"feels like **{temp(temperature_apparent, F)}**", - f":calendar: Daily low **{temp(values_today['temperatureMin'], F)}**, " - f"high **{temp(values_today['temperatureMax'], F)}**", - f":dash: Wind speed **{values_current['windSpeed']} m/s** " - f"with gusts of **{values_current['windGust']} m/s**", + ( + f":thermometer: Currently **{temp(temperature, F)}**, " + f"feels like **{temp(temperature_apparent, F)}**" + ), + ( + f":calendar: Daily low **{temp(values_today['temperatureMin'], F)}**, " + f"high **{temp(values_today['temperatureMax'], F)}**" + ), + ( + f":dash: Wind speed **{values_current['windSpeed']} m/s** " + f"with gusts of **{values_current['windGust']} m/s**" + ), f":sunrise: Sunrise at **{sunrise}**, sunset at **{sunset}**", f":sweat_drops: Air humidity **{values_current['humidity']}%**", f":map: [See on map](https://www.google.com/maps/search/?api=1&query={lat},{lon})", @@ -1006,7 +1014,7 @@ async def tz_list(self, ctx: commands.Context): user_ids = [user.id for user in ctx.guild.members] data = await self.bot.db.fetch( """ - SELECT user_id, timezone FROM user_settings + SELECT user_id, timezone FROM user_settings WHERE user_id IN %s AND timezone IS NOT NULL """, user_ids, diff --git a/modules/tiktok.py b/modules/tiktok.py index f1adddf..8cc9f1b 100644 --- a/modules/tiktok.py +++ b/modules/tiktok.py @@ -38,10 +38,12 @@ class TikTok: HEADERS: Dict[str, str] = { "Host": "musicaldown.com", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) " - "Gecko/20100101 Firefox/103.0", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9," - "image/avif,image/webp,*/*;q=0.8", + "User-Agent": ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:103.0) Gecko/20100101 Firefox/103.0" + ), + "Accept": ( + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8" + ), "Accept-Language": "en-US,en;q=0.5", "DNT": "1", "Upgrade-Insecure-Requests": "1", diff --git a/modules/util.py b/modules/util.py index 47f10f2..09ee86c 100644 --- a/modules/util.py +++ b/modules/util.py @@ -865,8 +865,10 @@ async def send_donation_beg(channel: "discord.abc.MessageableChannel"): donate_link = "https://misobot.xyz/donate" content = discord.Embed( color=int("be1931", 16), - description=f":loudspeaker: Miso Bot is running solely on donations; " - f"Consider [donating]({donate_link}) if you like the bot!", + description=( + f":loudspeaker: Miso Bot is running solely on donations; " + f"Consider [donating]({donate_link}) if you like the bot!" + ), ) await channel.send(embed=content, delete_after=15) From 49badaefcaab8611e2c6e0dcad9c196015fb99ea Mon Sep 17 00:00:00 2001 From: Joinemm Date: Tue, 13 Jun 2023 16:04:15 +0300 Subject: [PATCH 05/40] Fix regex --- modules/media_embedders.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/modules/media_embedders.py b/modules/media_embedders.py index c7f0655..49571b7 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -173,7 +173,11 @@ class InstagramEmbedder(BaseEmbedder): @staticmethod def extract_links(text: str, include_shortcodes=True) -> list[InstagramPost | InstagramStory]: text = "\n".join(text.split()) - instagram_regex = r"(?:https?:\/\/)?(?:www.)?instagram.com\/?([a-zA-Z0-9\.\_\-]+)?\/([p]+)? ([reel]+)?([tv]+)?([stories]+)?\/([a-zA-Z0-9\-\_\.]+)\/?([0-9]+)?" + instagram_regex = ( + r"(?:https?:\/\/)?(?:www.)?instagram.com\/" + r"?([a-zA-Z0-9\.\_\-]+)?\/([p]+)?([reel]+)?([tv]+)?([stories]+)?\/" + r"([a-zA-Z0-9\-\_\.]+)\/?([0-9]+)?" + ) results = [] for match in regex.finditer(instagram_regex, text): # group 1 for username @@ -259,7 +263,10 @@ def __init__(self, bot: "MisoBot"): @staticmethod def extract_links(text: str): text = "\n".join(text.split()) - pattern = r"\bhttps?:\/\/(?:m|www|vm)\.tiktok\.com\/.*\b(?:(?:usr|v|embed|user|video|t)\/|\?shareId=|\&item_id=)(\d+)\b" + pattern = ( + r"\bhttps?:\/\/(?:m|www|vm)\.tiktok\.com\/.*\b(?:(?:usr|v|embed|user|video|t)\/" + r"|\?shareId=|\&item_id=)(\d+)\b" + ) vm_pattern = r"\bhttps?:\/\/(?:vm|vt)\.tiktok\.com\/.*\b(\S+)\b" validated_urls = [ From 36e13d2238ff704e6b1421db879e4ae50258b6c3 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Thu, 15 Jun 2023 11:53:13 +0300 Subject: [PATCH 06/40] Add support for new tiktok url formats --- modules/media_embedders.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/modules/media_embedders.py b/modules/media_embedders.py index 49571b7..cb805ab 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -263,17 +263,20 @@ def __init__(self, bot: "MisoBot"): @staticmethod def extract_links(text: str): text = "\n".join(text.split()) - pattern = ( + video_id_pattern = ( r"\bhttps?:\/\/(?:m|www|vm)\.tiktok\.com\/.*\b(?:(?:usr|v|embed|user|video|t)\/" - r"|\?shareId=|\&item_id=)(\d+)\b" + r"|\?shareId=|\&item_id=)(\d+)(\b|\S+\b)" ) - vm_pattern = r"\bhttps?:\/\/(?:vm|vt)\.tiktok\.com\/.*\b(\S+)\b" + + shortcode_pattern = r"\bhttps?:\/\/(?:vm|vt|www)\.tiktok\.com\/(t/|)(\w+)/?" validated_urls = [ - f"https://m.tiktok.com/v/{match.group(1)}" for match in regex.finditer(pattern, text) + f"https://m.tiktok.com/v/{match.group(1)}" + for match in regex.finditer(video_id_pattern, text) ] validated_urls.extend( - f"https://vm.tiktok.com/{match.group(1)}" for match in regex.finditer(vm_pattern, text) + f"https://vm.tiktok.com/{match.group(1)}" + for match in regex.finditer(shortcode_pattern, text) ) return validated_urls From ecb440778672cf4c3b00af8c68ee07024d56d1a8 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Thu, 15 Jun 2023 12:06:11 +0300 Subject: [PATCH 07/40] Fix shortcode tiktok regex using wrong group --- modules/media_embedders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/media_embedders.py b/modules/media_embedders.py index cb805ab..2fe1ab6 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -275,7 +275,7 @@ def extract_links(text: str): for match in regex.finditer(video_id_pattern, text) ] validated_urls.extend( - f"https://vm.tiktok.com/{match.group(1)}" + f"https://vm.tiktok.com/{match.group(2)}" for match in regex.finditer(shortcode_pattern, text) ) From 1c2712e8d767c4e208235d02344fbced61aef5be Mon Sep 17 00:00:00 2001 From: Joinemm Date: Thu, 15 Jun 2023 15:43:25 +0300 Subject: [PATCH 08/40] Move documents to docs --- privacy-policy.md => docs/privacy-policy.md | 0 terms_of_service.md => docs/terms-of-service.md | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename privacy-policy.md => docs/privacy-policy.md (100%) rename terms_of_service.md => docs/terms-of-service.md (100%) diff --git a/privacy-policy.md b/docs/privacy-policy.md similarity index 100% rename from privacy-policy.md rename to docs/privacy-policy.md diff --git a/terms_of_service.md b/docs/terms-of-service.md similarity index 100% rename from terms_of_service.md rename to docs/terms-of-service.md From 98c6832587822c2fde22941225edabbd6043278c Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sun, 2 Jul 2023 20:10:27 +0300 Subject: [PATCH 09/40] fix twitter --- modules/media_embedders.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/modules/media_embedders.py b/modules/media_embedders.py index 2fe1ab6..69b3fa0 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -15,11 +15,11 @@ from discord.ext import commands from discord.ui import View from loguru import logger - -from modules import emojis, exceptions, instagram, util from modules.misobot import MisoBot from modules.tiktok import TikTok +from modules import emojis, exceptions, instagram, util + @dataclass class InstagramPost: @@ -371,7 +371,15 @@ async def create_message( f"Tweet `{tweet['url']}` does not include any media.", ) - timestamp = arrow.get(tweet["created_timestamp"]) + username = discord.utils.escape_markdown(screen_name) + caption = f"{self.EMOJI} **@{username}**" + + try: + timestamp = arrow.get(tweet["created_timestamp"]) + ts_format = timestamp.format("YYMMDD") + caption += f" " + except KeyError: + ts_format = "000000" tasks = [] for n, (extension, media_url) in enumerate(media_urls, start=1): @@ -387,8 +395,6 @@ async def create_message( ) ) - username = discord.utils.escape_markdown(screen_name) - caption = f"{self.EMOJI} **@{username}** " if options and options.captions: caption += f"\n>>> {tweet['text']}" From f29b7a25a0571db7f89c6884d88d4b7f805e1293 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sun, 2 Jul 2023 20:36:04 +0300 Subject: [PATCH 10/40] fix twitter --- modules/media_embedders.py | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/media_embedders.py b/modules/media_embedders.py index 69b3fa0..9df19b3 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -383,7 +383,6 @@ async def create_message( tasks = [] for n, (extension, media_url) in enumerate(media_urls, start=1): - ts_format = timestamp.format("YYMMDD") filename = f"{ts_format}-@{screen_name}-{tweet_id}-{n}.{extension}" tasks.append( self.download_media( From e585c402f034ec3d20e34f61ee68a6454bfd4379 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Thu, 6 Jul 2023 11:42:22 +0300 Subject: [PATCH 11/40] Update tiktok regex to work without subdomain --- modules/media_embedders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/media_embedders.py b/modules/media_embedders.py index 9df19b3..c00689a 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -264,7 +264,7 @@ def __init__(self, bot: "MisoBot"): def extract_links(text: str): text = "\n".join(text.split()) video_id_pattern = ( - r"\bhttps?:\/\/(?:m|www|vm)\.tiktok\.com\/.*\b(?:(?:usr|v|embed|user|video|t)\/" + r"\bhttps?:\/\/(?:m\.|www\.|vm\.|)tiktok\.com\/.*\b(?:(?:usr|v|embed|user|video|t)\/" r"|\?shareId=|\&item_id=)(\d+)(\b|\S+\b)" ) From c82c90e09e9f39eed71aef199eec84fdac412b4b Mon Sep 17 00:00:00 2001 From: Joinemm Date: Thu, 6 Jul 2023 12:32:34 +0300 Subject: [PATCH 12/40] Fix weather not overriding user location if inline argument was given --- cogs/utility.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cogs/utility.py b/cogs/utility.py index b8959a9..1923721 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -15,12 +15,12 @@ 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 +from modules import emojis, exceptions, queries, util + papago_pairs = [ "ko/en", "ko/ja", @@ -354,7 +354,9 @@ async def weather(self, ctx: commands.Context): @weather.command(name="now") async def weather_now(self, ctx: commands.Context, *, location: Optional[str] = None): - location = await self.get_user_location(ctx) + if location is None: + location = await self.get_user_location(ctx) + lat, lon, address = await self.geolocate(location) local_time, country_code = await self.get_country_information(lat, lon) @@ -454,7 +456,9 @@ def render(F: bool): @weather.command(name="forecast") async def weather_forecast(self, ctx: commands.Context, *, location: Optional[str] = None): - location = await self.get_user_location(ctx) + if location is None: + location = await self.get_user_location(ctx) + lat, lon, address = await self.geolocate(location) local_time, country_code = await self.get_country_information(lat, lon) body = { From 8af03660c2a7004c0d781c02c1f7118e0b587f38 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Fri, 7 Jul 2023 22:10:33 +0300 Subject: [PATCH 13/40] Fix daily chart --- cogs/lastfm.py | 26 ++++++++++++++++++-------- modules/misobot.py | 10 +++++++--- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/cogs/lastfm.py b/cogs/lastfm.py index a28a22c..9f3ec49 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" @@ -1117,7 +1117,11 @@ async def chart(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( { @@ -2176,7 +2180,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( @@ -2184,7 +2188,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"]) @@ -2195,7 +2199,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, } ) @@ -2217,6 +2221,8 @@ async def custom_period(self, user, group_by, shift_hours=24): } albumsdata = sorted(formatted_data.values(), key=lambda x: x["playcount"], reverse=True) + if limit: + albumsdata = albumsdata[:limit] return { "topalbums": { "album": albumsdata, @@ -2227,7 +2233,7 @@ async def custom_period(self, user, group_by, shift_hours=24): } } - if group_by in ["track", "user.gettoptracks"]: + elif group_by in ["track", "user.gettoptracks"]: for track in data["recenttracks"]["track"]: track_name = track["name"] artist_name = track["artist"]["#text"] @@ -2242,6 +2248,8 @@ async def custom_period(self, user, group_by, shift_hours=24): } tracksdata = sorted(formatted_data.values(), key=lambda x: x["playcount"], reverse=True) + if limit: + tracksdata = tracksdata[:limit] return { "toptracks": { "track": tracksdata, @@ -2252,7 +2260,7 @@ async def custom_period(self, user, group_by, shift_hours=24): } } - if group_by in ["artist", "user.gettopartists"]: + elif group_by in ["artist", "user.gettopartists"]: for track in data["recenttracks"]["track"]: artist_name = track["artist"]["#text"] if artist_name in formatted_data: @@ -2265,6 +2273,8 @@ async def custom_period(self, user, group_by, shift_hours=24): } artistdata = sorted(formatted_data.values(), key=lambda x: x["playcount"], reverse=True) + if limit: + artistdata = artistdata[:limit] return { "topartists": { "artist": artistdata, diff --git a/modules/misobot.py b/modules/misobot.py index 5f68e4c..77488d5 100644 --- a/modules/misobot.py +++ b/modules/misobot.py @@ -13,13 +13,13 @@ from discord.errors import Forbidden from discord.ext import commands from loguru import logger - -from modules import cache, maria, util from modules.help import EmbedHelpCommand from modules.instagram import Datalama from modules.keychain import Keychain from modules.redis import Redis +from modules import cache, maria, util + class MisoBot(commands.AutoShardedBot): def __init__(self, extensions: list[str], default_prefix: str, **kwargs: dict[str, Any]): @@ -80,7 +80,11 @@ async def setup_hook(self): ) await self.redis.start() await self.db.initialize_pool() - await self.cache.initialize_settings_cache() + try: + await self.cache.initialize_settings_cache() + except Exception as e: + logger.error(e) + await self.load_all_extensions() self.boot_up_time = time() - self.start_time From c746b1949ef8667558fce0a259a7ae4c7d59d3ed Mon Sep 17 00:00:00 2001 From: Joinemm Date: Mon, 10 Jul 2023 14:01:40 +0300 Subject: [PATCH 14/40] Add ability to export custom commands as json --- cogs/customcommands.py | 71 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/cogs/customcommands.py b/cogs/customcommands.py index 69c00dc..c3c6570 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"): @@ -214,17 +215,31 @@ async def command_list(self, ctx: commands.Context): else: 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) @@ -235,10 +250,48 @@ 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!" if await self.bot.db.fetch_value( @@ -253,8 +306,8 @@ async def import_command(self, ctx: commands.Context, name, text): ctx.guild.id, name, text, - arrow.utcnow().datetime, - ctx.author.id, + arrow.get(added_on).datetime, + owner_id, ) return True, name From b6fed8caad5dd3b3293fbdc671ce27a45399e05c Mon Sep 17 00:00:00 2001 From: Joinemm Date: Thu, 13 Jul 2023 16:54:02 +0300 Subject: [PATCH 15/40] Improve documentation of some commands --- cogs/configuration.py | 16 +++++++++++++++- cogs/information.py | 3 ++- cogs/media.py | 37 +++++++++++++++++++++++++------------ 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/cogs/configuration.py b/cogs/configuration.py index 5678889..84b73b7 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): @@ -96,6 +96,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 @@ -136,6 +143,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 diff --git a/cogs/information.py b/cogs/information.py index e9f6829..df642c3 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): @@ -204,6 +204,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") diff --git a/cogs/media.py b/cogs/media.py index 83376b6..6e6ee51 100644 --- a/cogs/media.py +++ b/cogs/media.py @@ -10,11 +10,11 @@ import orjson from bs4 import BeautifulSoup from discord.ext import commands - -from modules import emojis, exceptions, util from modules.media_embedders import InstagramEmbedder, TikTokEmbedder, TwitterEmbedder from modules.misobot import MisoBot +from modules import emojis, exceptions, util + class Media(commands.Cog): """Fetch various media""" @@ -51,22 +51,26 @@ async def youtube(self, ctx: commands.Context, *, query): index_entries=True, ) - @commands.command() + @commands.command(usage=" ") async def autoembedder( self, ctx: commands.Context, provider: Literal["instagram", "tiktok"], - state: Optional[bool] = None, + enabled: Optional[bool] = None, ): """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 + + Example: + >autoembedder instagram on + >autoembedder tiktok off """ if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") - if state is None: + if enabled is None: # show current state data = await self.bot.db.fetch_value( f""" @@ -91,7 +95,7 @@ async def autoembedder( ) # set new state - if state: + if enabled: # check for donation status if trying to turn on await util.patron_check(ctx) @@ -103,8 +107,8 @@ async def autoembedder( {provider} = %s """, ctx.guild.id, - state, - state, + enabled, + enabled, ) await self.bot.cache.cache_auto_embedders() @@ -112,20 +116,29 @@ async def autoembedder( await util.send_success( ctx, f"{provider.capitalize()} automatic embeds are now " - f"**{'ON' if state else 'OFF'}** for this server", + f"**{'ON' if enabled else 'OFF'}** for this server", ) - @commands.command(aliases=["ig", "insta"]) + @commands.command( + aliases=["ig", "insta"], + usage="[-c | --caption] [-s | --spoiler] [-d | --delete] ", + ) async def instagram(self, ctx: commands.Context, *, links: str): """Retrieve media from Instagram post, reel or story""" await InstagramEmbedder(self.bot).process(ctx, links) - @commands.command(aliases=["twt"]) + @commands.command( + aliases=["twt"], + usage="[-c | --caption] [-s | --spoiler] [-d | --delete] ", + ) async def twitter(self, ctx: commands.Context, *, links: str): """Retrieve media from a tweet""" await TwitterEmbedder(self.bot).process(ctx, links) - @commands.command(aliases=["tik", "tok", "tt"]) + @commands.command( + aliases=["tik", "tok", "tt"], + usage="[-c | --caption] [-s | --spoiler] [-d | --delete] ", + ) async def tiktok(self, ctx: commands.Context, *, links: str): """Retrieve video without watermark from a TikTok""" await TikTokEmbedder(self.bot).process(ctx, links) From c97209d27d5af85246ccf1799df5e909582baf3f Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sat, 15 Jul 2023 12:54:02 +0300 Subject: [PATCH 16/40] Explain the media options better --- cogs/media.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/cogs/media.py b/cogs/media.py index 6e6ee51..e34aae1 100644 --- a/cogs/media.py +++ b/cogs/media.py @@ -121,26 +121,44 @@ async def autoembedder( @commands.command( aliases=["ig", "insta"], - usage="[-c | --caption] [-s | --spoiler] [-d | --delete] ", + 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"], - usage="[-c | --caption] [-s | --spoiler] [-d | --delete] ", + 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"], - usage="[-c | --caption] [-s | --spoiler] [-d | --delete] ", + 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"]) From f35d9922f167b6ad4bb7e03529931ce75dc4eabf Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sat, 15 Jul 2023 13:03:57 +0300 Subject: [PATCH 17/40] Calculate instagram cache lifetime more accurately --- modules/instagram.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/instagram.py b/modules/instagram.py index 68da360..e1886eb 100644 --- a/modules/instagram.py +++ b/modules/instagram.py @@ -104,6 +104,10 @@ def make_cache_key(self, endpoint: str, params: dict): def get_url_expiry(media_url: str): return int(parse.parse_qs(parse.urlparse(media_url).query)["oe"][0], 16) + @staticmethod + def calculate_post_lifetime(media: list) -> int: + return min([m.expires for m in media]) - arrow.utcnow().int_timestamp + async def api_request_with_cache(self, endpoint: str, params: dict) -> tuple[dict, bool, str]: cache_key = self.make_cache_key(endpoint, params) @@ -172,9 +176,7 @@ async def get_post_v1(self, shortcode: str) -> IgPost: media = self.parse_resource_v1(data) if not was_cached and media: - lifetime = ( - (media[0].expires - arrow.utcnow().int_timestamp) if media[0].expires else 86400 - ) + lifetime = self.calculate_post_lifetime(media) await self.save_cache(cache_key, data, lifetime) return IgPost( @@ -214,9 +216,7 @@ async def get_story_v1(self, username: str, story_pk: str) -> IgPost: raise TypeError(f"Unknown IG media type {data['media_type']}") if not was_cached and media: - lifetime = ( - (media[0].expires - arrow.utcnow().int_timestamp) if media[0].expires else 86400 - ) + lifetime = self.calculate_post_lifetime(media) await self.save_cache(cache_key, data, lifetime) timestamp = int(arrow.get(data["taken_at"]).timestamp()) From 48d08baa88344b8bb4b9b8245f964376e126f96e Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sat, 22 Jul 2023 18:49:22 +0300 Subject: [PATCH 18/40] Refactor the help embeds a bit to make more sense --- modules/help.py | 76 +++++++++++++++++++++++-------------------------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/modules/help.py b/modules/help.py index 318d950..c89ed45 100644 --- a/modules/help.py +++ b/modules/help.py @@ -12,19 +12,17 @@ class EmbedHelpCommand(commands.HelpCommand): # Set the embed colour here COLOUR = int("ee84ca", 16) - def get_command_signature(self, command): - return f"{self.context.clean_prefix}{command.qualified_name} {command.signature}" + def get_command_signature(self, command: commands.Command): + sig = " ".join( + reversed([f"{p.name} {p.signature}".strip() for p in [command] + command.parents]) + ) + return self.context.clean_prefix + sig - def get_subcommands(self, c, depth=1): + def get_subcommands(self, c, depth=0): this_cmd = "" if hasattr(c, "commands"): for subc in c.commands: - this_cmd += "\n" - this_cmd += f"{' '*depth}└ **{subc.name}**" - # + ( - # f"\n{' '*(depth+1)}{subc.short_doc}" if subc.short_doc is not None else "-" - # ) - this_cmd += self.get_subcommands(subc, depth + 1) + this_cmd += f"\n{' '*depth}└ **{subc.name}**{self.get_subcommands(subc, depth + 1)}" return this_cmd @@ -52,60 +50,56 @@ async def send_bot_help(self, mapping): async def send_cog_help(self, cog): embed = discord.Embed( - title=(f"{cog.icon} " if hasattr(cog, "icon") else "") + cog.qualified_name, + title=f"{cog.icon} {cog.qualified_name}", colour=self.COLOUR, + description=cog.description or "", ) - if cog.description: - embed.description = cog.description - filtered = await self.filter_commands(cog.get_commands(), sort=True) - for command in filtered: + filtered_commands = await self.filter_commands(cog.get_commands(), sort=True) + for command in filtered_commands: embed.add_field( name=f"{self.get_command_signature(command)}", - value=(command.short_doc or "no description") + self.get_subcommands(command), + value=(command.help or command.short_doc or "") + self.get_subcommands(command), inline=False, ) embed.set_footer(text=f"{self.context.clean_prefix}help [command] for more details.") await self.get_destination().send(embed=embed) - async def send_group_help(self, group): - embed = discord.Embed(title=group.qualified_name, colour=self.COLOUR) - if group.help: - embed.description = group.help - elif group.short_doc: - embed.description = group.short_doc - - if isinstance(group, commands.Group): - filtered = await self.filter_commands(group.commands, sort=True) - for command in filtered: - embed.add_field( - name=f"{self.get_command_signature(command)}", - value="<:blank:749966895293268048>" - + (command.short_doc or "...") - + self.get_subcommands(command), - inline=False, - ) + async def send_group_help(self, group: commands.Group): + embed = discord.Embed( + title=f"{group.cog.icon} {self.get_command_signature(group)} [subcommand]", + colour=self.COLOUR, + description=group.help or group.short_doc or "", + ) - embed.set_footer(text=f"{self.context.clean_prefix}help [command] for more details.") + embed.description += "\n\n:small_orange_diamond: __**subcommands**__ :small_orange_diamond:" + filtered_commands = await self.filter_commands(group.commands, sort=True) + for command in filtered_commands: + embed.add_field( + name=f"{command.name} {command.signature}", + value=f"{command.short_doc} {self.get_subcommands(command)}", + inline=False, + ) + + embed.set_footer( + text=( + f"{self.context.clean_prefix}help {group.qualified_name} " + "[subcommand] for more details." + ) + ) await self.get_destination().send(embed=embed) async def send_command_help(self, command): embed = discord.Embed( - title=f"{self.get_command_signature(command)}", + title=f"{command.cog.icon} {self.get_command_signature(command)}", colour=self.COLOUR, + description=command.help or command.brief or "", ) if command.aliases: embed.set_footer(text="Aliases: " + ", ".join(command.aliases)) - if command.help: - embed.description = command.help - elif command.brief: - embed.description = command.brief - else: - embed.description = "..." - await self.get_destination().send(embed=embed) async def group_help_brief(self, ctx: commands.Context, group): From 1f0e3d5205684004a939e5e9887f98d717b1f5e0 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sat, 22 Jul 2023 18:49:49 +0300 Subject: [PATCH 19/40] Make autoembedder configurable --- cogs/events.py | 62 +++++++++++++--- cogs/media.py | 140 ++++++++++++++++++++++++++----------- modules/cache.py | 2 +- modules/media_embedders.py | 23 ++++-- sql/init/0_schema.sql | 12 +++- 5 files changed, 181 insertions(+), 58 deletions(-) diff --git a/cogs/events.py b/cogs/events.py index 81ab3a5..29d1099 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""" @@ -216,7 +216,7 @@ async def on_member_remove(self, member): # goodbye message goodbye = await self.bot.db.fetch_row( """ - SELECT channel_id, is_enabled, message_format + SELECT channel_id, is_enabled, message_format FROM goodbye_settings WHERE guild_id = %s """, member.guild.id, @@ -303,24 +303,64 @@ async def on_message(self, message: discord.Message): if self.bot.cache.autoresponse.get(str(message.guild.id), True): await self.easter_eggs(message) + 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 + else: + 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): diff --git a/cogs/media.py b/cogs/media.py index e34aae1..d3677b6 100644 --- a/cogs/media.py +++ b/cogs/media.py @@ -4,13 +4,18 @@ 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 InstagramEmbedder, TikTokEmbedder, TwitterEmbedder +from modules.media_embedders import ( + BaseEmbedder, + InstagramEmbedder, + TikTokEmbedder, + TwitterEmbedder, +) from modules.misobot import MisoBot from modules import emojis, exceptions, util @@ -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): @@ -51,72 +56,129 @@ async def youtube(self, ctx: commands.Context, *, query): index_entries=True, ) - @commands.command(usage=" ") - async def autoembedder( - self, - ctx: commands.Context, - provider: Literal["instagram", "tiktok"], - enabled: Optional[bool] = None, - ): + @util.patrons_only() + @commands.group() + async def autoembedder(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 - - Example: - >autoembedder instagram on - >autoembedder tiktok off + without requiring the use of the corresponding command """ if ctx.guild is None: raise exceptions.CommandError("Unable to get current guild") - if enabled 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( + title=f"{provider.capitalize()} autoembedder", description=( - f"{provider.capitalize()} automatic embeds are " - f"currently **{current_state}** for this server" - ) + 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 enabled: - # 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, - enabled, - enabled, + not data, + not data, ) await self.bot.cache.cache_auto_embedders() await util.send_success( ctx, - f"{provider.capitalize()} automatic embeds are now " - f"**{'ON' if enabled 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", + ) + + @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( diff --git a/modules/cache.py b/modules/cache.py index 124d746..7aaeced 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -92,7 +92,7 @@ async def cache_autoroles(self): async def cache_auto_embedders(self): media_embed_settings = await self.bot.db.fetch( - "SELECT guild_id, instagram, twitter, tiktok, reddit FROM media_auto_embed_settings" + "SELECT guild_id, instagram, twitter, tiktok, reddit FROM media_auto_embed_enabled" ) if media_embed_settings: for guild_id, instagram, twitter, tiktok, reddit in media_embed_settings: diff --git a/modules/media_embedders.py b/modules/media_embedders.py index c00689a..a71ce87 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -37,6 +37,7 @@ class Options: captions: bool = False delete_after: bool = False spoiler: bool = False + sanitized_string: str = "" def filesize_limit(guild: discord.Guild | None): @@ -56,18 +57,24 @@ def __init__(self, bot) -> None: self.bot: MisoBot = bot @staticmethod - def get_options(text: str): + def get_options(text: str) -> Options: options = Options() + valid_options = [] words = text.lower().split() + if "-c" in words or "--caption" in words: options.captions = True + valid_options.append("-c") if "-d" in words or "--delete" in words: options.delete_after = True + valid_options.append("-d") if "-s" in words or "--spoiler" in words: options.spoiler = True + valid_options.append("-s") + options.sanitized_string = " ".join(valid_options) return options async def process(self, ctx: commands.Context, user_input: str): @@ -150,17 +157,23 @@ async def send(self, ctx: commands.Context, media: Any, options: Options | None message_contents["view"].approved_deletors.append(ctx.author) async def send_contextless( - self, channel: "discord.abc.MessageableChannel", author: discord.User, media: Any + self, + channel: "discord.abc.MessageableChannel", + author: discord.User, + media: Any, + options: Options | None = None, ): """Send the media without relying on command context, for example in a message event""" - message_contents = await self.create_message(channel, media) + message_contents = await self.create_message(channel, media, options=options) msg = await channel.send(**message_contents) message_contents["view"].message_ref = msg message_contents["view"].approved_deletors.append(author) - async def send_reply(self, message: discord.Message, media: Any): + async def send_reply( + self, message: discord.Message, media: Any, options: Options | None = None + ): """Send the media as a reply to another message""" - message_contents = await self.create_message(message.channel, media) + message_contents = await self.create_message(message.channel, media, options=options) msg = await message.reply(**message_contents, mention_author=False) message_contents["view"].message_ref = msg message_contents["view"].approved_deletors.append(message.author) diff --git a/sql/init/0_schema.sql b/sql/init/0_schema.sql index a5e4786..21dafa9 100644 --- a/sql/init/0_schema.sql +++ b/sql/init/0_schema.sql @@ -250,7 +250,15 @@ CREATE TABLE IF NOT EXISTS guild_settings ( PRIMARY KEY (guild_id) ); -CREATE TABLE IF NOT EXISTS media_auto_embed_settings ( +CREATE TABLE IF NOT EXISTS media_auto_embed_options ( + guild_id BIGINT, + provider ENUM('instagram', 'twitter', 'tiktok', 'reddit') NOT NULL, + options VARCHAR(32), + reply BOOLEAN DEFAULT TRUE, + PRIMARY KEY (guild_id, provider) +); + +CREATE TABLE IF NOT EXISTS media_auto_embed_enabled ( guild_id BIGINT, instagram BOOLEAN DEFAULT FALSE, twitter BOOLEAN DEFAULT FALSE, @@ -398,4 +406,4 @@ CREATE TABLE IF NOT EXISTS colorizer_settings ( baserole_id BIGINT, enabled BOOLEAN DEFAULT FALSE, PRIMARY KEY (guild_id) -); \ No newline at end of file +); From 06d7c18343ecf27ce3186e53d62bb1161f159e5a Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sun, 23 Jul 2023 13:07:05 +0300 Subject: [PATCH 20/40] Remove the redundant t.co link from twitter text --- modules/media_embedders.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/modules/media_embedders.py b/modules/media_embedders.py index a71ce87..6cee010 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -332,6 +332,23 @@ class TwitterEmbedder(BaseEmbedder): EMOJI = "<:twitter:937425165241946162>" NO_RESULTS_ERROR = "Found no Twitter links to embed!" + @staticmethod + def remove_tco(text: str) -> str: + """Get rid of the t.co link to the same tweet""" + if text.startswith("https://t.co"): + # the caption is only the link + return "" + + try: + pre_text, tco = text.rsplit(maxsplit=1) + except ValueError: + return text + + if tco.startswith("https://t.co"): + return pre_text + else: + return pre_text + " " + tco + @staticmethod def extract_links(text: str, include_id_only=True): text = "\n".join(text.split()) @@ -389,14 +406,14 @@ async def create_message( try: timestamp = arrow.get(tweet["created_timestamp"]) - ts_format = timestamp.format("YYMMDD") + ts_format = timestamp.format("YYMMDD") + "-" caption += f" " except KeyError: - ts_format = "000000" + ts_format = "" tasks = [] for n, (extension, media_url) in enumerate(media_urls, start=1): - filename = f"{ts_format}-@{screen_name}-{tweet_id}-{n}.{extension}" + filename = f"{ts_format}@{screen_name}-{tweet_id}-{n}.{extension}" tasks.append( self.download_media( media_url, @@ -407,9 +424,6 @@ async def create_message( ) ) - if options and options.captions: - caption += f"\n>>> {tweet['text']}" - files = [] too_big_files = [] results = await asyncio.gather(*tasks) @@ -419,6 +433,11 @@ async def create_message( else: too_big_files.append(result) + if options and options.captions: + tweet_text = self.remove_tco(tweet["text"]) + if tweet_text: + caption += f"\n>>> {tweet_text}" + caption = "\n".join([caption] + too_big_files) return { "content": caption, From bfdcd23608174af52de57532204158d191b1aa2d Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sun, 23 Jul 2023 14:40:53 +0300 Subject: [PATCH 21/40] Fix multiple shortcodes in one call of >ig --- modules/media_embedders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/media_embedders.py b/modules/media_embedders.py index 6cee010..8d9e5cf 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -210,7 +210,7 @@ def extract_links(text: str, include_shortcodes=True) -> list[InstagramPost | In results.append(InstagramPost(match.group(6))) if include_shortcodes: - shortcode_regex = r"^([a-zA-Z0-9\-\_\.]+)$" + shortcode_regex = r"(?:\s|^)([a-zA-Z0-9\-\_\.]+)(?=\s|$)" for match in regex.finditer(shortcode_regex, text): results.append(InstagramPost(match.group(1))) From 58849e8ba119a6871dcdb8a0bcb55963d78ad92e Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sun, 23 Jul 2023 15:17:01 +0300 Subject: [PATCH 22/40] Improve error handling on instagram api --- modules/instagram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/instagram.py b/modules/instagram.py index e1886eb..f9f40b7 100644 --- a/modules/instagram.py +++ b/modules/instagram.py @@ -157,7 +157,8 @@ async def api_request(self, endpoint: str, params: dict) -> dict: or data.get("detail") is not None ): raise InstagramError( - f"ERROR {response.status} | {data.get('exc_type')} | {data.get('detail')}" + f"API returned **{response.status} {data.get('detail')}**" + f"```json\n{params}```" ) return data From 2fc515ac61f9839618bf0e916edbbfa9be60519d Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sun, 23 Jul 2023 17:08:48 +0300 Subject: [PATCH 23/40] Dont parse instagram options as shortcodes --- modules/media_embedders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/media_embedders.py b/modules/media_embedders.py index 8d9e5cf..8fa2b40 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -210,7 +210,7 @@ def extract_links(text: str, include_shortcodes=True) -> list[InstagramPost | In results.append(InstagramPost(match.group(6))) if include_shortcodes: - shortcode_regex = r"(?:\s|^)([a-zA-Z0-9\-\_\.]+)(?=\s|$)" + shortcode_regex = r"(?:\s|^)([^-][a-zA-Z0-9\-\_\.]{9,})(?=\s|$)" for match in regex.finditer(shortcode_regex, text): results.append(InstagramPost(match.group(1))) From c4e3bfd4c2a14e00b9cb32fdfaa4ff5fd4f206c4 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sun, 23 Jul 2023 19:33:56 +0300 Subject: [PATCH 24/40] update dependencies --- dev-requirements.txt | 24 +++++++++++------------ requirements.txt | 46 ++++++++++++++++++++++---------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 8a41c87..7e66a71 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,23 +4,23 @@ # # pip-compile --resolver=backtracking dev-requirements.in # -astroid==2.15.5 +astroid==2.15.6 # via pylint -black==23.3.0 +black==23.7.0 # via -r dev-requirements.in cfgv==3.3.1 # via pre-commit -click==8.1.3 +click==8.1.6 # via # -c requirements.txt # black -dill==0.3.6 +dill==0.3.7 # via pylint -distlib==0.3.6 +distlib==0.3.7 # via virtualenv filelock==3.12.2 # via virtualenv -identify==2.5.24 +identify==2.5.26 # via pre-commit isort==5.12.0 # via @@ -42,24 +42,24 @@ packaging==23.1 # black pathspec==0.11.1 # via black -platformdirs==3.5.3 +platformdirs==3.9.1 # via # black # pylint # virtualenv -pre-commit==3.3.2 +pre-commit==3.3.3 # via -r dev-requirements.in pylint==2.17.4 # via -r dev-requirements.in -pyright==1.1.313 +pyright==1.1.318 # via -r dev-requirements.in -pyyaml==6.0 +pyyaml==6.0.1 # via pre-commit -ruff==0.0.272 +ruff==0.0.280 # via -r dev-requirements.in tomlkit==0.11.8 # via pylint -virtualenv==20.23.0 +virtualenv==20.24.1 # via pre-commit wrapt==1.15.0 # via diff --git a/requirements.txt b/requirements.txt index 3fc5a93..f276552 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiodns==3.0.0 # via discord-py aiofiles==22.1.0 # via shazamio -aiohttp==3.8.4 +aiohttp==3.8.5 # via # -r requirements.in # aiohttp-cors @@ -21,7 +21,7 @@ aiomysql==0.2.0 # via -r requirements.in aiosignal==1.3.1 # via aiohttp -anyio==3.7.0 +anyio==3.7.1 # via shazamio arrow==1.2.3 # via -r requirements.in @@ -45,31 +45,31 @@ brotli==1.0.9 # via discord-py cffi==1.15.1 # via pycares -charset-normalizer==3.1.0 +charset-normalizer==3.2.0 # via aiohttp -click==8.1.3 +click==8.1.6 # via jishaku colorgram-py==1.2.0 # via -r requirements.in -contourpy==1.0.7 +contourpy==1.1.0 # via matplotlib cycler==0.11.0 # via matplotlib dataclass-factory==2.16 # via shazamio -discord-py[speed]==2.3.0 +discord-py[speed]==2.3.1 # via -r requirements.in dnspython==2.3.0 # via minestat durations-nlp==1.0.1 # via -r requirements.in -fonttools==4.40.0 +fonttools==4.41.1 # via matplotlib -frozenlist==1.3.3 +frozenlist==1.4.0 # via # aiohttp # aiosignal -humanize==4.6.0 +humanize==4.7.0 # via -r requirements.in idna==3.4 # via @@ -89,11 +89,11 @@ line-profiler==4.0.3 # via jishaku loguru==0.7.0 # via -r requirements.in -lxml==4.9.2 +lxml==4.9.3 # via -r requirements.in markdownify==0.11.6 # via -r requirements.in -matplotlib==3.7.1 +matplotlib==3.7.2 # via -r requirements.in minestat==2.6.1 # via -r requirements.in @@ -101,29 +101,29 @@ multidict==6.0.4 # via # aiohttp # yarl -numpy==1.24.3 +numpy==1.25.1 # via # -r requirements.in # contourpy # matplotlib # scipy # shazamio -orjson==3.9.1 +orjson==3.9.2 # via discord-py packaging==23.1 # via # matplotlib # pytest -pillow==9.5.0 +pillow==10.0.0 # via # -r requirements.in # colorgram-py # matplotlib -pluggy==1.0.0 +pluggy==1.2.0 # via pytest prometheus-async==22.2.0 # via -r requirements.in -prometheus-client==0.17.0 +prometheus-client==0.17.1 # via prometheus-async psutil==5.9.5 # via -r requirements.in @@ -131,15 +131,15 @@ pycares==4.3.0 # via aiodns pycparser==2.21 # via cffi -pydantic==1.10.9 +pydantic==1.10.11 # via shazamio pydub==0.25.1 # via shazamio -pymysql==1.0.3 +pymysql==1.1.0 # via aiomysql pyparsing==3.0.9 # via matplotlib -pytest==7.3.2 +pytest==7.4.0 # via # pytest-asyncio # shazamio @@ -153,11 +153,11 @@ python-dotenv==1.0.0 # via -r requirements.in random-user-agent==1.0.1 # via -r requirements.in -redis==4.5.5 +redis==4.6.0 # via -r requirements.in regex==2023.6.3 # via -r requirements.in -scipy==1.10.1 +scipy==1.11.1 # via -r requirements.in shazamio==0.4.0.1 # via -r requirements.in @@ -171,13 +171,13 @@ sniffio==1.3.0 # via anyio soupsieve==2.4.1 # via beautifulsoup4 -typing-extensions==4.6.3 +typing-extensions==4.7.1 # via pydantic uvloop==0.17.0 # via -r requirements.in webencodings==0.5.1 # via bleach -wheel==0.40.0 +wheel==0.41.0 # via astunparse wrapt==1.15.0 # via prometheus-async From de3e83e1432b0ef4b5ca85ae1f8522a8de0dc987 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Sun, 23 Jul 2023 22:32:05 +0300 Subject: [PATCH 25/40] Fix some deepsource code quality issues --- cogs/events.py | 4 ++-- cogs/lastfm.py | 8 +++----- cogs/media.py | 6 ------ cogs/mod.py | 21 +++++++++++---------- cogs/notifications.py | 6 +++--- cogs/owner.py | 4 ++-- cogs/utility.py | 1 - modules/instagram.py | 2 +- modules/media_embedders.py | 9 ++++----- 9 files changed, 26 insertions(+), 35 deletions(-) diff --git a/cogs/events.py b/cogs/events.py index 29d1099..6a229c4 100644 --- a/cogs/events.py +++ b/cogs/events.py @@ -317,8 +317,8 @@ async def get_autoembed_options( print(options_data) if options_data: return options_data - else: - return None, None + + return None, None async def parse_media_auto_embed(self, message: discord.Message, media_settings: dict): if media_settings["instagram"]: diff --git a/cogs/lastfm.py b/cogs/lastfm.py index 9f3ec49..5b47670 100644 --- a/cogs/lastfm.py +++ b/cogs/lastfm.py @@ -1955,7 +1955,7 @@ def check(message): except ValueError: return False else: - return num <= len(results) and num > 0 + return len(results) <= num > 0 else: return False @@ -2050,7 +2050,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( @@ -2107,7 +2106,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): @@ -2233,7 +2231,7 @@ async def custom_period(self, user, group_by, shift_hours=24, limit=None): } } - elif group_by in ["track", "user.gettoptracks"]: + if group_by in ["track", "user.gettoptracks"]: for track in data["recenttracks"]["track"]: track_name = track["name"] artist_name = track["artist"]["#text"] @@ -2260,7 +2258,7 @@ async def custom_period(self, user, group_by, shift_hours=24, limit=None): } } - elif group_by in ["artist", "user.gettopartists"]: + if group_by in ["artist", "user.gettopartists"]: for track in data["recenttracks"]["track"]: artist_name = track["artist"]["#text"] if artist_name in formatted_data: diff --git a/cogs/media.py b/cogs/media.py index d3677b6..0836ede 100644 --- a/cogs/media.py +++ b/cogs/media.py @@ -367,12 +367,6 @@ async def confirm(self, interaction: discord.Interaction, _button: discord.ui.Bu 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/mod.py b/cogs/mod.py index 12151c4..c22f30f 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): @@ -178,21 +178,22 @@ async def timeout(self, ctx: commands.Context, member: discord.Member, *, durati if duration and duration.strip().lower() == "remove": await member.timeout(None) 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 " 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() diff --git a/cogs/notifications.py b/cogs/notifications.py index 602b0b6..ff93721 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): @@ -271,8 +271,8 @@ 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 + SELECT guild_id, keyword, times_triggered + FROM notification WHERE user_id = %s ORDER BY keyword """, ctx.author.id, diff --git a/cogs/owner.py b/cogs/owner.py index 8380719..c4e6727 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): @@ -137,7 +137,7 @@ async def donator_add( await self.bot.db.execute( """ - INSERT INTO donator (user_id, platform, external_username, + INSERT INTO donator (user_id, platform, external_username, donation_tier, donating_since, amount) VALUES (%s, %s, %s, %s, %s, %s) """, diff --git a/cogs/utility.py b/cogs/utility.py index 1923721..61aa1b6 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -406,7 +406,6 @@ async def weather_now(self, ctx: commands.Context, *, location: Optional[str] = daily_data = next(filter(lambda t: t["timestep"] == "1d", data["data"]["timelines"])) values_current = current_data["intervals"][0]["values"] values_today = daily_data["intervals"][0]["values"] - # values_tomorrow = daily_data["intervals"][1]["values"] temperature = values_current["temperature"] temperature_apparent = values_current["temperatureApparent"] sunrise = arrow.get(values_current["sunriseTime"]).to(local_time.tzinfo).format("HH:mm") diff --git a/modules/instagram.py b/modules/instagram.py index f9f40b7..e9716fb 100644 --- a/modules/instagram.py +++ b/modules/instagram.py @@ -106,7 +106,7 @@ def get_url_expiry(media_url: str): @staticmethod def calculate_post_lifetime(media: list) -> int: - return min([m.expires for m in media]) - arrow.utcnow().int_timestamp + return min(m.expires for m in media) - arrow.utcnow().int_timestamp async def api_request_with_cache(self, endpoint: str, params: dict) -> tuple[dict, bool, str]: cache_key = self.make_cache_key(endpoint, params) diff --git a/modules/media_embedders.py b/modules/media_embedders.py index 8fa2b40..12cfb3a 100644 --- a/modules/media_embedders.py +++ b/modules/media_embedders.py @@ -341,13 +341,12 @@ def remove_tco(text: str) -> str: try: pre_text, tco = text.rsplit(maxsplit=1) + if tco.startswith("https://t.co"): + return pre_text except ValueError: - return text + pass - if tco.startswith("https://t.co"): - return pre_text - else: - return pre_text + " " + tco + return text @staticmethod def extract_links(text: str, include_id_only=True): From 7bda1b44000541d04b7003f08e0ec9ef017f7efe Mon Sep 17 00:00:00 2001 From: Joinemm Date: Wed, 26 Jul 2023 21:03:24 +0300 Subject: [PATCH 26/40] Don't show multiline help message in cog help --- modules/help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/help.py b/modules/help.py index c89ed45..6e3c366 100644 --- a/modules/help.py +++ b/modules/help.py @@ -59,7 +59,7 @@ async def send_cog_help(self, cog): for command in filtered_commands: embed.add_field( name=f"{self.get_command_signature(command)}", - value=(command.help or command.short_doc or "") + self.get_subcommands(command), + value=(command.short_doc or "") + self.get_subcommands(command), inline=False, ) From d4888a32a5e9a9fc0ee16638dd9c6bda76c719e2 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Wed, 26 Jul 2023 21:09:31 +0300 Subject: [PATCH 27/40] Add leaderboard for most fishy gifted --- cogs/user.py | 44 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/cogs/user.py b/cogs/user.py index ca5780c..0a32733 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): @@ -317,9 +317,49 @@ 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") From 4bffa1d0dfba8a7d8514c2edd5ce014f45500105 Mon Sep 17 00:00:00 2001 From: Joinemm Date: Wed, 26 Jul 2023 21:40:41 +0300 Subject: [PATCH 28/40] Change the black line length to default 88 --- .pre-commit-config.yaml | 6 +- cogs/configuration.py | 155 +++++++++++++++----- cogs/customcommands.py | 57 ++++++-- cogs/errorhandler.py | 60 ++++++-- cogs/events.py | 48 +++++-- cogs/fishy.py | 22 ++- cogs/information.py | 63 +++++++-- cogs/kpop.py | 23 ++- cogs/lastfm.py | 283 ++++++++++++++++++++++++++++--------- cogs/media.py | 35 +++-- cogs/misc.py | 57 +++++--- cogs/mod.py | 65 +++++++-- cogs/notifications.py | 53 +++++-- cogs/owner.py | 52 +++++-- cogs/roles.py | 46 ++++-- cogs/typings.py | 66 ++++++--- cogs/user.py | 123 ++++++++++++---- cogs/utility.py | 146 ++++++++++++++----- cogs/webserver.py | 13 +- main.py | 4 +- modules/cache.py | 25 +++- modules/exceptions.py | 8 +- modules/genius.py | 9 +- modules/help.py | 20 ++- modules/instagram.py | 37 +++-- modules/maria.py | 4 +- modules/media_embedders.py | 39 +++-- modules/misobot.py | 16 ++- modules/queries.py | 4 +- modules/tiktok.py | 12 +- modules/ui.py | 20 ++- modules/util.py | 36 +++-- pyproject.toml | 5 +- 33 files changed, 1204 insertions(+), 408 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 163a097..e77b9c6 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] diff --git a/cogs/configuration.py b/cogs/configuration.py index 84b73b7..64c4b7f 100644 --- a/cogs/configuration.py +++ b/cogs/configuration.py @@ -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}") @@ -132,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): @@ -171,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 @@ -188,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 @@ -209,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): @@ -218,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 @@ -240,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") @@ -250,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") @@ -276,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() @@ -288,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 @@ -372,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") @@ -399,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") @@ -415,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") @@ -426,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, @@ -446,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", @@ -476,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() @@ -496,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): @@ -557,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) @@ -639,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: @@ -707,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)", @@ -715,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(), @@ -727,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!") @@ -764,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: @@ -812,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( """ @@ -821,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) @@ -848,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 c3c6570..c480055 100644 --- a/cogs/customcommands.py +++ b/cogs/customcommands.py @@ -58,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() @@ -107,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, @@ -153,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 ( @@ -173,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): @@ -207,13 +218,16 @@ 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" + ) @command.command(name="import") @commands.has_permissions(manage_guild=True) @@ -233,7 +247,9 @@ async def command_import(self, ctx: commands.Context): ``` """ if not ctx.message.attachments: - raise exceptions.CommandWarning("Please attach a `.json` file to the message") + raise exceptions.CommandWarning( + "Please attach a `.json` file to the message" + ) jsonfile = ctx.message.attachments[0] imported = json.loads(await jsonfile.read()) @@ -264,7 +280,9 @@ async def command_export(self, ctx: commands.Context): ctx.guild.id, ) if not data: - 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" + ) jsondata = [ { @@ -299,7 +317,10 @@ async def import_command(self, ctx: commands.Context, command: dict): 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)", @@ -315,7 +336,9 @@ async def import_command(self, ctx: commands.Context, command: dict): @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." @@ -345,7 +368,9 @@ 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 = 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**." @@ -370,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 6d1b47d..23476cd 100644 --- a/cogs/errorhandler.py +++ b/cogs/errorhandler.py @@ -8,21 +8,27 @@ 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.`" @@ -35,7 +41,9 @@ def __init__(self, bot): self.bot: MisoBot = bot @staticmethod - def log_format(ctx: commands.Context, error: Exception | None, message: str | None = None): + 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)}" @@ -68,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) @@ -91,9 +109,13 @@ 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 = ( @@ -112,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, @@ -145,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) @@ -161,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 @@ -255,7 +283,9 @@ 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:") diff --git a/cogs/events.py b/cogs/events.py index 6a229c4..5c9ea9e 100644 --- a/cogs/events.py +++ b/cogs/events.py @@ -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: @@ -168,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 @@ -200,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: @@ -231,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 @@ -262,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) @@ -320,7 +334,9 @@ async def get_autoembed_options( return None, None - async def parse_media_auto_embed(self, message: discord.Message, media_settings: dict): + 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) @@ -430,7 +446,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 @@ -499,7 +517,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 @@ -549,7 +569,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: @@ -570,7 +592,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 df642c3..d63d80b 100644 --- a/cogs/information.py +++ b/cogs/information.py @@ -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) @@ -258,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): @@ -273,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) @@ -288,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) @@ -357,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( @@ -396,9 +423,13 @@ 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") @@ -479,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) + 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 a668d2f..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 "" @@ -158,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})", @@ -208,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 @@ -232,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 5b47670..c262557 100644 --- a/cogs/lastfm.py +++ b/cogs/lastfm.py @@ -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() @@ -577,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. @@ -596,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!") @@ -680,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: @@ -728,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) @@ -747,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(), @@ -776,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) @@ -789,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)}" @@ -801,7 +827,9 @@ async def artist_overview(self, ctx: commands.Context, period, artistname): soup = BeautifulSoup(data, "lxml") try: - albumsdiv, tracksdiv, _ = soup.findAll("tbody", {"data-playlisting-add-entries": ""}) + albumsdiv, tracksdiv, _ = soup.findAll( + "tbody", {"data-playlisting-add-entries": ""} + ) except ValueError: artistname = discord.utils.escape_markdown(artistname) @@ -814,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", "") @@ -834,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) @@ -889,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) @@ -910,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 ( @@ -982,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( @@ -1006,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, " @@ -1054,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): @@ -1085,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" @@ -1102,7 +1146,9 @@ async def colorchart(self, ctx: commands.Context, colour, size="3x3"): if warn is not None: await warn.delete() - @fm.command(aliases=["collage"], usage="[album | artist] [timeframe] [size] 'notitle'") + @fm.command( + aliases=["collage"], usage="[album | artist] [timeframe] [size] 'notitle'" + ) async def chart(self, ctx: commands.Context, *args): """ Collage of your top albums or artists @@ -1156,7 +1202,9 @@ async def chart(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" @@ -1183,11 +1231,15 @@ async def chart(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, @@ -1204,7 +1256,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: @@ -1232,7 +1286,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): """ @@ -1268,7 +1324,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 = [] @@ -1286,7 +1344,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" @@ -1344,12 +1405,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 @@ -1399,12 +1466,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 = "" @@ -1447,7 +1520,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) @@ -1464,7 +1541,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() @@ -1473,7 +1552,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 ): @@ -1503,7 +1584,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) @@ -1521,7 +1606,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() @@ -1530,7 +1617,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, @@ -1541,7 +1630,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) @@ -1560,7 +1651,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) @@ -1578,7 +1673,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() @@ -1587,7 +1684,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, @@ -1598,7 +1697,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) @@ -1654,7 +1755,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 = [] @@ -1674,7 +1777,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) @@ -1716,7 +1821,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) @@ -1733,7 +1840,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) @@ -1751,14 +1860,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 = [] @@ -1769,7 +1882,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) @@ -1778,7 +1893,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) @@ -1809,7 +1926,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) @@ -1827,14 +1946,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 = [] @@ -1845,7 +1968,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) @@ -1854,7 +1979,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) @@ -1930,7 +2057,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) @@ -2157,7 +2286,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( @@ -2218,7 +2349,9 @@ async def custom_period(self, user, group_by, shift_hours=24, limit=None): "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 { @@ -2245,7 +2378,9 @@ async def custom_period(self, user, group_by, shift_hours=24, limit=None): "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 { @@ -2270,7 +2405,9 @@ async def custom_period(self, user, group_by, shift_hours=24, limit=None): "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 { @@ -2292,7 +2429,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"], @@ -2426,7 +2567,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 @@ -2462,7 +2605,8 @@ async def scrape_artists_for_chart(self, username, period, amount): soup = BeautifulSoup(data, "lxml") imagedivs = soup.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 @@ -2649,7 +2793,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)" diff --git a/cogs/media.py b/cogs/media.py index 0836ede..7a40d2e 100644 --- a/cogs/media.py +++ b/cogs/media.py @@ -50,7 +50,10 @@ 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, @@ -58,7 +61,9 @@ async def youtube(self, ctx: commands.Context, *, query): @util.patrons_only() @commands.group() - async def autoembedder(self, ctx: commands.Context, provider: Literal["instagram", "tiktok"]): + async def autoembedder( + 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, @@ -236,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 = [] @@ -305,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( @@ -353,17 +366,23 @@ 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() diff --git a/cogs/misc.py b/cogs/misc.py index 30db1ac..94965e8 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 = [] @@ -439,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", @@ -462,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: @@ -525,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): @@ -540,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, @@ -571,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)): @@ -763,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() @@ -784,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: @@ -811,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): @@ -924,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: @@ -994,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 c22f30f..a9393ef 100644 --- a/cogs/mod.py +++ b/cogs/mod.py @@ -107,7 +107,9 @@ async def check_mutes(self): ) ) except discord.errors.Forbidden: - logger.warning("Unable to send unmuting message due to missing permissions!") + logger.warning( + "Unable to send unmuting message due to missing permissions!" + ) @commands.command(aliases=["clean"], usage=" [@mentions...]") @commands.guild_only() @@ -125,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() @@ -154,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 = [] @@ -172,12 +179,16 @@ 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( @@ -198,7 +209,9 @@ async def timeout(self, ctx: commands.Context, member: discord.Member, *, durati @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") @@ -227,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) @@ -242,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: @@ -364,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() @@ -435,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(): @@ -458,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() @@ -485,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 = [] @@ -510,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 ff93721..80787b2 100644 --- a/cogs/notifications.py +++ b/cogs/notifications.py @@ -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( @@ -147,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): @@ -214,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): @@ -264,7 +277,9 @@ 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): @@ -292,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): @@ -317,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( """ @@ -326,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() @@ -361,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 c4e6727..e9a651e 100644 --- a/cogs/owner.py +++ b/cogs/owner.py @@ -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: @@ -176,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( """ @@ -185,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 f94795e..3de1f5a 100644 --- a/cogs/roles.py +++ b/cogs/roles.py @@ -5,11 +5,11 @@ import asyncio import discord +from cogs.errorhandler import ErrorHander 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) @@ -145,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() @@ -199,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: @@ -270,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: @@ -336,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, @@ -366,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!" @@ -376,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): @@ -430,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( diff --git a/cogs/typings.py b/cogs/typings.py index 7eb14e0..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 @@ -194,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: @@ -206,7 +224,9 @@ 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 @@ -216,7 +236,11 @@ def check(_reaction, _user): 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( @@ -239,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 @@ -302,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(): @@ -334,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 0a32733..c735e26 100644 --- a/cogs/user.py +++ b/cogs/user.py @@ -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", @@ -351,7 +391,8 @@ async def leaderboard_fishy_gifted(self, ctx: commands.Context, scope=""): content = discord.Embed( title=( - f":fish: {'Global' if global_data else ctx.guild.name} " "gifted fishy leaderboard" + f":fish: {'Global' if global_data else ctx.guild.name} " + "gifted fishy leaderboard" ), color=int("55acee", 16), ) @@ -382,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!") @@ -412,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 @@ -461,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."] @@ -474,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: @@ -597,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): @@ -652,7 +705,9 @@ async def marry(self, ctx: commands.Context, user: discord.Member): ), ) ) - 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)) @@ -683,7 +738,9 @@ async def divorce(self, ctx: commands.Context): 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) + partner = ctx.guild.get_member(partner) or await util.find_user( + self.bot, partner + ) content = discord.Embed( description=":broken_heart:" @@ -716,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() @@ -740,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 diff --git a/cogs/utility.py b/cogs/utility.py index 61aa1b6..551d806 100644 --- a/cogs/utility.py +++ b/cogs/utility.py @@ -137,7 +137,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_fmt} for being over 6 hours late") + logger.info( + f"Deleting reminder set for {date_fmt} for being over 6 hours late" + ) else: embed = discord.Embed( color=int("d3a940", 16), @@ -155,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}") @@ -174,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 @@ -244,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() @@ -253,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( @@ -271,7 +279,9 @@ async def shazam(self, ctx: commands.Context, url_or_attachment: Optional[str]): content.set_thumbnail(url=result.cover_art) await ctx.send(embed=content) - @commands.command(usage="<'in' | 'on'>