From 05fbc0af6e4cb6b58432d0b7c111ca6b2db5ee57 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Thu, 10 Jun 2021 16:36:41 +0530 Subject: Add modpings schedule feature --- bot/exts/moderation/modpings.py | 82 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 1ad5005de..17afe3b77 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -1,8 +1,9 @@ +import asyncio import datetime import logging from async_rediscache import RedisCache -from dateutil.parser import isoparse +from dateutil.parser import isoparse, parse from discord import Embed, Member from discord.ext.commands import Cog, Context, group, has_any_role @@ -13,6 +14,7 @@ from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) +MINIMUM_WORK_LIMIT = 16 class ModPings(Cog): """Commands for a moderator to turn moderator pings on and off.""" @@ -22,6 +24,12 @@ class ModPings(Cog): # The cache's values are the times when the role should be re-applied to them, stored in ISO format. pings_off_mods = RedisCache() + + # RedisCache[discord.Member.id, 'start timestamp|total worktime in seconds'] + # The cache's keys are mod's ID + # The cache's values are their pings on schedule timestamp and the total seconds (work time) until pings off + modpings_schedule = RedisCache() + def __init__(self, bot: Bot): self.bot = bot self._role_scheduler = Scheduler(self.__class__.__name__) @@ -30,6 +38,7 @@ class ModPings(Cog): self.moderators_role = None self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="mod-pings-reschedule") + self.modpings_schedule_task = self.bot.loop.create_task(self.reschedule_modpings_schedule()) async def reschedule_roles(self) -> None: """Reschedule moderators role re-apply times.""" @@ -55,6 +64,50 @@ class ModPings(Cog): expiry = isoparse(pings_off[mod.id]).replace(tzinfo=None) self._role_scheduler.schedule_at(expiry, mod.id, self.reapply_role(mod)) + async def reschedule_modpings_schedule(self) -> None: + """Reschedule moderators schedule ping.""" + await self.bot.wait_until_guild_available() + schedule_cache = await self.modpings_schedule.to_dict() + + log.info("Scheduling modpings schedule for applicable moderators found in cache.") + for mod_id, schedule in schedule_cache: + start_timestamp, work_time = schedule.split("|") + start = datetime.datetime.fromtimestamp(start_timestamp) + + mod = self.bot.fetch_user(mod_id) + self._role_scheduler.schedule_at( + start, + mod_id, + self.add_role_schedule(mod, work_time, start) + ) + + async def remove_role_schedule(self, mod: Member, work_time: int, schedule_start: datetime.datetime) -> None: + """Removes the moderator's role to the given moderator.""" + log.trace(f"Removing moderator role to mod with ID {mod.id}") + await mod.remove_roles(self.moderators_role, reason="Moderator schedule time expired.") + + # Add the task again + log.trace(f"Adding mod pings schedule task again for mod with ID {mod.id}") + schedule_start += datetime.timedelta(minutes=1) + self._role_scheduler.schedule_at( + schedule_start, + mod.id, + self.add_role_schedule(mod, work_time, schedule_start) + ) + + async def add_role_schedule(self, mod: Member, work_time: int, schedule_start: datetime.datetime) -> None: + """Adds the moderator's role to the given moderator.""" + # If the moderator has pings off, then skip adding role + if mod in await self.pings_off_mods.to_dict(): + log.trace(f"Skipping adding moderator role to mod with ID {mod.id}") + else: + log.trace(f"Applying moderator role to mod with ID {mod.id}") + await mod.add_roles(self.moderators_role, reason="Moderator schedule time started!") + + log.trace(f"Sleeping for {work_time} seconds, worktime for mod with ID {mod.id}") + await asyncio.sleep(work_time) + await self.remove_role_schedule(mod, work_time, schedule_start) + async def reapply_role(self, mod: Member) -> None: """Reapply the moderator's role to the given moderator.""" log.trace(f"Re-applying role to mod with ID {mod.id}.") @@ -126,6 +179,33 @@ class ModPings(Cog): await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") + @modpings_group.command(name='schedule') + @has_any_role(*MODERATION_ROLES) + async def schedule_modpings(self, ctx: Context, start: str, end: str) -> None: + start, end = parse(start), parse(end) + + if end < start: + end += datetime.timedelta(days=1) + + if (end - start) < datetime.timedelta(hours=MINIMUM_WORK_LIMIT): + await ctx.send(f":x: {ctx.author.mention} You need to have the role on for a minimum of {MINIMUM_WORK_LIMIT} hours!") + return + + start, end = start.replace(tzinfo=None), end.replace(tzinfo=None) + work_time = (end - start).total_seconds() + + await self.modpings_schedule.set(ctx.author.id, f"{start.timestamp()}|{work_time}") + + self._role_scheduler.schedule_at( + start, + ctx.author.id, + self.add_role_schedule(ctx.author, work_time, start) + ) + + await ctx.send( + f"{Emojis.ok_hand} {ctx.author.mention} Scheduled mod pings from {start: %I:%M%p} to {end: %I:%M%p} UTC Timing!" + ) + def cog_unload(self) -> None: """Cancel role tasks when the cog unloads.""" log.trace("Cog unload: canceling role tasks.") -- cgit v1.2.3 From a97f0680e70769e4a59015bf5e791198936a9c7b Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Fri, 11 Jun 2021 07:19:04 +0530 Subject: (modpings): Cancel the task before scheduling it again --- bot/exts/moderation/modpings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 17afe3b77..2aff2ded2 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -86,6 +86,9 @@ class ModPings(Cog): log.trace(f"Removing moderator role to mod with ID {mod.id}") await mod.remove_roles(self.moderators_role, reason="Moderator schedule time expired.") + # Remove the task before scheduling it again + self._role_scheduler.cancel(mod.id) + # Add the task again log.trace(f"Adding mod pings schedule task again for mod with ID {mod.id}") schedule_start += datetime.timedelta(minutes=1) -- cgit v1.2.3 From 547f8837cf6f8cabcdac4209c373bc68776c3fc4 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Fri, 11 Jun 2021 07:50:05 +0530 Subject: (modpings): Use separate scheduler for modpings schedule --- bot/exts/moderation/modpings.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 2aff2ded2..f5ce9160d 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -32,7 +32,8 @@ class ModPings(Cog): def __init__(self, bot: Bot): self.bot = bot - self._role_scheduler = Scheduler(self.__class__.__name__) + self._role_scheduler = Scheduler("ModPingsOnOff") + self._modpings_scheduler = Scheduler("ModPingsSchedule") self.guild = None self.moderators_role = None @@ -75,7 +76,7 @@ class ModPings(Cog): start = datetime.datetime.fromtimestamp(start_timestamp) mod = self.bot.fetch_user(mod_id) - self._role_scheduler.schedule_at( + self._modpings_scheduler.schedule_at( start, mod_id, self.add_role_schedule(mod, work_time, start) @@ -87,12 +88,12 @@ class ModPings(Cog): await mod.remove_roles(self.moderators_role, reason="Moderator schedule time expired.") # Remove the task before scheduling it again - self._role_scheduler.cancel(mod.id) + self._modpings_scheduler.cancel(mod.id) # Add the task again log.trace(f"Adding mod pings schedule task again for mod with ID {mod.id}") schedule_start += datetime.timedelta(minutes=1) - self._role_scheduler.schedule_at( + self._modpings_scheduler.schedule_at( schedule_start, mod.id, self.add_role_schedule(mod, work_time, schedule_start) @@ -101,8 +102,8 @@ class ModPings(Cog): async def add_role_schedule(self, mod: Member, work_time: int, schedule_start: datetime.datetime) -> None: """Adds the moderator's role to the given moderator.""" # If the moderator has pings off, then skip adding role - if mod in await self.pings_off_mods.to_dict(): - log.trace(f"Skipping adding moderator role to mod with ID {mod.id}") + if mod.id in await self.pings_off_mods.to_dict(): + log.trace(f"Skipping adding moderator role to mod with ID {mod.id} - found in pings off cache.") else: log.trace(f"Applying moderator role to mod with ID {mod.id}") await mod.add_roles(self.moderators_role, reason="Moderator schedule time started!") @@ -199,7 +200,7 @@ class ModPings(Cog): await self.modpings_schedule.set(ctx.author.id, f"{start.timestamp()}|{work_time}") - self._role_scheduler.schedule_at( + self._modpings_scheduler.schedule_at( start, ctx.author.id, self.add_role_schedule(ctx.author, work_time, start) -- cgit v1.2.3 From f8fa9ba626a404aa825b3554ba136cf4196bd87c Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Fri, 11 Jun 2021 07:54:09 +0530 Subject: (modpings): Make flake8 happy! --- bot/exts/moderation/modpings.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index f5ce9160d..1154bce9c 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -16,6 +16,7 @@ log = logging.getLogger(__name__) MINIMUM_WORK_LIMIT = 16 + class ModPings(Cog): """Commands for a moderator to turn moderator pings on and off.""" @@ -24,7 +25,6 @@ class ModPings(Cog): # The cache's values are the times when the role should be re-applied to them, stored in ISO format. pings_off_mods = RedisCache() - # RedisCache[discord.Member.id, 'start timestamp|total worktime in seconds'] # The cache's keys are mod's ID # The cache's values are their pings on schedule timestamp and the total seconds (work time) until pings off @@ -186,13 +186,17 @@ class ModPings(Cog): @modpings_group.command(name='schedule') @has_any_role(*MODERATION_ROLES) async def schedule_modpings(self, ctx: Context, start: str, end: str) -> None: + """Schedule modpings role to be added at and removed at everyday at UTC time!""" start, end = parse(start), parse(end) if end < start: end += datetime.timedelta(days=1) if (end - start) < datetime.timedelta(hours=MINIMUM_WORK_LIMIT): - await ctx.send(f":x: {ctx.author.mention} You need to have the role on for a minimum of {MINIMUM_WORK_LIMIT} hours!") + await ctx.send( + f":x: {ctx.author.mention} You need to have the role on for " + f"a minimum of {MINIMUM_WORK_LIMIT} hours!" + ) return start, end = start.replace(tzinfo=None), end.replace(tzinfo=None) @@ -207,7 +211,8 @@ class ModPings(Cog): ) await ctx.send( - f"{Emojis.ok_hand} {ctx.author.mention} Scheduled mod pings from {start: %I:%M%p} to {end: %I:%M%p} UTC Timing!" + f"{Emojis.ok_hand} {ctx.author.mention} Scheduled mod pings from " + f"{start: %I:%M%p} to {end: %I:%M%p} UTC Timing!" ) def cog_unload(self) -> None: -- cgit v1.2.3 From 9bf22be683fa6e1b6ae542855e983bf360ed1b20 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Fri, 11 Jun 2021 16:04:21 +0530 Subject: (modpings): Cancel scheduler and tasks on cog unload --- bot/exts/moderation/modpings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 1154bce9c..bafd40580 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -221,6 +221,9 @@ class ModPings(Cog): self.reschedule_task.cancel() self._role_scheduler.cancel_all() + self.modpings_schedule_task.cancel() + self._modpings_scheduler.cancel_all() + def setup(bot: Bot) -> None: """Load the ModPings cog.""" -- cgit v1.2.3 From 8e39e8bbc71e3964046b05022be9bc0060c6d75c Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Sun, 13 Jun 2021 07:25:34 +0530 Subject: (modpings): Use 24 hour format --- bot/exts/moderation/modpings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index bafd40580..0922d068f 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -212,7 +212,7 @@ class ModPings(Cog): await ctx.send( f"{Emojis.ok_hand} {ctx.author.mention} Scheduled mod pings from " - f"{start: %I:%M%p} to {end: %I:%M%p} UTC Timing!" + f"{start: %H:%M} to {end: %H:%M} UTC Timing!" ) def cog_unload(self) -> None: -- cgit v1.2.3 From 4aa45f6dd3ca0aba401731ff21c242df60fb2c94 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Sun, 13 Jun 2021 07:53:59 +0530 Subject: (modpings): Use scheduling.create_task wrapper --- bot/exts/moderation/modpings.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 0922d068f..f0a1ce590 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -10,7 +10,7 @@ from discord.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.constants import Colours, Emojis, Guild, Icons, MODERATION_ROLES, Roles from bot.converters import Expiry -from bot.utils.scheduling import Scheduler +from bot.utils.scheduling import Scheduler, create_task log = logging.getLogger(__name__) @@ -39,7 +39,7 @@ class ModPings(Cog): self.moderators_role = None self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="mod-pings-reschedule") - self.modpings_schedule_task = self.bot.loop.create_task(self.reschedule_modpings_schedule()) + self.modpings_schedule_task = create_task(self.reschedule_modpings_schedule(), event_loop=self.bot.loop) async def reschedule_roles(self) -> None: """Reschedule moderators role re-apply times.""" @@ -92,7 +92,7 @@ class ModPings(Cog): # Add the task again log.trace(f"Adding mod pings schedule task again for mod with ID {mod.id}") - schedule_start += datetime.timedelta(minutes=1) + schedule_start += datetime.timedelta(day=1) self._modpings_scheduler.schedule_at( schedule_start, mod.id, @@ -192,12 +192,12 @@ class ModPings(Cog): if end < start: end += datetime.timedelta(days=1) - if (end - start) < datetime.timedelta(hours=MINIMUM_WORK_LIMIT): - await ctx.send( - f":x: {ctx.author.mention} You need to have the role on for " - f"a minimum of {MINIMUM_WORK_LIMIT} hours!" - ) - return + # if (end - start) < datetime.timedelta(hours=MINIMUM_WORK_LIMIT): + # await ctx.send( + # f":x: {ctx.author.mention} You need to have the role on for " + # f"a minimum of {MINIMUM_WORK_LIMIT} hours!" + # ) + # return start, end = start.replace(tzinfo=None), end.replace(tzinfo=None) work_time = (end - start).total_seconds() -- cgit v1.2.3 From ced656c7518e35626e17d165976283f3d894d722 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Sun, 13 Jun 2021 12:27:03 +0530 Subject: (modpings): 16 hours is the maximum schedule limit --- bot/exts/moderation/modpings.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index f0a1ce590..c0e742699 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -14,7 +14,7 @@ from bot.utils.scheduling import Scheduler, create_task log = logging.getLogger(__name__) -MINIMUM_WORK_LIMIT = 16 +MAXIMUM_WORK_LIMIT = 16 class ModPings(Cog): @@ -192,12 +192,12 @@ class ModPings(Cog): if end < start: end += datetime.timedelta(days=1) - # if (end - start) < datetime.timedelta(hours=MINIMUM_WORK_LIMIT): - # await ctx.send( - # f":x: {ctx.author.mention} You need to have the role on for " - # f"a minimum of {MINIMUM_WORK_LIMIT} hours!" - # ) - # return + if (end - start) > datetime.timedelta(hours=MAXIMUM_WORK_LIMIT): + await ctx.send( + f":x: {ctx.author.mention} You can't have the modpings role for" + f" more than {MAXIMUM_WORK_LIMIT} hours!" + ) + return start, end = start.replace(tzinfo=None), end.replace(tzinfo=None) work_time = (end - start).total_seconds() -- cgit v1.2.3 From 5b6500aa1630d774c13d40ef317b8b5e4f07d6da Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Sun, 13 Jun 2021 12:39:25 +0530 Subject: (modpings): Add subcommand to delete your modpings schedule --- bot/exts/moderation/modpings.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index c0e742699..9b7843e20 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -183,7 +183,11 @@ class ModPings(Cog): await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") - @modpings_group.command(name='schedule') + @modpings_group.group( + name='schedule', + aliases=('s',), + invoke_without_command=True + ) @has_any_role(*MODERATION_ROLES) async def schedule_modpings(self, ctx: Context, start: str, end: str) -> None: """Schedule modpings role to be added at and removed at everyday at UTC time!""" @@ -215,6 +219,14 @@ class ModPings(Cog): f"{start: %H:%M} to {end: %H:%M} UTC Timing!" ) + @schedule_modpings.command(name='delete', aliases=('del', 'd')) + async def modpings_schedule_delete(self, ctx: Context): + """Delete your modpings schedule.""" + self._modpings_scheduler.cancel(ctx.author.id) + await self.modpings_schedule.delete(ctx.author.id) + await ctx.send(f"{Emojis.ok_hand} {ctx.author.mention} Deleted your modpings schedule!") + + def cog_unload(self) -> None: """Cancel role tasks when the cog unloads.""" log.trace("Cog unload: canceling role tasks.") -- cgit v1.2.3 From 99e597584516c1495e165647e5b2c131e232175f Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Sun, 13 Jun 2021 13:20:08 +0530 Subject: (modpings): Add a day to datetime if already passed --- bot/exts/moderation/modpings.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 9b7843e20..207480a68 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -92,7 +92,7 @@ class ModPings(Cog): # Add the task again log.trace(f"Adding mod pings schedule task again for mod with ID {mod.id}") - schedule_start += datetime.timedelta(day=1) + schedule_start += datetime.timedelta(days=1) self._modpings_scheduler.schedule_at( schedule_start, mod.id, @@ -203,11 +203,19 @@ class ModPings(Cog): ) return + if start < datetime.datetime.utcnow(): + # The datetime has already gone for the day, so make it tomorrow + # otherwise the scheduler would schedule it immediately + start += datetime.timedelta(days=1) + start, end = start.replace(tzinfo=None), end.replace(tzinfo=None) work_time = (end - start).total_seconds() await self.modpings_schedule.set(ctx.author.id, f"{start.timestamp()}|{work_time}") + if ctx.author.id in self._modpings_scheduler: + self._modpings_scheduler.cancel(ctx.author.id) + self._modpings_scheduler.schedule_at( start, ctx.author.id, -- cgit v1.2.3 From b47228ee03b1f39b91209ed04d39e66c1bee9b54 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Sun, 13 Jun 2021 13:21:38 +0530 Subject: (modpings): Make flake8 happy! --- bot/exts/moderation/modpings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 207480a68..cf45a2182 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -228,13 +228,12 @@ class ModPings(Cog): ) @schedule_modpings.command(name='delete', aliases=('del', 'd')) - async def modpings_schedule_delete(self, ctx: Context): + async def modpings_schedule_delete(self, ctx: Context) -> None: """Delete your modpings schedule.""" self._modpings_scheduler.cancel(ctx.author.id) await self.modpings_schedule.delete(ctx.author.id) await ctx.send(f"{Emojis.ok_hand} {ctx.author.mention} Deleted your modpings schedule!") - def cog_unload(self) -> None: """Cancel role tasks when the cog unloads.""" log.trace("Cog unload: canceling role tasks.") -- cgit v1.2.3 From c301f3e2a5a3ca80f88ff13539bebd86f95a8833 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 30 Jun 2021 02:35:07 +0200 Subject: Base functionality of tag fetching with groups and in file metadata The code was restructured to hold tags and their identifiers in individual classes and some methods moved to function to detach some of the not directly related functionality from the cog class --- bot/exts/backend/error_handler.py | 12 +- bot/exts/info/tags.py | 244 ++++++++++++++++++++++++-------------- 2 files changed, 167 insertions(+), 89 deletions(-) diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index d8de177f5..78822aece 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -11,6 +11,7 @@ from bot.bot import Bot from bot.constants import Colours, Icons, MODERATION_ROLES from bot.converters import TagNameConverter from bot.errors import InvalidInfractedUser, LockedResourceError +from bot.exts.info import tags from bot.utils.checks import ContextCheckFailure log = logging.getLogger(__name__) @@ -154,14 +155,21 @@ class ErrorHandler(Cog): return try: - tag_name = await TagNameConverter.convert(ctx, ctx.invoked_with) + tag_identifier = tags.extract_tag_identifier(ctx.message.content) + if tag_identifier.group is not None: + tag_name = await TagNameConverter.convert(ctx, tag_identifier.name) + tag_name_or_group = await TagNameConverter.convert(ctx, tag_identifier.group) + else: + tag_name = None + tag_name_or_group = await TagNameConverter.convert(ctx, tag_identifier.name) + except errors.BadArgument: log.debug( f"{ctx.author} tried to use an invalid command " f"and the fallback tag failed validation in TagNameConverter." ) else: - if await ctx.invoke(tags_get_command, tag_name=tag_name): + if await ctx.invoke(tags_get_command, tag_name_or_group, tag_name): return if not any(role.id in MODERATION_ROLES for role in ctx.author.roles): diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index bb91a8563..3c7b9ea0b 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -1,9 +1,13 @@ +from __future__ import annotations + import logging import re import time from pathlib import Path -from typing import Callable, Dict, Iterable, List, Optional +from typing import Callable, Iterable, List, NamedTuple, Optional +import discord +import frontmatter from discord import Colour, Embed, Member from discord.ext.commands import Cog, Context, group @@ -24,90 +28,128 @@ REGEX_NON_ALPHABET = re.compile(r"[^a-z]", re.MULTILINE & re.IGNORECASE) FOOTER_TEXT = f"To show a tag, type {constants.Bot.prefix}tags ." +class TagIdentifier(NamedTuple): + """Stores the group and name used as an identifier for a tag.""" + + group: Optional[str] + name: str + + def get_fuzzy_score(self, fuzz_tag_identifier: TagIdentifier) -> float: + """Get fuzzy score, using `fuzz_tag_identifier` as the identifier to fuzzy match with.""" + if self.group is None: + if fuzz_tag_identifier.group is None: + # We're only fuzzy matching the name + group_score = 1 + else: + # Ignore tags without groups if the identifier contains a group + return .0 + else: + if fuzz_tag_identifier.group is None: + # Ignore tags with groups if the identifier does not have a group + return .0 + else: + group_score = _fuzzy_search(fuzz_tag_identifier.group, self.group) + + fuzzy_score = group_score * _fuzzy_search(fuzz_tag_identifier.name, self.name) * 100 + if fuzzy_score: + log.trace(f"Fuzzy score {fuzzy_score:=06.2f} for tag {self!r} with fuzz {fuzz_tag_identifier!r}") + return fuzzy_score + + def __str__(self) -> str: + return f"{self.group or ''} {self.name}" + + +class Tag: + """Provide an interface to a tag from resources with `file_content`.""" + + def __init__(self, file_content: str): + post = frontmatter.loads(file_content) + self.content = post.content + self.metadata = post.metadata + self._restricted_to: set[int] = set(self.metadata.get("restricted_to", ())) + + @property + def embed(self) -> Embed: + """Create an embed for the tag.""" + embed = Embed.from_dict(self.metadata.get("embed", {})) + embed.description = self.content + return embed + + def accessible_by(self, member: discord.Member) -> bool: + """Check whether `member` can access the tag.""" + return bool( + not self._restricted_to + or self._restricted_to & {role.id for role in member.roles} + ) + + +def _fuzzy_search(search: str, target: str) -> float: + """A simple scoring algorithm based on how many letters are found / total, with order in mind.""" + current, index = 0, 0 + _search = REGEX_NON_ALPHABET.sub("", search.lower()) + _targets = iter(REGEX_NON_ALPHABET.split(target.lower())) + _target = next(_targets) + try: + while True: + while index < len(_target) and _search[current] == _target[index]: + current += 1 + index += 1 + index, _target = 0, next(_targets) + except (StopIteration, IndexError): + pass + return current / len(_search) + + class Tags(Cog): """Save new tags and fetch existing tags.""" def __init__(self, bot: Bot): self.bot = bot self.tag_cooldowns = {} - self._cache = self.get_tags() - - @staticmethod - def get_tags() -> dict: - """Get all tags.""" - cache = {} + self._tags: dict[TagIdentifier, Tag] = {} + self.initialize_tags() + def initialize_tags(self) -> None: + """Load all tags from resources into `self._tags`.""" base_path = Path("bot", "resources", "tags") + for file in base_path.glob("**/*"): if file.is_file(): - tag_title = file.stem - tag = { - "title": tag_title, - "embed": { - "description": file.read_text(encoding="utf8"), - }, - "restricted_to": None, - "location": f"/bot/{file}" - } - - # Convert to a list to allow negative indexing. - parents = list(file.relative_to(base_path).parents) - if len(parents) > 1: - # -1 would be '.' hence -2 is used as the index. - tag["restricted_to"] = parents[-2].name - - cache[tag_title] = tag - - return cache - - @staticmethod - def check_accessibility(user: Member, tag: dict) -> bool: - """Check if user can access a tag.""" - return not tag["restricted_to"] or tag["restricted_to"].lower() in [role.name.lower() for role in user.roles] - - @staticmethod - def _fuzzy_search(search: str, target: str) -> float: - """A simple scoring algorithm based on how many letters are found / total, with order in mind.""" - current, index = 0, 0 - _search = REGEX_NON_ALPHABET.sub('', search.lower()) - _targets = iter(REGEX_NON_ALPHABET.split(target.lower())) - _target = next(_targets) - try: - while True: - while index < len(_target) and _search[current] == _target[index]: - current += 1 - index += 1 - index, _target = 0, next(_targets) - except (StopIteration, IndexError): - pass - return current / len(_search) * 100 + parent_dir = file.relative_to(base_path).parent - def _get_suggestions(self, tag_name: str, thresholds: Optional[List[int]] = None) -> List[str]: - """Return a list of suggested tags.""" - scores: Dict[str, int] = { - tag_title: Tags._fuzzy_search(tag_name, tag['title']) - for tag_title, tag in self._cache.items() - } + tag_name = file.stem + tag_group = parent_dir.name if parent_dir.name else None + self._tags[TagIdentifier(tag_group, tag_name)] = Tag(file.read_text("utf-8")) + + def _get_suggestions( + self, + tag_identifier: TagIdentifier, + thresholds: Optional[list[int]] = None + ) -> list[tuple[TagIdentifier, Tag]]: + """Return a list of suggested tags for `tag_identifier`.""" thresholds = thresholds or [100, 90, 80, 70, 60] for threshold in thresholds: suggestions = [ - self._cache[tag_title] - for tag_title, matching_score in scores.items() - if matching_score >= threshold + (identifier, tag) + for identifier, tag in self._tags.items() + if identifier.get_fuzzy_score(tag_identifier) >= threshold ] if suggestions: return suggestions return [] - def _get_tag(self, tag_name: str) -> list: - """Get a specific tag.""" - found = [self._cache.get(tag_name.lower(), None)] - if not found[0]: - return self._get_suggestions(tag_name) - return found + def get_fuzzy_matches(self, tag_identifier: TagIdentifier) -> list[tuple[TagIdentifier, Tag]]: + """Get tags with identifiers similar to `tag_identifier`.""" + if tag_identifier.group is None: + suggestions = self._get_suggestions(tag_identifier) + else: + # Try fuzzy matching with only a name first + suggestions = self._get_suggestions(TagIdentifier(None, tag_identifier.group)) + suggestions += self._get_suggestions(tag_identifier) + return suggestions def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str, user: Member) -> list: """ @@ -158,9 +200,14 @@ class Tags(Cog): ) @group(name='tags', aliases=('tag', 't'), invoke_without_command=True) - async def tags_group(self, ctx: Context, *, tag_name: TagNameConverter = None) -> None: + async def tags_group( + self, + ctx: Context, + tag_name_or_group: TagNameConverter = None, + tag_name: TagNameConverter = None, + ) -> None: """Show all known tags, a single tag, or run a subcommand.""" - await self.get_command(ctx, tag_name=tag_name) + await self.get_command(ctx, tag_name_or_group=tag_name_or_group, tag_name=tag_name) @tags_group.group(name='search', invoke_without_command=True) async def search_tag_content(self, ctx: Context, *, keywords: str) -> None: @@ -182,7 +229,7 @@ class Tags(Cog): matching_tags = self._get_tags_via_content(any, keywords or 'any', ctx.author) await self._send_matching_tags(ctx, keywords, matching_tags) - async def display_tag(self, ctx: Context, tag_name: str = None) -> bool: + async def display_tag(self, ctx: Context, tag_identifier: TagIdentifier) -> bool: """ If a tag is not found, display similar tag names as suggestions. @@ -210,45 +257,50 @@ class Tags(Cog): return True return False - if _command_on_cooldown(tag_name): - time_elapsed = time.time() - self.tag_cooldowns[tag_name]["time"] + if _command_on_cooldown(tag_identifier.name): + time_elapsed = time.time() - self.tag_cooldowns[tag_identifier.name]["time"] time_left = constants.Cooldowns.tags - time_elapsed log.info( - f"{ctx.author} tried to get the '{tag_name}' tag, but the tag is on cooldown. " + f"{ctx.author} tried to get the '{tag_identifier.name}' tag, but the tag is on cooldown. " f"Cooldown ends in {time_left:.1f} seconds." ) return True - if tag_name is not None: - temp_founds = self._get_tag(tag_name) - - founds = [] + if tag_identifier.name is not None: - for found_tag in temp_founds: - if self.check_accessibility(ctx.author, found_tag): - founds.append(found_tag) - - if len(founds) == 1: - tag = founds[0] + if (tag := self._tags.get(tag_identifier)) is not None and tag.accessible_by(ctx.author): if ctx.channel.id not in TEST_CHANNELS: - self.tag_cooldowns[tag_name] = { + self.tag_cooldowns[tag_identifier.name] = { "time": time.time(), "channel": ctx.channel.id } - self.bot.stats.incr(f"tags.usages.{tag['title'].replace('-', '_')}") + self.bot.stats.incr( + f"tags.usages" + f"{'.' + tag_identifier.group.replace('-', '_') if tag_identifier.group else ''}" + f".{tag_identifier.name.replace('-', '_')}" + ) await wait_for_deletion( - await ctx.send(embed=Embed.from_dict(tag['embed'])), + await ctx.send(embed=tag.embed), [ctx.author.id], ) return True - elif founds and len(tag_name) >= 3: + + elif len(tag_identifier.name) >= 3: + suggested_tags = self.get_fuzzy_matches(tag_identifier)[:10] + if not suggested_tags: + return False + suggested_tags_text = "\n".join( + str(identifier) + for identifier, tag in suggested_tags + if tag.accessible_by(ctx.author) + ) await wait_for_deletion( await ctx.send( embed=Embed( - title='Did you mean ...', - description='\n'.join(tag['title'] for tag in founds[:10]) + title="Did you mean ...", + description=suggested_tags_text ) ), [ctx.author.id], @@ -281,16 +333,34 @@ class Tags(Cog): return False @tags_group.command(name='get', aliases=('show', 'g')) - async def get_command(self, ctx: Context, *, tag_name: TagNameConverter = None) -> bool: + async def get_command( + self, ctx: Context, + tag_name_or_group: TagNameConverter = None, + tag_name: TagNameConverter = None, + ) -> bool: """ Get a specified tag, or a list of all tags if no tag is specified. Returns True if something can be sent, or if the tag is on cooldown. Returns False if no matches are found. """ - return await self.display_tag(ctx, tag_name) + if tag_name is None: + tag_name = tag_name_or_group + tag_group = None + else: + tag_group = tag_name_or_group + return await self.display_tag(ctx, TagIdentifier(tag_group, tag_name)) def setup(bot: Bot) -> None: """Load the Tags cog.""" bot.add_cog(Tags(bot)) + + +def extract_tag_identifier(string: str) -> TagIdentifier: + """Create a `TagIdentifier` instance from beginning of `string`.""" + split_string = string.removeprefix(constants.Bot.prefix).split(" ", maxsplit=2) + if len(split_string) == 1: + return TagIdentifier(None, split_string[0]) + else: + return TagIdentifier(split_string[0], split_string[1]) -- cgit v1.2.3 From 605768d14d450dd46057c19560599bbbf0d8d597 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 30 Jun 2021 17:00:47 +0200 Subject: Move cooldown handling to the Tag class Instead of the Cog keeping track of cooldowns of all tags, every tag now handles its own cooldowns which are registered with the `set_cooldown_for` method. This change also fixes the bug where cooldowns can only be on cooldown in only one channel at a time, with invokations in other places cancelling cooldowns. --- bot/exts/info/tags.py | 49 +++++++++++++++---------------------------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 3c7b9ea0b..1665275b9 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -67,6 +67,7 @@ class Tag: self.content = post.content self.metadata = post.metadata self._restricted_to: set[int] = set(self.metadata.get("restricted_to", ())) + self._cooldowns: dict[discord.TextChannel, float] = {} @property def embed(self) -> Embed: @@ -82,6 +83,14 @@ class Tag: or self._restricted_to & {role.id for role in member.roles} ) + def on_cooldown_in(self, channel: discord.TextChannel) -> bool: + """Check whether the tag is on cooldown in `channel`.""" + return channel in self._cooldowns and self._cooldowns[channel] > time.time() + + def set_cooldown_for(self, channel: discord.TextChannel) -> None: + """Set the tag to be on cooldown in `channel` for `constants.Cooldowns.tags` seconds.""" + self._cooldowns[channel] = time.time() + constants.Cooldowns.tags + def _fuzzy_search(search: str, target: str) -> float: """A simple scoring algorithm based on how many letters are found / total, with order in mind.""" @@ -105,7 +114,6 @@ class Tags(Cog): def __init__(self, bot: Bot): self.bot = bot - self.tag_cooldowns = {} self._tags: dict[TagIdentifier, Tag] = {} self.initialize_tags() @@ -238,42 +246,14 @@ class Tags(Cog): Tags are on cooldowns on a per-tag, per-channel basis. If a tag is on cooldown, display nothing and return True. """ - def _command_on_cooldown(tag_name: str) -> bool: - """ - Check if the command is currently on cooldown, on a per-tag, per-channel basis. - - The cooldown duration is set in constants.py. - """ - now = time.time() - - cooldown_conditions = ( - tag_name - and tag_name in self.tag_cooldowns - and (now - self.tag_cooldowns[tag_name]["time"]) < constants.Cooldowns.tags - and self.tag_cooldowns[tag_name]["channel"] == ctx.channel.id - ) - - if cooldown_conditions: - return True - return False - - if _command_on_cooldown(tag_identifier.name): - time_elapsed = time.time() - self.tag_cooldowns[tag_identifier.name]["time"] - time_left = constants.Cooldowns.tags - time_elapsed - log.info( - f"{ctx.author} tried to get the '{tag_identifier.name}' tag, but the tag is on cooldown. " - f"Cooldown ends in {time_left:.1f} seconds." - ) - return True - if tag_identifier.name is not None: if (tag := self._tags.get(tag_identifier)) is not None and tag.accessible_by(ctx.author): - if ctx.channel.id not in TEST_CHANNELS: - self.tag_cooldowns[tag_identifier.name] = { - "time": time.time(), - "channel": ctx.channel.id - } + + if tag.on_cooldown_in(ctx.channel): + log.debug(f"Tag {str(tag_identifier)!r} is on cooldown.") + return True + tag.set_cooldown_for(ctx.channel) self.bot.stats.incr( f"tags.usages" @@ -295,6 +275,7 @@ class Tags(Cog): str(identifier) for identifier, tag in suggested_tags if tag.accessible_by(ctx.author) + and not tag.on_cooldown_in(ctx.channel) ) await wait_for_deletion( await ctx.send( -- cgit v1.2.3 From 48e989fe07019401eca6caa6d0eb3981f003eddd Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 30 Jun 2021 21:20:31 +0200 Subject: Move tag listing to new design and move it outside of tag display method The display method was renamed to get_tag_embed and now exclusively handles embed for a tag/suggestions instead of holding the logic of the whole command fixup! Move tag listing to new design and move it outside of tag display method --- bot/exts/info/tags.py | 170 ++++++++++++++++++++++++++++---------------------- 1 file changed, 97 insertions(+), 73 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 1665275b9..4aa590430 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -1,14 +1,15 @@ from __future__ import annotations +import enum import logging import re import time from pathlib import Path -from typing import Callable, Iterable, List, NamedTuple, Optional +from typing import Callable, Iterable, List, Literal, NamedTuple, Optional, Union import discord import frontmatter -from discord import Colour, Embed, Member +from discord import Embed, Member from discord.ext.commands import Cog, Context, group from bot import constants @@ -28,6 +29,12 @@ REGEX_NON_ALPHABET = re.compile(r"[^a-z]", re.MULTILINE & re.IGNORECASE) FOOTER_TEXT = f"To show a tag, type {constants.Bot.prefix}tags ." +class COOLDOWN(enum.Enum): + """Sentinel value to signal that a tag is on cooldown.""" + + obj = object() + + class TagIdentifier(NamedTuple): """Stores the group and name used as an identifier for a tag.""" @@ -237,81 +244,80 @@ class Tags(Cog): matching_tags = self._get_tags_via_content(any, keywords or 'any', ctx.author) await self._send_matching_tags(ctx, keywords, matching_tags) - async def display_tag(self, ctx: Context, tag_identifier: TagIdentifier) -> bool: + async def get_tag_embed( + self, + ctx: Context, + tag_identifier: TagIdentifier, + ) -> Optional[Union[Embed, Literal[COOLDOWN.obj]]]: """ - If a tag is not found, display similar tag names as suggestions. - - If a tag is not specified, display a paginated embed of all tags. + Generate an embed of the requested tag or of suggestions if the tag doesn't exist/isn't accessible by the user. - Tags are on cooldowns on a per-tag, per-channel basis. If a tag is on cooldown, display - nothing and return True. + If the requested tag is on cooldown or no suggestions were found, return None. """ - if tag_identifier.name is not None: + if (tag := self._tags.get(tag_identifier)) is not None and tag.accessible_by(ctx.author): - if (tag := self._tags.get(tag_identifier)) is not None and tag.accessible_by(ctx.author): + if tag.on_cooldown_in(ctx.channel): + log.debug(f"Tag {str(tag_identifier)!r} is on cooldown.") + return COOLDOWN.obj + tag.set_cooldown_for(ctx.channel) - if tag.on_cooldown_in(ctx.channel): - log.debug(f"Tag {str(tag_identifier)!r} is on cooldown.") - return True - tag.set_cooldown_for(ctx.channel) - - self.bot.stats.incr( - f"tags.usages" - f"{'.' + tag_identifier.group.replace('-', '_') if tag_identifier.group else ''}" - f".{tag_identifier.name.replace('-', '_')}" - ) - - await wait_for_deletion( - await ctx.send(embed=tag.embed), - [ctx.author.id], - ) - return True - - elif len(tag_identifier.name) >= 3: - suggested_tags = self.get_fuzzy_matches(tag_identifier)[:10] - if not suggested_tags: - return False - suggested_tags_text = "\n".join( - str(identifier) - for identifier, tag in suggested_tags - if tag.accessible_by(ctx.author) - and not tag.on_cooldown_in(ctx.channel) - ) - await wait_for_deletion( - await ctx.send( - embed=Embed( - title="Did you mean ...", - description=suggested_tags_text - ) - ), - [ctx.author.id], - ) - return True + self.bot.stats.incr( + f"tags.usages" + f"{'.' + tag_identifier.group.replace('-', '_') if tag_identifier.group else ''}" + f".{tag_identifier.name.replace('-', '_')}" + ) + return tag.embed + + elif len(tag_identifier.name) >= 3: + suggested_tags = self.get_fuzzy_matches(tag_identifier)[:10] + if not suggested_tags: + return None + suggested_tags_text = "\n".join( + str(identifier) + for identifier, tag in suggested_tags + if tag.accessible_by(ctx.author) + and not tag.on_cooldown_in(ctx.channel) + ) + return Embed( + title="Did you mean ...", + description=suggested_tags_text + ) - else: - tags = self._cache.values() - if not tags: - await ctx.send(embed=Embed( - description="**There are no tags in the database!**", - colour=Colour.red() - )) - return True + async def list_all_tags(self, ctx: Context) -> None: + """Send a paginator with all loaded tags accessible by `ctx.author`, groups first, and alphabetically sorted.""" + def tag_sort_key(tag_item: tuple[TagIdentifier, tag]) -> str: + ident = tag_item[0] + if ident.group is None: + # Max codepoint character to force tags without a group to the end + group = chr(0x10ffff) else: - embed: Embed = Embed(title="**Current tags**") - await LinePaginator.paginate( - sorted( - f"**»** {tag['title']}" for tag in tags - if self.check_accessibility(ctx.author, tag) - ), - ctx, - embed, - footer_text=FOOTER_TEXT, - empty=False, - max_lines=15 - ) - return True - - return False + group = ident.group + return group+ident.name + + result_lines = [] + current_group = object() + group_accessible = True + + for identifier, tag in sorted(self._tags.items(), key=tag_sort_key): + + if identifier.group != current_group: + if not group_accessible: + # Remove group separator line if no tags in the previous group were accessible by the user. + result_lines.pop() + # A new group began, add a separator with the group name. + if identifier.group is not None: + group_accessible = False + result_lines.append(f"\n\N{BULLET} **{identifier.group}**") + else: + result_lines.append("\n\N{BULLET}") + current_group = identifier.group + + if tag.accessible_by(ctx.author): + result_lines.append(f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier.name}") + group_accessible = True + + embed = Embed(title="Current tags") + await LinePaginator.paginate(result_lines, ctx, embed=embed, max_lines=15, empty=False, footer_text=FOOTER_TEXT) @tags_group.command(name='get', aliases=('show', 'g')) async def get_command( @@ -322,15 +328,33 @@ class Tags(Cog): """ Get a specified tag, or a list of all tags if no tag is specified. - Returns True if something can be sent, or if the tag is on cooldown. - Returns False if no matches are found. + Returns True if something was sent, or if the tag is on cooldown. + Returns False if no message was sent. """ + if tag_name_or_group is None and tag_name is None: + if self._tags: + await self.list_all_tags(ctx) + return True + else: + await ctx.send(embed=Embed(description="**There are no tags!**")) + return True + if tag_name is None: tag_name = tag_name_or_group tag_group = None else: tag_group = tag_name_or_group - return await self.display_tag(ctx, TagIdentifier(tag_group, tag_name)) + + embed = await self.get_tag_embed(ctx, TagIdentifier(tag_group, tag_name)) + if embed is not None: + if embed is not COOLDOWN.obj: + await wait_for_deletion( + await ctx.send(embed=embed), + (ctx.author.id,) + ) + return True + else: + return False def setup(bot: Bot) -> None: -- cgit v1.2.3 From 7cc8925c681386e20094a918713b5e5b53cea3dd Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 30 Jun 2021 21:23:09 +0200 Subject: Add option to list all tags in a group --- bot/exts/info/tags.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 4aa590430..2c6dbd29d 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -319,6 +319,16 @@ class Tags(Cog): embed = Embed(title="Current tags") await LinePaginator.paginate(result_lines, ctx, embed=embed, max_lines=15, empty=False, footer_text=FOOTER_TEXT) + async def list_tags_in_group(self, ctx: Context, group: str) -> None: + """Send a paginator with all tags under `group`, that are accessible by `ctx.author`.""" + embed = Embed(title=f"**Tags under *{group}***") + tag_lines = sorted( + f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}" + for identifier, tag in self._tags.items() + if identifier.group == group and tag.accessible_by(ctx.author) + ) + await LinePaginator.paginate(tag_lines, ctx, embed, footer_text=FOOTER_TEXT, empty=False, max_lines=15) + @tags_group.command(name='get', aliases=('show', 'g')) async def get_command( self, ctx: Context, @@ -340,8 +350,15 @@ class Tags(Cog): return True if tag_name is None: - tag_name = tag_name_or_group - tag_group = None + if any( + tag_name_or_group == identifier.group and tag.accessible_by(ctx.author) + for identifier, tag in self._tags.items() + ): + await self.list_tags_in_group(ctx, tag_name_or_group) + return True + else: + tag_name = tag_name_or_group + tag_group = None else: tag_group = tag_name_or_group -- cgit v1.2.3 From 82160274aa63a3820f5424eeb54fef4722248293 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 30 Jun 2021 22:13:17 +0200 Subject: Move tag search to new design --- bot/exts/info/tags.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 2c6dbd29d..854db5c5c 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -166,7 +166,12 @@ class Tags(Cog): suggestions += self._get_suggestions(tag_identifier) return suggestions - def _get_tags_via_content(self, check: Callable[[Iterable], bool], keywords: str, user: Member) -> list: + def _get_tags_via_content( + self, + check: Callable[[Iterable], bool], + keywords: str, + user: Member, + ) -> list[tuple[TagIdentifier, Tag]]: """ Search for tags via contents. @@ -186,27 +191,29 @@ class Tags(Cog): keywords_processed = [keywords] matching_tags = [] - for tag in self._cache.values(): - matches = (query in tag['embed']['description'].casefold() for query in keywords_processed) - if self.check_accessibility(user, tag) and check(matches): - matching_tags.append(tag) + for identifier, tag in self._tags.items(): + matches = (query in tag.content.casefold() for query in keywords_processed) + if tag.accessible_by(user) and check(matches): + matching_tags.append((identifier, tag)) return matching_tags - async def _send_matching_tags(self, ctx: Context, keywords: str, matching_tags: list) -> None: + async def _send_matching_tags( + self, + ctx: Context, + keywords: str, + matching_tags: list[tuple[TagIdentifier, Tag]], + ) -> None: """Send the result of matching tags to user.""" - if not matching_tags: - pass - elif len(matching_tags) == 1: - await ctx.send(embed=Embed().from_dict(matching_tags[0]['embed'])) - else: + if len(matching_tags) == 1: + await ctx.send(embed=matching_tags[0][1].embed) + elif matching_tags: is_plural = keywords.strip().count(' ') > 0 or keywords.strip().count(',') > 0 embed = Embed( title=f"Here are the tags containing the given keyword{'s' * is_plural}:", - description='\n'.join(tag['title'] for tag in matching_tags[:10]) ) await LinePaginator.paginate( - sorted(f"**»** {tag['title']}" for tag in matching_tags), + sorted(f"**»** {identifier.name}" for identifier, _ in matching_tags), ctx, embed, footer_text=FOOTER_TEXT, -- cgit v1.2.3 From 2478b2125918e04d82d18fef78fd8c0e719776ee Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 30 Jun 2021 22:13:58 +0200 Subject: remove unused thresholds parameter --- bot/exts/info/tags.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 854db5c5c..bbec5b86d 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -137,15 +137,9 @@ class Tags(Cog): self._tags[TagIdentifier(tag_group, tag_name)] = Tag(file.read_text("utf-8")) - def _get_suggestions( - self, - tag_identifier: TagIdentifier, - thresholds: Optional[list[int]] = None - ) -> list[tuple[TagIdentifier, Tag]]: + def _get_suggestions(self, tag_identifier: TagIdentifier) -> list[tuple[TagIdentifier, Tag]]: """Return a list of suggested tags for `tag_identifier`.""" - thresholds = thresholds or [100, 90, 80, 70, 60] - - for threshold in thresholds: + for threshold in [100, 90, 80, 70, 60]: suggestions = [ (identifier, tag) for identifier, tag in self._tags.items() -- cgit v1.2.3 From 2e944b673d2c744c6768814d8d7e073e5d2fe100 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 30 Jun 2021 22:18:18 +0200 Subject: Update strings to use double quotes --- bot/exts/info/tags.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index bbec5b86d..9b477d7cc 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -172,7 +172,7 @@ class Tags(Cog): `predicate` will be the built-in any, all, or a custom callable. Must return a bool. """ keywords_processed: List[str] = [] - for keyword in keywords.split(','): + for keyword in keywords.split(","): keyword_sanitized = keyword.strip().casefold() if not keyword_sanitized: # this happens when there are leading / trailing / consecutive comma. @@ -180,7 +180,7 @@ class Tags(Cog): keywords_processed.append(keyword_sanitized) if not keywords_processed: - # after sanitizing, we can end up with an empty list, for example when keywords is ',' + # after sanitizing, we can end up with an empty list, for example when keywords is "," # in that case, we simply want to search for such keywords directly instead. keywords_processed = [keywords] @@ -202,7 +202,7 @@ class Tags(Cog): if len(matching_tags) == 1: await ctx.send(embed=matching_tags[0][1].embed) elif matching_tags: - is_plural = keywords.strip().count(' ') > 0 or keywords.strip().count(',') > 0 + is_plural = keywords.strip().count(" ") > 0 or keywords.strip().count(",") > 0 embed = Embed( title=f"Here are the tags containing the given keyword{'s' * is_plural}:", ) @@ -215,7 +215,7 @@ class Tags(Cog): max_lines=15 ) - @group(name='tags', aliases=('tag', 't'), invoke_without_command=True) + @group(name="tags", aliases=("tag", "t"), invoke_without_command=True) async def tags_group( self, ctx: Context, @@ -225,7 +225,7 @@ class Tags(Cog): """Show all known tags, a single tag, or run a subcommand.""" await self.get_command(ctx, tag_name_or_group=tag_name_or_group, tag_name=tag_name) - @tags_group.group(name='search', invoke_without_command=True) + @tags_group.group(name="search", invoke_without_command=True) async def search_tag_content(self, ctx: Context, *, keywords: str) -> None: """ Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma. @@ -235,14 +235,14 @@ class Tags(Cog): matching_tags = self._get_tags_via_content(all, keywords, ctx.author) await self._send_matching_tags(ctx, keywords, matching_tags) - @search_tag_content.command(name='any') - async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Optional[str] = 'any') -> None: + @search_tag_content.command(name="any") + async def search_tag_content_any_keyword(self, ctx: Context, *, keywords: Optional[str] = "any") -> None: """ Search inside tags' contents for tags. Allow searching for multiple keywords separated by comma. Search for tags that has ANY of the keywords. """ - matching_tags = self._get_tags_via_content(any, keywords or 'any', ctx.author) + matching_tags = self._get_tags_via_content(any, keywords or "any", ctx.author) await self._send_matching_tags(ctx, keywords, matching_tags) async def get_tag_embed( @@ -330,7 +330,7 @@ class Tags(Cog): ) await LinePaginator.paginate(tag_lines, ctx, embed, footer_text=FOOTER_TEXT, empty=False, max_lines=15) - @tags_group.command(name='get', aliases=('show', 'g')) + @tags_group.command(name="get", aliases=("show", "g")) async def get_command( self, ctx: Context, tag_name_or_group: TagNameConverter = None, -- cgit v1.2.3 From 12dc173cea1d2846c405743dde7c3df36c75659e Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 30 Jun 2021 22:18:56 +0200 Subject: Remove unnecessary typehint --- bot/exts/info/tags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 9b477d7cc..594a3e409 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -5,7 +5,7 @@ import logging import re import time from pathlib import Path -from typing import Callable, Iterable, List, Literal, NamedTuple, Optional, Union +from typing import Callable, Iterable, Literal, NamedTuple, Optional, Union import discord import frontmatter @@ -171,7 +171,7 @@ class Tags(Cog): `predicate` will be the built-in any, all, or a custom callable. Must return a bool. """ - keywords_processed: List[str] = [] + keywords_processed = [] for keyword in keywords.split(","): keyword_sanitized = keyword.strip().casefold() if not keyword_sanitized: -- cgit v1.2.3 From 2e10a303aa7d62339fea5b8c489fe6c7d9dd4f24 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 30 Jun 2021 22:33:33 +0200 Subject: Add leading » when listing tag suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 594a3e409..01d0b97fa 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -274,7 +274,7 @@ class Tags(Cog): if not suggested_tags: return None suggested_tags_text = "\n".join( - str(identifier) + f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}" for identifier, tag in suggested_tags if tag.accessible_by(ctx.author) and not tag.on_cooldown_in(ctx.channel) -- cgit v1.2.3 From 46e62b877d1db210a7788249eea3f205b9021d68 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 30 Jun 2021 22:52:22 +0200 Subject: Fix tests Unnecessary invoked with mocks were removed and some more checks added for the new behaviour --- tests/bot/exts/backend/test_error_handler.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index bd4fb5942..4a466c22e 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -273,14 +273,12 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase): async def test_try_get_tag_get_command(self): """Should call `Bot.get_command` with `tags get` argument.""" self.bot.get_command.reset_mock() - self.ctx.invoked_with = "foo" await self.cog.try_get_tag(self.ctx) self.bot.get_command.assert_called_once_with("tags get") async def test_try_get_tag_invoked_from_error_handler(self): """`self.ctx` should have `invoked_from_error_handler` `True`.""" self.ctx.invoked_from_error_handler = False - self.ctx.invoked_with = "foo" await self.cog.try_get_tag(self.ctx) self.assertTrue(self.ctx.invoked_from_error_handler) @@ -295,38 +293,48 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase): err = errors.CommandError() self.tag.get_command.can_run = AsyncMock(side_effect=err) self.cog.on_command_error = AsyncMock() - self.ctx.invoked_with = "foo" self.assertIsNone(await self.cog.try_get_tag(self.ctx)) self.cog.on_command_error.assert_awaited_once_with(self.ctx, err) @patch("bot.exts.backend.error_handler.TagNameConverter") async def test_try_get_tag_convert_success(self, tag_converter): """Converting tag should successful.""" - self.ctx.invoked_with = "foo" + self.ctx.message = MagicMock(content="foo") tag_converter.convert = AsyncMock(return_value="foo") self.assertIsNone(await self.cog.try_get_tag(self.ctx)) tag_converter.convert.assert_awaited_once_with(self.ctx, "foo") self.ctx.invoke.assert_awaited_once() + self.ctx.reset_mock() + self.ctx.message = MagicMock(content="foo bar") + tag_converter.convert = AsyncMock(return_value="foo bar") + self.assertIsNone(await self.cog.try_get_tag(self.ctx)) + self.assertEqual(tag_converter.convert.call_count, 2) + self.ctx.invoke.assert_awaited_once() + @patch("bot.exts.backend.error_handler.TagNameConverter") async def test_try_get_tag_convert_fail(self, tag_converter): """Converting tag should raise `BadArgument`.""" self.ctx.reset_mock() - self.ctx.invoked_with = "bar" tag_converter.convert = AsyncMock(side_effect=errors.BadArgument()) self.assertIsNone(await self.cog.try_get_tag(self.ctx)) self.ctx.invoke.assert_not_awaited() async def test_try_get_tag_ctx_invoke(self): """Should call `ctx.invoke` with proper args/kwargs.""" - self.ctx.reset_mock() - self.ctx.invoked_with = "foo" - self.assertIsNone(await self.cog.try_get_tag(self.ctx)) - self.ctx.invoke.assert_awaited_once_with(self.tag.get_command, tag_name="foo") + test_cases = ( + ("foo", ("foo", None)), + ("foo bar", ("foo", "bar")), + ) + for message_content, args in test_cases: + self.ctx.reset_mock() + self.ctx.message = MagicMock(content=message_content) + self.assertIsNone(await self.cog.try_get_tag(self.ctx)) + self.ctx.invoke.assert_awaited_once_with(self.tag.get_command, *args) async def test_dont_call_suggestion_tag_sent(self): """Should never call command suggestion if tag is already sent.""" - self.ctx.invoked_with = "foo" + self.ctx.message = MagicMock(content="foo") self.ctx.invoke = AsyncMock(return_value=True) self.cog.send_command_suggestion = AsyncMock() -- cgit v1.2.3 From 08c068ef8f8ccc43c576ec653f250a67d7458438 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 30 Jun 2021 22:55:25 +0200 Subject: Update outdated docstring The saving functionality has not been present on the bot for a while --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 01d0b97fa..1847fa240 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -117,7 +117,7 @@ def _fuzzy_search(search: str, target: str) -> float: class Tags(Cog): - """Save new tags and fetch existing tags.""" + """Fetch tags by name or content.""" def __init__(self, bot: Bot): self.bot = bot -- cgit v1.2.3 From 14d6fca825bf87c35f82215da3e9561bd1db1ab8 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 1 Jul 2021 00:08:39 +0200 Subject: Do not add suggestion for tags with short names if a group is specified --- bot/exts/info/tags.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 1847fa240..798be6543 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -153,12 +153,20 @@ class Tags(Cog): def get_fuzzy_matches(self, tag_identifier: TagIdentifier) -> list[tuple[TagIdentifier, Tag]]: """Get tags with identifiers similar to `tag_identifier`.""" if tag_identifier.group is None: - suggestions = self._get_suggestions(tag_identifier) + if len(tag_identifier.name) < 3: + return [] + else: + return self._get_suggestions(tag_identifier) else: - # Try fuzzy matching with only a name first - suggestions = self._get_suggestions(TagIdentifier(None, tag_identifier.group)) - suggestions += self._get_suggestions(tag_identifier) - return suggestions + if len(tag_identifier.group) < 3: + suggestions = [] + else: + # Try fuzzy matching with only a name first + suggestions = self._get_suggestions(TagIdentifier(None, tag_identifier.group)) + + if not len(tag_identifier.name) < 3: + suggestions += self._get_suggestions(tag_identifier) + return suggestions def _get_tags_via_content( self, @@ -269,7 +277,7 @@ class Tags(Cog): ) return tag.embed - elif len(tag_identifier.name) >= 3: + else: suggested_tags = self.get_fuzzy_matches(tag_identifier)[:10] if not suggested_tags: return None -- cgit v1.2.3 From aafd0e614512c7ffdfaa90ad384168033549ad8a Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 1 Jul 2021 00:31:56 +0200 Subject: Emit tag if only one fuzzy match is found This feature was accidentally removed when restructuring the code --- bot/exts/info/tags.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 798be6543..6971397d4 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -263,8 +263,18 @@ class Tags(Cog): If the requested tag is on cooldown or no suggestions were found, return None. """ - if (tag := self._tags.get(tag_identifier)) is not None and tag.accessible_by(ctx.author): - + filtered_tags = [ + (ident, tag) for ident, tag in + self.get_fuzzy_matches(tag_identifier)[:10] + if tag.accessible_by(ctx.author) + ] + + if (tag := self._tags.get(tag_identifier)) is None: + if len(filtered_tags) == 1: + tag_identifier = filtered_tags[0][0] + tag = filtered_tags[0][1] + + if tag is not None: if tag.on_cooldown_in(ctx.channel): log.debug(f"Tag {str(tag_identifier)!r} is on cooldown.") return COOLDOWN.obj @@ -278,14 +288,12 @@ class Tags(Cog): return tag.embed else: - suggested_tags = self.get_fuzzy_matches(tag_identifier)[:10] - if not suggested_tags: + if not filtered_tags: return None suggested_tags_text = "\n".join( f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}" - for identifier, tag in suggested_tags - if tag.accessible_by(ctx.author) - and not tag.on_cooldown_in(ctx.channel) + for identifier, tag in filtered_tags + if not tag.on_cooldown_in(ctx.channel) ) return Embed( title="Did you mean ...", -- cgit v1.2.3 From cce90faf3ea394104dede886ef4bf5747573a612 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 7 Jul 2021 22:53:44 +0200 Subject: Fix leading space in str of identifiers without a group This issue doesn't show on discord as whitespace is collapsed in embeds, but could be seen in logs --- bot/exts/info/tags.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 6971397d4..0bedd6e10 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -63,7 +63,10 @@ class TagIdentifier(NamedTuple): return fuzzy_score def __str__(self) -> str: - return f"{self.group or ''} {self.name}" + if self.group is not None: + return f"{self.group} {self.name}" + else: + return self.name class Tag: -- cgit v1.2.3 From 7e3b7cae852fcb8d2a13d648fd06ea74d863981e Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Mon, 12 Jul 2021 16:49:46 +0530 Subject: Fix bugs when scheduling from cache 1. Dict was missing .items() method, causing TypeError. 2. Timestamp wasn't converted to float before passing to dt.fromtimestamp(), was stored as a joined string with work_time. --- bot/exts/moderation/modpings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index cf45a2182..1f6b7984a 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -71,9 +71,9 @@ class ModPings(Cog): schedule_cache = await self.modpings_schedule.to_dict() log.info("Scheduling modpings schedule for applicable moderators found in cache.") - for mod_id, schedule in schedule_cache: + for mod_id, schedule in schedule_cache.items(): start_timestamp, work_time = schedule.split("|") - start = datetime.datetime.fromtimestamp(start_timestamp) + start = datetime.datetime.fromtimestamp(float(start_timestamp)) mod = self.bot.fetch_user(mod_id) self._modpings_scheduler.schedule_at( -- cgit v1.2.3 From f1894e5adcb5711ea849752a2c94b65ba57fb0ce Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 15:22:56 -0700 Subject: Remove unnecessary config constant It's only being used as an anchor in the YAML file. There is no need to have it in Python if no Python code references it. --- bot/constants.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 500803f33..6ff0ceebe 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -435,8 +435,6 @@ class Channels(metaclass=YAMLGetter): off_topic_1: int off_topic_2: int - black_formatter: int - bot_commands: int discord_py: int esoteric: int -- cgit v1.2.3 From af3c1459ba491e748339545687a8939b4dd70e43 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 15:25:12 -0700 Subject: Add util function to send an infraction using an Infraction dict There was some redundant pre-processing of arguments happening before calling `notify_infraction`. --- bot/exts/moderation/infraction/_scheduler.py | 18 +++------- bot/exts/moderation/infraction/_utils.py | 38 +++++++++++++++++++++- bot/exts/moderation/infraction/superstarify.py | 4 +-- tests/bot/exts/moderation/infraction/test_utils.py | 4 +-- 4 files changed, 45 insertions(+), 19 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 8286d3635..19402d01d 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -162,20 +162,12 @@ class InfractionScheduler: # apply kick/ban infractions first, this would mean that we'd make it # impossible for us to deliver a DM. See python-discord/bot#982. if not infraction["hidden"]: - dm_result = f"{constants.Emojis.failmail} " - dm_log_text = "\nDM: **Failed**" - - # Sometimes user is a discord.Object; make it a proper user. - try: - if not isinstance(user, (discord.Member, discord.User)): - user = await self.bot.fetch_user(user.id) - except discord.HTTPException as e: - log.error(f"Failed to DM {user.id}: could not fetch user (status {e.status})") + if await _utils.notify_infraction(infraction, user, user_reason): + dm_result = ":incoming_envelope: " + dm_log_text = "\nDM: Sent" else: - # Accordingly display whether the user was successfully notified via DM. - if await _utils.notify_infraction(user, infr_type.replace("_", " ").title(), expiry, user_reason, icon): - dm_result = ":incoming_envelope: " - dm_log_text = "\nDM: Sent" + dm_result = f"{constants.Emojis.failmail} " + dm_log_text = "\nDM: **Failed**" end_msg = "" if infraction["actor"] == self.bot.user.id: diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index adbc641fa..a6f180c8c 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -5,9 +5,11 @@ from datetime import datetime import discord from discord.ext.commands import Context +import bot from bot.api import ResponseCodeError from bot.constants import Colours, Icons from bot.errors import InvalidInfractedUserError +from bot.utils import time log = logging.getLogger(__name__) @@ -152,7 +154,7 @@ async def get_active_infraction( log.trace(f"{user} does not have active infractions of type {infr_type}.") -async def notify_infraction( +async def send_infraction_embed( user: UserObject, infr_type: str, expires_at: t.Optional[str] = None, @@ -188,6 +190,40 @@ async def notify_infraction( return await send_private_embed(user, embed) +async def notify_infraction( + infraction: Infraction, + user: t.Optional[UserSnowflake] = None, + reason: t.Optional[str] = None +) -> bool: + """ + DM a user about their new infraction and return True if the DM is successful. + + `user` and `reason` can be used to override what is in `infraction`. Otherwise, this data will + be retrieved from `infraction`. + + Also return False if the user needs to be fetched but fails to be fetched. + """ + if user is None: + user = discord.Object(infraction["user"]) + + # Sometimes user is a discord.Object; make it a proper user. + try: + if not isinstance(user, (discord.Member, discord.User)): + user = await bot.instance.fetch_user(user.id) + except discord.HTTPException as e: + log.error(f"Failed to DM {user.id}: could not fetch user (status {e.status})") + return False + + type_ = infraction["type"].replace("_", " ").title() + icon = INFRACTION_ICONS[infraction["type"]][0] + expiry = time.format_infraction_with_duration(infraction["expires_at"]) + + if reason is None: + reason = infraction["reason"] + + return await send_infraction_embed(user, type_, expiry, reason, icon) + + async def notify_pardon( user: UserObject, title: str, diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 07e79b9fe..6dd9924ad 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -70,15 +70,13 @@ class Superstarify(InfractionScheduler, Cog): ) notified = await _utils.notify_infraction( + infraction=infraction, user=after, - infr_type="Superstarify", - expires_at=format_infraction(infraction["expires_at"]), reason=( "You have tried to change your nickname on the **Python Discord** server " f"from **{before.display_name}** to **{after.display_name}**, but as you " "are currently in superstar-prison, you do not have permission to do so." ), - icon_url=_utils.INFRACTION_ICONS["superstar"][0] ) if not notified: diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 50a717bb5..d35120992 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -124,7 +124,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.ctx.send.assert_not_awaited() @patch("bot.exts.moderation.infraction._utils.send_private_embed") - async def test_notify_infraction(self, send_private_embed_mock): + async def test_send_infraction_embed(self, send_private_embed_mock): """ Should send an embed of a certain format as a DM and return `True` if DM successful. @@ -230,7 +230,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): send_private_embed_mock.reset_mock() send_private_embed_mock.return_value = case["send_result"] - result = await utils.notify_infraction(*case["args"]) + result = await utils.send_infraction_embed(*case["args"]) self.assertEqual(case["send_result"], result) -- cgit v1.2.3 From 780074f24d110534fd0c1d1975cb351420b61b0a Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 15:25:43 -0700 Subject: Add command to resend infraction embed Resolve #1664 --- bot/exts/moderation/infraction/management.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index b3783cd60..813559030 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -11,6 +11,7 @@ from discord.utils import escape_markdown from bot import constants from bot.bot import Bot from bot.converters import Expiry, Infraction, Snowflake, UserMention, allowed_strings, proxy_user +from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction.infractions import Infractions from bot.exts.moderation.modlog import ModLog from bot.pagination import LinePaginator @@ -38,13 +39,22 @@ class ModManagement(commands.Cog): """Get currently loaded Infractions cog instance.""" return self.bot.get_cog("Infractions") - # region: Edit infraction commands - @commands.group(name='infraction', aliases=('infr', 'infractions', 'inf', 'i'), invoke_without_command=True) async def infraction_group(self, ctx: Context) -> None: - """Infraction manipulation commands.""" + """Infraction management commands.""" await ctx.send_help(ctx.command) + @infraction_group.command(name="resend", aliases=("send", "rs", "dm")) + async def infraction_resend(self, ctx: Context, infraction: Infraction) -> None: + """Resend a DM to a user about a given infraction of theirs.""" + id_ = infraction["id"] + if await _utils.notify_infraction(infraction): + await ctx.send(f":incoming_envelope: Resent DM for infraction `{id_}`.") + else: + await ctx.send(f"{constants.Emojis.failmail} Failed to resend DM for infraction `{id_}`.") + + # region: Edit infraction commands + @infraction_group.command(name="append", aliases=("amend", "add", "a")) async def infraction_append( self, -- cgit v1.2.3 From fa797cea8d8b5e2115e80802c0b4f4ef2d51609f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 15:40:03 -0700 Subject: Fix superstarify reason displaying the incorrect nickname Because the edit was happening before the reason string was formatted, the edit updated the state of the user object, causing the nickname to be the superstarified one rather than the one the user was attempting to use. --- bot/exts/moderation/infraction/superstarify.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 6dd9924ad..160c1ad19 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -60,6 +60,12 @@ class Superstarify(InfractionScheduler, Cog): if after.display_name == forced_nick: return # Nick change was triggered by this event. Ignore. + reason = ( + "You have tried to change your nickname on the **Python Discord** server " + f"from **{before.display_name}** to **{after.display_name}**, but as you " + "are currently in superstar-prison, you do not have permission to do so." + ) + log.info( f"{after.display_name} ({after.id}) tried to escape superstar prison. " f"Changing the nick back to {before.display_name}." @@ -69,17 +75,7 @@ class Superstarify(InfractionScheduler, Cog): reason=f"Superstarified member tried to escape the prison: {infraction['id']}" ) - notified = await _utils.notify_infraction( - infraction=infraction, - user=after, - reason=( - "You have tried to change your nickname on the **Python Discord** server " - f"from **{before.display_name}** to **{after.display_name}**, but as you " - "are currently in superstar-prison, you do not have permission to do so." - ), - ) - - if not notified: + if not await _utils.notify_infraction(infraction, after, reason): log.info("Failed to DM user about why they cannot change their nickname.") @Cog.listener() -- cgit v1.2.3 From 44451dc6e12e074376f6693c64f686f2379c00c3 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 19:07:53 -0700 Subject: Disallow resending hidden infractions --- bot/exts/moderation/infraction/management.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 813559030..aeadee9d0 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -47,6 +47,10 @@ class ModManagement(commands.Cog): @infraction_group.command(name="resend", aliases=("send", "rs", "dm")) async def infraction_resend(self, ctx: Context, infraction: Infraction) -> None: """Resend a DM to a user about a given infraction of theirs.""" + if infraction["hidden"]: + await ctx.send(f"{constants.Emojis.failmail} You may not resend hidden infractions.") + return + id_ = infraction["id"] if await _utils.notify_infraction(infraction): await ctx.send(f":incoming_envelope: Resent DM for infraction `{id_}`.") -- cgit v1.2.3 From d914046dc5b661d144537715dbf80ae6b361b0e8 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 15 Jul 2021 19:33:33 -0700 Subject: Clarify that a resent infraction DM is not a new infraction. --- bot/exts/moderation/infraction/management.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index aeadee9d0..08d7e0b6d 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -52,7 +52,10 @@ class ModManagement(commands.Cog): return id_ = infraction["id"] - if await _utils.notify_infraction(infraction): + reason = infraction["reason"] or "No reason provided." + reason += "\n\n**This is a re-sent message for a previously applied infraction which may have been edited.**" + + if await _utils.notify_infraction(infraction, reason=reason): await ctx.send(f":incoming_envelope: Resent DM for infraction `{id_}`.") else: await ctx.send(f"{constants.Emojis.failmail} Failed to resend DM for infraction `{id_}`.") -- cgit v1.2.3 From 567c911e4b3a15559d8203da13174ef1e43b1312 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 14:26:11 +0200 Subject: Correct the documented return objects --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 0bedd6e10..b6a91269a 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -264,7 +264,7 @@ class Tags(Cog): """ Generate an embed of the requested tag or of suggestions if the tag doesn't exist/isn't accessible by the user. - If the requested tag is on cooldown or no suggestions were found, return None. + If the requested tag is on cooldown return `COOLDOWN.obj`, otherwise if no suggestions were found return None. """ filtered_tags = [ (ident, tag) for ident, tag in -- cgit v1.2.3 From b5d8e5ac9f094973ac73f1998fec844586244d8b Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 14:29:18 +0200 Subject: Add missing "the" Co-authored-by: Bluenix --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index b6a91269a..5e3dd400d 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -400,7 +400,7 @@ def setup(bot: Bot) -> None: def extract_tag_identifier(string: str) -> TagIdentifier: - """Create a `TagIdentifier` instance from beginning of `string`.""" + """Create a `TagIdentifier` instance from the beginning of `string`.""" split_string = string.removeprefix(constants.Bot.prefix).split(" ", maxsplit=2) if len(split_string) == 1: return TagIdentifier(None, split_string[0]) -- cgit v1.2.3 From e40cd9840c188210a7cfde6bffbf08ce563c2a11 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 14:41:46 +0200 Subject: Improve help output of get command --- bot/exts/info/tags.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 5e3dd400d..655ec2dd7 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -356,11 +356,15 @@ class Tags(Cog): tag_name: TagNameConverter = None, ) -> bool: """ - Get a specified tag, or a list of all tags if no tag is specified. + When arguments are passed in: + If a single argument is given and it matches a group name, list accessible all tags from that group. + Otherwise display the tag if one was found for the given arguments, or try to display suggestions for that name + + With no arguments, list all accessible tags Returns True if something was sent, or if the tag is on cooldown. Returns False if no message was sent. - """ + """ # noqa: D205, D415 if tag_name_or_group is None and tag_name is None: if self._tags: await self.list_all_tags(ctx) -- cgit v1.2.3 From 4d8b06e99ed9cc7e1c2e60aae64c7c945f697a97 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 14:46:45 +0200 Subject: Use \N escape --- bot/exts/info/tags.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 655ec2dd7..6e9f1cf90 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -218,7 +218,10 @@ class Tags(Cog): title=f"Here are the tags containing the given keyword{'s' * is_plural}:", ) await LinePaginator.paginate( - sorted(f"**»** {identifier.name}" for identifier, _ in matching_tags), + sorted( + f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier.name}" + for identifier, _ in matching_tags + ), ctx, embed, footer_text=FOOTER_TEXT, -- cgit v1.2.3 From 96a8390290f379a90fa0f966b74993dd1ddf9f44 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 14:52:34 +0200 Subject: Remove embed title bolding for group listing The other embeds don't have a bold title --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 6e9f1cf90..2b610a5fb 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -344,7 +344,7 @@ class Tags(Cog): async def list_tags_in_group(self, ctx: Context, group: str) -> None: """Send a paginator with all tags under `group`, that are accessible by `ctx.author`.""" - embed = Embed(title=f"**Tags under *{group}***") + embed = Embed(title=f"Tags under *{group}*") tag_lines = sorted( f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}" for identifier, tag in self._tags.items() -- cgit v1.2.3 From e3b9924920a32e19adfa1274cfdbc4cf9ddb874a Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 14:57:10 +0200 Subject: Do not pass in embed as a kwarg Using a doesn't add anything to the readability of the line and makes it inconsistent with other uses --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 2b610a5fb..34592adde 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -340,7 +340,7 @@ class Tags(Cog): group_accessible = True embed = Embed(title="Current tags") - await LinePaginator.paginate(result_lines, ctx, embed=embed, max_lines=15, empty=False, footer_text=FOOTER_TEXT) + await LinePaginator.paginate(result_lines, ctx, embed, max_lines=15, empty=False, footer_text=FOOTER_TEXT) async def list_tags_in_group(self, ctx: Context, group: str) -> None: """Send a paginator with all tags under `group`, that are accessible by `ctx.author`.""" -- cgit v1.2.3 From dcf2e4ab3690d96e3788a0204ff2ba5f45834482 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 15:02:27 +0200 Subject: Move assignment to its own line instead of using an assignment expr --- bot/exts/info/tags.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 34592adde..884e8e10f 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -275,7 +275,8 @@ class Tags(Cog): if tag.accessible_by(ctx.author) ] - if (tag := self._tags.get(tag_identifier)) is None: + tag = self._tags.get(tag_identifier) + if tag is None: if len(filtered_tags) == 1: tag_identifier = filtered_tags[0][0] tag = filtered_tags[0][1] -- cgit v1.2.3 From a37c390618a436fe01e18ce52a5598de1b11cdf4 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 15:03:59 +0200 Subject: Use "message" in docstring for consistency --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 884e8e10f..8d4073342 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -366,7 +366,7 @@ class Tags(Cog): With no arguments, list all accessible tags - Returns True if something was sent, or if the tag is on cooldown. + Returns True if a message was sent, or if the tag is on cooldown. Returns False if no message was sent. """ # noqa: D205, D415 if tag_name_or_group is None and tag_name is None: -- cgit v1.2.3 From 5cbb7d20713c0a7027c2e0909ddcdb6e02dbddc2 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 15:13:46 +0200 Subject: Move `current_group` assignment and use it instead of `identifier.group` --- bot/exts/info/tags.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 8d4073342..50e63b479 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -329,12 +329,12 @@ class Tags(Cog): # Remove group separator line if no tags in the previous group were accessible by the user. result_lines.pop() # A new group began, add a separator with the group name. - if identifier.group is not None: + current_group = identifier.group + if current_group is not None: group_accessible = False - result_lines.append(f"\n\N{BULLET} **{identifier.group}**") + result_lines.append(f"\n\N{BULLET} **{current_group}**") else: result_lines.append("\n\N{BULLET}") - current_group = identifier.group if tag.accessible_by(ctx.author): result_lines.append(f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier.name}") -- cgit v1.2.3 From ab14da21715c549b7cb5508fc6339b6ee9a31490 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 20:33:51 +0200 Subject: Use an and instead of nested ifs --- bot/exts/info/tags.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 50e63b479..7efaae3c3 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -276,10 +276,9 @@ class Tags(Cog): ] tag = self._tags.get(tag_identifier) - if tag is None: - if len(filtered_tags) == 1: - tag_identifier = filtered_tags[0][0] - tag = filtered_tags[0][1] + if tag is None and len(filtered_tags) == 1: + tag_identifier = filtered_tags[0][0] + tag = filtered_tags[0][1] if tag is not None: if tag.on_cooldown_in(ctx.channel): -- cgit v1.2.3 From 6421ebed42c87ccc6a271ef3baf5345797b3b42e Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 20:35:14 +0200 Subject: Change if to elif to indicate it's exclusive with the above if --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 7efaae3c3..f2b5c0823 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -376,7 +376,7 @@ class Tags(Cog): await ctx.send(embed=Embed(description="**There are no tags!**")) return True - if tag_name is None: + elif tag_name is None: if any( tag_name_or_group == identifier.group and tag.accessible_by(ctx.author) for identifier, tag in self._tags.items() -- cgit v1.2.3 From 359a17c00bb036f9a90fe7681592dee29f84c806 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 20:38:09 +0200 Subject: Move tag identifier creation method to a TagIdentifier constructor --- bot/exts/backend/error_handler.py | 2 +- bot/exts/info/tags.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index 78822aece..51b6bc660 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -155,7 +155,7 @@ class ErrorHandler(Cog): return try: - tag_identifier = tags.extract_tag_identifier(ctx.message.content) + tag_identifier = tags.TagIdentifier.from_string(ctx.message.content) if tag_identifier.group is not None: tag_name = await TagNameConverter.convert(ctx, tag_identifier.name) tag_name_or_group = await TagNameConverter.convert(ctx, tag_identifier.group) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index f2b5c0823..c05528daf 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -68,6 +68,15 @@ class TagIdentifier(NamedTuple): else: return self.name + @classmethod + def from_string(cls, string: str) -> TagIdentifier: + """Create a `TagIdentifier` instance from the beginning of `string`.""" + split_string = string.removeprefix(constants.Bot.prefix).split(" ", maxsplit=2) + if len(split_string) == 1: + return cls(None, split_string[0]) + else: + return cls(split_string[0], split_string[1]) + class Tag: """Provide an interface to a tag from resources with `file_content`.""" @@ -404,12 +413,3 @@ class Tags(Cog): def setup(bot: Bot) -> None: """Load the Tags cog.""" bot.add_cog(Tags(bot)) - - -def extract_tag_identifier(string: str) -> TagIdentifier: - """Create a `TagIdentifier` instance from the beginning of `string`.""" - split_string = string.removeprefix(constants.Bot.prefix).split(" ", maxsplit=2) - if len(split_string) == 1: - return TagIdentifier(None, split_string[0]) - else: - return TagIdentifier(split_string[0], split_string[1]) -- cgit v1.2.3 From 116c4af36990df5f2c8413101748f68a612e07f4 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 20:51:44 +0200 Subject: Use opposite comparison operator instead of negating condition --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index c05528daf..50a88ed19 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -176,7 +176,7 @@ class Tags(Cog): # Try fuzzy matching with only a name first suggestions = self._get_suggestions(TagIdentifier(None, tag_identifier.group)) - if not len(tag_identifier.name) < 3: + if len(tag_identifier.name) >= 3: suggestions += self._get_suggestions(tag_identifier) return suggestions -- cgit v1.2.3 From 18c6c5b1a0e7dd29a7c9b142401a44d4e1971633 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 20:54:28 +0200 Subject: Simplify condition by assigning group and name before it --- bot/exts/info/tags.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 50a88ed19..fb7f60aa7 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -318,13 +318,12 @@ class Tags(Cog): async def list_all_tags(self, ctx: Context) -> None: """Send a paginator with all loaded tags accessible by `ctx.author`, groups first, and alphabetically sorted.""" def tag_sort_key(tag_item: tuple[TagIdentifier, tag]) -> str: - ident = tag_item[0] - if ident.group is None: + group, name = tag_item[0] + if group is None: # Max codepoint character to force tags without a group to the end group = chr(0x10ffff) - else: - group = ident.group - return group+ident.name + + return group + name result_lines = [] current_group = object() -- cgit v1.2.3 From e2157f90975cc37e9970d300bb8d2abf21b0a09b Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 2 Aug 2021 21:04:57 +0200 Subject: Make return condition clearer --- bot/exts/info/tags.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index fb7f60aa7..beabade58 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -398,16 +398,17 @@ class Tags(Cog): tag_group = tag_name_or_group embed = await self.get_tag_embed(ctx, TagIdentifier(tag_group, tag_name)) - if embed is not None: - if embed is not COOLDOWN.obj: - await wait_for_deletion( - await ctx.send(embed=embed), - (ctx.author.id,) - ) - return True - else: + if embed is None: return False + if embed is not COOLDOWN.obj: + await wait_for_deletion( + await ctx.send(embed=embed), + (ctx.author.id,) + ) + # A valid tag was found and was either sent, or is on cooldown + return True + def setup(bot: Bot) -> None: """Load the Tags cog.""" -- cgit v1.2.3 From 6b280b19ed5c564e824e55a1ec9bb13120c0193d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 5 Aug 2021 16:13:49 -0700 Subject: Time: remove RFC1123 support It's not used anywhere and hasn't been for a very long time. --- bot/utils/time.py | 6 ------ tests/bot/utils/test_time.py | 7 ------- 2 files changed, 13 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index eaa9b72e9..545e50859 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -7,7 +7,6 @@ import arrow import dateutil.parser from dateutil.relativedelta import relativedelta -RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" DISCORD_TIMESTAMP_REGEX = re.compile(r"") _DURATION_REGEX = re.compile( @@ -167,11 +166,6 @@ def time_since(past_datetime: datetime.datetime) -> str: return discord_timestamp(past_datetime, TimestampFormats.RELATIVE) -def parse_rfc1123(stamp: str) -> datetime.datetime: - """Parse RFC1123 time string into datetime.""" - return datetime.datetime.strptime(stamp, RFC1123_FORMAT).replace(tzinfo=datetime.timezone.utc) - - def format_infraction(timestamp: str) -> str: """Format an infraction timestamp to a discord timestamp.""" return discord_timestamp(dateutil.parser.isoparse(timestamp)) diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py index a3dcbfc0a..9c52fed27 100644 --- a/tests/bot/utils/test_time.py +++ b/tests/bot/utils/test_time.py @@ -43,13 +43,6 @@ class TimeTests(unittest.TestCase): time.humanize_delta(relativedelta(days=2, hours=2), 'hours', max_units) self.assertEqual(str(error.exception), 'max_units must be positive') - def test_parse_rfc1123(self): - """Testing parse_rfc1123.""" - self.assertEqual( - time.parse_rfc1123('Sun, 15 Sep 2019 12:00:00 GMT'), - datetime(2019, 9, 15, 12, 0, 0, tzinfo=timezone.utc) - ) - def test_format_infraction(self): """Testing format_infraction.""" self.assertEqual(time.format_infraction('2019-12-12T00:01:00Z'), '') -- cgit v1.2.3 From 469cd57693925e78bef6a1163b620a39da208670 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 5 Aug 2021 16:27:13 -0700 Subject: Time: qualify uses of functions with the module name In cases where many time utility functions were being imported, this makes the imports shorter and cleaner. In other cases, the function names read better when they're qualified with "time"; the extra context it adds is helpful. --- bot/converters.py | 4 +-- bot/exts/info/information.py | 10 ++++---- bot/exts/moderation/defcon.py | 29 +++++++++++----------- bot/exts/moderation/infraction/management.py | 7 +++--- bot/exts/moderation/infraction/superstarify.py | 6 ++--- bot/exts/moderation/modlog.py | 6 ++--- bot/exts/moderation/modpings.py | 7 +++--- bot/exts/moderation/stream.py | 7 +++--- bot/exts/moderation/watchchannels/_watchchannel.py | 7 +++--- bot/exts/recruitment/talentpool/_cog.py | 3 +-- bot/exts/recruitment/talentpool/_review.py | 8 +++--- bot/exts/utils/reminders.py | 10 ++++---- bot/exts/utils/utils.py | 5 ++-- 13 files changed, 51 insertions(+), 58 deletions(-) diff --git a/bot/converters.py b/bot/converters.py index 559e759e1..b68c4d623 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -20,9 +20,9 @@ from bot.errors import InvalidInfraction from bot.exts.info.doc import _inventory_parser from bot.exts.info.tags import TagIdentifier from bot.log import get_logger +from bot.utils import time from bot.utils.extensions import EXTENSIONS, unqualify from bot.utils.regex import INVITE_RE -from bot.utils.time import parse_duration_string if t.TYPE_CHECKING: from bot.exts.info.source import SourceType @@ -338,7 +338,7 @@ class DurationDelta(Converter): The units need to be provided in descending order of magnitude. """ - if not (delta := parse_duration_string(duration)): + if not (delta := time.parse_duration_string(duration)): raise BadArgument(f"`{duration}` is not a valid duration string.") return delta diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 1f95c460f..a83ce4d53 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -18,10 +18,10 @@ from bot.decorators import in_whitelist from bot.errors import NonExistentRoleError from bot.log import get_logger from bot.pagination import LinePaginator +from bot.utils import time from bot.utils.channel import is_mod_channel, is_staff_channel from bot.utils.checks import cooldown_with_role_bypass, has_no_roles_check, in_whitelist_check from bot.utils.members import get_or_fetch_member -from bot.utils.time import TimestampFormats, discord_timestamp, humanize_delta log = get_logger(__name__) @@ -83,7 +83,7 @@ class Information(Cog): defcon_info = "" if cog := self.bot.get_cog("Defcon"): - threshold = humanize_delta(cog.threshold) if cog.threshold else "-" + threshold = time.humanize_delta(cog.threshold) if cog.threshold else "-" defcon_info = f"Defcon threshold: {threshold}\n" verification = f"Verification level: {ctx.guild.verification_level.name}\n" @@ -173,7 +173,7 @@ class Information(Cog): """Returns an embed full of server information.""" embed = Embed(colour=Colour.og_blurple(), title="Server Information") - created = discord_timestamp(ctx.guild.created_at, TimestampFormats.RELATIVE) + created = time.discord_timestamp(ctx.guild.created_at, time.TimestampFormats.RELATIVE) num_roles = len(ctx.guild.roles) - 1 # Exclude @everyone # Server Features are only useful in certain channels @@ -249,7 +249,7 @@ class Information(Cog): """Creates an embed containing information on the `user`.""" on_server = bool(await get_or_fetch_member(ctx.guild, user.id)) - created = discord_timestamp(user.created_at, TimestampFormats.RELATIVE) + created = time.discord_timestamp(user.created_at, time.TimestampFormats.RELATIVE) name = str(user) if on_server and user.nick: @@ -272,7 +272,7 @@ class Information(Cog): if on_server: if user.joined_at: - joined = discord_timestamp(user.joined_at, TimestampFormats.RELATIVE) + joined = time.discord_timestamp(user.joined_at, time.TimestampFormats.RELATIVE) else: joined = "Unable to get join date" diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 14db37367..048e0f990 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -17,12 +17,9 @@ from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_RO from bot.converters import DurationDelta, Expiry from bot.exts.moderation.modlog import ModLog from bot.log import get_logger -from bot.utils import scheduling +from bot.utils import scheduling, time from bot.utils.messages import format_user from bot.utils.scheduling import Scheduler -from bot.utils.time import ( - TimestampFormats, discord_timestamp, humanize_delta, parse_duration_string, relativedelta_to_timedelta -) log = get_logger(__name__) @@ -88,7 +85,7 @@ class Defcon(Cog): try: settings = await self.defcon_settings.to_dict() - self.threshold = parse_duration_string(settings["threshold"]) if settings.get("threshold") else None + self.threshold = time.parse_duration_string(settings["threshold"]) if settings.get("threshold") else None self.expiry = datetime.fromisoformat(settings["expiry"]) if settings.get("expiry") else None except RedisError: log.exception("Unable to get DEFCON settings!") @@ -102,7 +99,7 @@ class Defcon(Cog): self.scheduler.schedule_at(self.expiry, 0, self._remove_threshold()) self._update_notifier() - log.info(f"DEFCON synchronized: {humanize_delta(self.threshold) if self.threshold else '-'}") + log.info(f"DEFCON synchronized: {time.humanize_delta(self.threshold) if self.threshold else '-'}") self._update_channel_topic() @@ -112,7 +109,7 @@ class Defcon(Cog): if self.threshold: now = arrow.utcnow() - if now - member.created_at < relativedelta_to_timedelta(self.threshold): + if now - member.created_at < time.relativedelta_to_timedelta(self.threshold): log.info(f"Rejecting user {member}: Account is too new") message_sent = False @@ -151,11 +148,12 @@ class Defcon(Cog): @has_any_role(*MODERATION_ROLES) async def status(self, ctx: Context) -> None: """Check the current status of DEFCON mode.""" + expiry = time.discord_timestamp(self.expiry, time.TimestampFormats.RELATIVE) if self.expiry else "-" embed = Embed( colour=Colour.og_blurple(), title="DEFCON Status", description=f""" - **Threshold:** {humanize_delta(self.threshold) if self.threshold else "-"} - **Expires:** {discord_timestamp(self.expiry, TimestampFormats.RELATIVE) if self.expiry else "-"} + **Threshold:** {time.humanize_delta(self.threshold) if self.threshold else "-"} + **Expires:** {expiry} **Verification level:** {ctx.guild.verification_level.name} """ ) @@ -213,7 +211,8 @@ class Defcon(Cog): def _update_channel_topic(self) -> None: """Update the #defcon channel topic with the current DEFCON status.""" - new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {humanize_delta(self.threshold) if self.threshold else '-'})" + threshold = time.humanize_delta(self.threshold) if self.threshold else '-' + new_topic = f"{BASE_CHANNEL_TOPIC}\n(Threshold: {threshold})" self.mod_log.ignore(Event.guild_channel_update, Channels.defcon) scheduling.create_task(self.channel.edit(topic=new_topic)) @@ -256,11 +255,11 @@ class Defcon(Cog): expiry_message = "" if expiry: activity_duration = relativedelta(expiry, arrow.utcnow().datetime) - expiry_message = f" for the next {humanize_delta(activity_duration, max_units=2)}" + expiry_message = f" for the next {time.humanize_delta(activity_duration, max_units=2)}" if self.threshold: channel_message = ( - f"updated; accounts must be {humanize_delta(self.threshold)} " + f"updated; accounts must be {time.humanize_delta(self.threshold)} " f"old to join the server{expiry_message}" ) else: @@ -290,7 +289,7 @@ class Defcon(Cog): def _log_threshold_stat(self, threshold: relativedelta) -> None: """Adds the threshold to the bot stats in days.""" - threshold_days = relativedelta_to_timedelta(threshold).total_seconds() / SECONDS_IN_DAY + threshold_days = time.relativedelta_to_timedelta(threshold).total_seconds() / SECONDS_IN_DAY self.bot.stats.gauge("defcon.threshold", threshold_days) async def _send_defcon_log(self, action: Action, actor: User) -> None: @@ -298,7 +297,7 @@ class Defcon(Cog): info = action.value log_msg: str = ( f"**Staffer:** {actor.mention} {actor} (`{actor.id}`)\n" - f"{info.template.format(threshold=(humanize_delta(self.threshold) if self.threshold else '-'))}" + f"{info.template.format(threshold=(time.humanize_delta(self.threshold) if self.threshold else '-'))}" ) status_msg = f"DEFCON {action.name.lower()}" @@ -317,7 +316,7 @@ class Defcon(Cog): @tasks.loop(hours=1) async def defcon_notifier(self) -> None: """Routinely notify moderators that DEFCON is active.""" - await self.channel.send(f"Defcon is on and is set to {humanize_delta(self.threshold)}.") + await self.channel.send(f"Defcon is on and is set to {time.humanize_delta(self.threshold)}.") def cog_unload(self) -> None: """Cancel the notifer and threshold removal tasks when the cog unloads.""" diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 9649ff852..fb5af9eaa 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -20,7 +20,6 @@ from bot.pagination import LinePaginator from bot.utils import messages, time from bot.utils.channel import is_mod_channel from bot.utils.members import get_or_fetch_member -from bot.utils.time import humanize_delta, until_expiration log = get_logger(__name__) @@ -183,8 +182,8 @@ class ModManagement(commands.Cog): self.infractions_cog.schedule_expiration(new_infraction) log_text += f""" - Previous expiry: {until_expiration(infraction['expires_at']) or "Permanent"} - New expiry: {until_expiration(new_infraction['expires_at']) or "Permanent"} + Previous expiry: {time.until_expiration(infraction['expires_at']) or "Permanent"} + New expiry: {time.until_expiration(new_infraction['expires_at']) or "Permanent"} """.rstrip() changes = ' & '.join(confirm_messages) @@ -377,7 +376,7 @@ class ModManagement(commands.Cog): timezone.utc ) date_to = dateutil.parser.isoparse(expires_at) - duration = humanize_delta(relativedelta(date_to, date_from)) + duration = time.humanize_delta(relativedelta(date_to, date_from)) # Format `dm_sent` if dm_sent is None: diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 08c92b8f3..2e272dbb0 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -14,9 +14,9 @@ from bot.converters import Duration, Expiry from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler from bot.log import get_logger +from bot.utils import time from bot.utils.members import get_or_fetch_member from bot.utils.messages import format_user -from bot.utils.time import format_infraction log = get_logger(__name__) NICKNAME_POLICY_URL = "https://pythondiscord.com/pages/rules/#nickname-policy" @@ -73,7 +73,7 @@ class Superstarify(InfractionScheduler, Cog): notified = await _utils.notify_infraction( user=after, infr_type="Superstarify", - expires_at=format_infraction(infraction["expires_at"]), + expires_at=time.format_infraction(infraction["expires_at"]), reason=( "You have tried to change your nickname on the **Python Discord** server " f"from **{before.display_name}** to **{after.display_name}**, but as you " @@ -150,7 +150,7 @@ class Superstarify(InfractionScheduler, Cog): id_ = infraction["id"] forced_nick = self.get_nick(id_, member.id) - expiry_str = format_infraction(infraction["expires_at"]) + expiry_str = time.format_infraction(infraction["expires_at"]) # Apply the infraction async def action() -> None: diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index fc9204998..d5e209d81 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -16,8 +16,8 @@ from discord.utils import escape_markdown from bot.bot import Bot from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, Roles, URLs from bot.log import get_logger +from bot.utils import time from bot.utils.messages import format_user -from bot.utils.time import humanize_delta log = get_logger(__name__) @@ -407,7 +407,7 @@ class ModLog(Cog, name="ModLog"): now = datetime.now(timezone.utc) difference = abs(relativedelta(now, member.created_at)) - message = format_user(member) + "\n\n**Account age:** " + humanize_delta(difference) + message = format_user(member) + "\n\n**Account age:** " + time.humanize_delta(difference) if difference.days < 1 and difference.months < 1 and difference.years < 1: # New user account! message = f"{Emojis.new} {message}" @@ -713,7 +713,7 @@ class ModLog(Cog, name="ModLog"): # datetime as the baseline and create a human-readable delta between this edit event # and the last time the message was edited timestamp = msg_before.edited_at - delta = humanize_delta(relativedelta(msg_after.edited_at, msg_before.edited_at)) + delta = time.humanize_delta(relativedelta(msg_after.edited_at, msg_before.edited_at)) footer = f"Last edited {delta} ago" else: # Message was not previously edited, use the created_at datetime as the baseline, no diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 20a8c39d7..b5cd29b12 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -11,9 +11,8 @@ from bot.bot import Bot from bot.constants import Colours, Emojis, Guild, Icons, MODERATION_ROLES, Roles from bot.converters import Expiry from bot.log import get_logger -from bot.utils import scheduling +from bot.utils import scheduling, time from bot.utils.scheduling import Scheduler -from bot.utils.time import TimestampFormats, discord_timestamp log = get_logger(__name__) @@ -233,8 +232,8 @@ class ModPings(Cog): await ctx.send( f"{Emojis.ok_hand} {ctx.author.mention} Scheduled mod pings from " - f"{discord_timestamp(start, TimestampFormats.TIME)} to " - f"{discord_timestamp(end, TimestampFormats.TIME)}!" + f"{time.discord_timestamp(start, time.TimestampFormats.TIME)} to " + f"{time.discord_timestamp(end, time.TimestampFormats.TIME)}!" ) @schedule_modpings.command(name='delete', aliases=('del', 'd')) diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index 99bbd8721..5a7b12295 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -14,9 +14,8 @@ from bot.constants import ( from bot.converters import Expiry from bot.log import get_logger from bot.pagination import LinePaginator -from bot.utils import scheduling +from bot.utils import scheduling, time from bot.utils.members import get_or_fetch_member -from bot.utils.time import discord_timestamp, format_infraction_with_duration log = get_logger(__name__) @@ -131,10 +130,10 @@ class Stream(commands.Cog): await member.add_roles(discord.Object(Roles.video), reason="Temporary streaming access granted") - await ctx.send(f"{Emojis.check_mark} {member.mention} can now stream until {discord_timestamp(duration)}.") + await ctx.send(f"{Emojis.check_mark} {member.mention} can now stream until {time.discord_timestamp(duration)}.") # Convert here for nicer logging - revoke_time = format_infraction_with_duration(str(duration)) + revoke_time = time.format_infraction_with_duration(str(duration)) log.debug(f"Successfully gave {member} ({member.id}) permission to stream until {revoke_time}.") @commands.command(aliases=("pstream",)) diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py index 34d445912..106483527 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -18,9 +18,8 @@ from bot.exts.filters.webhook_remover import WEBHOOK_URL_RE from bot.exts.moderation.modlog import ModLog from bot.log import CustomLogger, get_logger from bot.pagination import LinePaginator -from bot.utils import CogABCMeta, messages, scheduling +from bot.utils import CogABCMeta, messages, scheduling, time from bot.utils.members import get_or_fetch_member -from bot.utils.time import get_time_delta log = get_logger(__name__) @@ -286,7 +285,7 @@ class WatchChannel(metaclass=CogABCMeta): actor = actor.display_name if actor else self.watched_users[user_id]['actor'] inserted_at = self.watched_users[user_id]['inserted_at'] - time_delta = get_time_delta(inserted_at) + time_delta = time.get_time_delta(inserted_at) reason = self.watched_users[user_id]['reason'] @@ -360,7 +359,7 @@ class WatchChannel(metaclass=CogABCMeta): if member: line += f" ({member.name}#{member.discriminator})" inserted_at = user_data['inserted_at'] - line += f", added {get_time_delta(inserted_at)}" + line += f", added {time.get_time_delta(inserted_at)}" if not member: # Cross off users who left the server. line = f"~~{line}~~" list_data["info"][user_id] = line diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 8fa0be5b1..80274eaea 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -17,7 +17,6 @@ from bot.log import get_logger from bot.pagination import LinePaginator from bot.utils import scheduling, time from bot.utils.members import get_or_fetch_member -from bot.utils.time import get_time_delta AUTOREVIEW_ENABLED_KEY = "autoreview_enabled" REASON_MAX_CHARS = 1000 @@ -181,7 +180,7 @@ class TalentPool(Cog, name="Talentpool"): if member: line += f" ({member.name}#{member.discriminator})" inserted_at = user_data['inserted_at'] - line += f", added {get_time_delta(inserted_at)}" + line += f", added {time.get_time_delta(inserted_at)}" if not member: # Cross off users who left the server. line = f"~~{line}~~" if user_data['reviewed']: diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 0e7194892..bbffbe6e3 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -17,10 +17,10 @@ from bot.api import ResponseCodeError from bot.bot import Bot from bot.constants import Channels, Colours, Emojis, Guild, Roles from bot.log import get_logger +from bot.utils import time from bot.utils.members import get_or_fetch_member from bot.utils.messages import count_unique_users_reaction, pin_no_system_message from bot.utils.scheduling import Scheduler -from bot.utils.time import get_time_delta, time_since if typing.TYPE_CHECKING: from bot.exts.recruitment.talentpool._cog import TalentPool @@ -273,7 +273,7 @@ class Reviewer: last_channel = user_activity["top_channel_activity"][-1] channels += f", and {last_channel[1]} in {last_channel[0]}" - joined_at_formatted = time_since(member.joined_at) + joined_at_formatted = time.time_since(member.joined_at) review = ( f"{member.name} joined the server **{joined_at_formatted}**" f" and has **{messages} messages**{channels}." @@ -321,7 +321,7 @@ class Reviewer: infractions += ", with the last infraction issued " # Infractions were ordered by time since insertion descending. - infractions += get_time_delta(infraction_list[0]['inserted_at']) + infractions += time.get_time_delta(infraction_list[0]['inserted_at']) return f"They have {infractions}." @@ -365,7 +365,7 @@ class Reviewer: nomination_times = f"{num_entries} times" if num_entries > 1 else "once" rejection_times = f"{len(history)} times" if len(history) > 1 else "once" - end_time = time_since(isoparse(history[0]['ended_at'])) + end_time = time.time_since(isoparse(history[0]['ended_at'])) review = ( f"They were nominated **{nomination_times}** before" diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 90677b2dd..dc7782727 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -13,13 +13,12 @@ from bot.constants import Guild, Icons, MODERATION_ROLES, POSITIVE_REPLIES, Role from bot.converters import Duration, UnambiguousUser from bot.log import get_logger from bot.pagination import LinePaginator -from bot.utils import scheduling +from bot.utils import scheduling, time from bot.utils.checks import has_any_role_check, has_no_roles_check from bot.utils.lock import lock_arg from bot.utils.members import get_or_fetch_member from bot.utils.messages import send_denial from bot.utils.scheduling import Scheduler -from bot.utils.time import TimestampFormats, discord_timestamp log = get_logger(__name__) @@ -310,7 +309,8 @@ class Reminders(Cog): } ) - mention_string = f"Your reminder will arrive on {discord_timestamp(expiration, TimestampFormats.DAY_TIME)}" + formatted_time = time.discord_timestamp(expiration, time.TimestampFormats.DAY_TIME) + mention_string = f"Your reminder will arrive on {formatted_time}" if mentions: mention_string += f" and will mention {len(mentions)} other(s)" @@ -348,7 +348,7 @@ class Reminders(Cog): for content, remind_at, id_, mentions in reminders: # Parse and humanize the time, make it pretty :D remind_datetime = isoparse(remind_at) - time = discord_timestamp(remind_datetime, TimestampFormats.RELATIVE) + expiry = time.discord_timestamp(remind_datetime, time.TimestampFormats.RELATIVE) mentions = ", ".join([ # Both Role and User objects have the `name` attribute @@ -357,7 +357,7 @@ class Reminders(Cog): mention_string = f"\n**Mentions:** {mentions}" if mentions else "" text = textwrap.dedent(f""" - **Reminder #{id_}:** *expires {time}* (ID: {id_}){mention_string} + **Reminder #{id_}:** *expires {expiry}* (ID: {id_}){mention_string} {content} """).strip() diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index f76eea516..00fa7a388 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -13,8 +13,7 @@ from bot.converters import Snowflake from bot.decorators import in_whitelist from bot.log import get_logger from bot.pagination import LinePaginator -from bot.utils import messages -from bot.utils.time import time_since +from bot.utils import messages, time log = get_logger(__name__) @@ -173,7 +172,7 @@ class Utils(Cog): lines = [] for snowflake in snowflakes: created_at = snowflake_time(snowflake) - lines.append(f"**{snowflake}**\nCreated at {created_at} ({time_since(created_at)}).") + lines.append(f"**{snowflake}**\nCreated at {created_at} ({time.time_since(created_at)}).") await LinePaginator.paginate( lines, -- cgit v1.2.3 From 0bfdc16fc74be8ead1e6b8784757b9202293b745 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 5 Aug 2021 16:32:23 -0700 Subject: Time: rename time_since to format_relative While the function is basically just a wrapper for discord_timestamp now, it is very common to use the relative format. It's cumbersome to import the format enum and pass it to discord_timestamp calls, so keeping this function around will be nice. --- bot/exts/recruitment/talentpool/_review.py | 4 ++-- bot/exts/utils/utils.py | 2 +- bot/utils/time.py | 15 ++++++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index bbffbe6e3..474f669c6 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -273,7 +273,7 @@ class Reviewer: last_channel = user_activity["top_channel_activity"][-1] channels += f", and {last_channel[1]} in {last_channel[0]}" - joined_at_formatted = time.time_since(member.joined_at) + joined_at_formatted = time.format_relative(member.joined_at) review = ( f"{member.name} joined the server **{joined_at_formatted}**" f" and has **{messages} messages**{channels}." @@ -365,7 +365,7 @@ class Reviewer: nomination_times = f"{num_entries} times" if num_entries > 1 else "once" rejection_times = f"{len(history)} times" if len(history) > 1 else "once" - end_time = time.time_since(isoparse(history[0]['ended_at'])) + end_time = time.format_relative(isoparse(history[0]['ended_at'])) review = ( f"They were nominated **{nomination_times}** before" diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 00fa7a388..2a074788e 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -172,7 +172,7 @@ class Utils(Cog): lines = [] for snowflake in snowflakes: created_at = snowflake_time(snowflake) - lines.append(f"**{snowflake}**\nCreated at {created_at} ({time.time_since(created_at)}).") + lines.append(f"**{snowflake}**\nCreated at {created_at} ({time.format_relative(created_at)}).") await LinePaginator.paginate( lines, diff --git a/bot/utils/time.py b/bot/utils/time.py index 545e50859..e6dcdee15 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -125,7 +125,7 @@ def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units: def get_time_delta(time_string: str) -> str: """Returns the time in human-readable time delta format.""" date_time = dateutil.parser.isoparse(time_string) - time_delta = time_since(date_time) + time_delta = format_relative(date_time) return time_delta @@ -161,9 +161,14 @@ def relativedelta_to_timedelta(delta: relativedelta) -> datetime.timedelta: return utcnow + delta - utcnow -def time_since(past_datetime: datetime.datetime) -> str: - """Takes a datetime and returns a discord timestamp that describes how long ago that datetime was.""" - return discord_timestamp(past_datetime, TimestampFormats.RELATIVE) +def format_relative(timestamp: ValidTimestamp) -> str: + """ + Format `timestamp` as a relative Discord timestamp. + + A relative timestamp describes how much time has elapsed since `timestamp` or how much time + remains until `timestamp` is reached. See `time.discord_timestamp`. + """ + return discord_timestamp(timestamp, TimestampFormats.RELATIVE) def format_infraction(timestamp: str) -> str: @@ -211,7 +216,7 @@ def until_expiration( Get the remaining time until infraction's expiration, in a discord timestamp. Returns a human-readable version of the remaining duration between arrow.utcnow() and an expiry. - Similar to time_since, except that this function doesn't error on a null input + Similar to format_relative, except that this function doesn't error on a null input and return null if the expiry is in the paste """ if not expiry: -- cgit v1.2.3 From ad1fcfbdab5be55f16ab157bcf86927c7996ed07 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 5 Aug 2021 16:32:44 -0700 Subject: Time: replace discord_timestamp calls with format_relative Use the latter where the former was being called with the relative format type. --- bot/exts/info/information.py | 6 +++--- bot/exts/moderation/defcon.py | 2 +- bot/exts/utils/reminders.py | 2 +- bot/utils/time.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index a83ce4d53..29a00ec5d 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -173,7 +173,7 @@ class Information(Cog): """Returns an embed full of server information.""" embed = Embed(colour=Colour.og_blurple(), title="Server Information") - created = time.discord_timestamp(ctx.guild.created_at, time.TimestampFormats.RELATIVE) + created = time.format_relative(ctx.guild.created_at) num_roles = len(ctx.guild.roles) - 1 # Exclude @everyone # Server Features are only useful in certain channels @@ -249,7 +249,7 @@ class Information(Cog): """Creates an embed containing information on the `user`.""" on_server = bool(await get_or_fetch_member(ctx.guild, user.id)) - created = time.discord_timestamp(user.created_at, time.TimestampFormats.RELATIVE) + created = time.format_relative(user.created_at) name = str(user) if on_server and user.nick: @@ -272,7 +272,7 @@ class Information(Cog): if on_server: if user.joined_at: - joined = time.discord_timestamp(user.joined_at, time.TimestampFormats.RELATIVE) + joined = time.format_relative(user.joined_at) else: joined = "Unable to get join date" diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 048e0f990..263e8136e 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -148,7 +148,7 @@ class Defcon(Cog): @has_any_role(*MODERATION_ROLES) async def status(self, ctx: Context) -> None: """Check the current status of DEFCON mode.""" - expiry = time.discord_timestamp(self.expiry, time.TimestampFormats.RELATIVE) if self.expiry else "-" + expiry = time.format_relative(self.expiry) if self.expiry else "-" embed = Embed( colour=Colour.og_blurple(), title="DEFCON Status", description=f""" diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index dc7782727..bfa294809 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -348,7 +348,7 @@ class Reminders(Cog): for content, remind_at, id_, mentions in reminders: # Parse and humanize the time, make it pretty :D remind_datetime = isoparse(remind_at) - expiry = time.discord_timestamp(remind_datetime, time.TimestampFormats.RELATIVE) + expiry = time.format_relative(remind_datetime) mentions = ", ".join([ # Both Role and User objects have the `name` attribute diff --git a/bot/utils/time.py b/bot/utils/time.py index e6dcdee15..da56bcea8 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -228,4 +228,4 @@ def until_expiration( if since < now: return None - return discord_timestamp(since, TimestampFormats.RELATIVE) + return format_relative(since) -- cgit v1.2.3 From 3ac1ef92b928c26985342a3f35934a1c7c08d2b4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 5 Aug 2021 16:35:41 -0700 Subject: Time: remove absolute param from format_infraction_with_duration It's not used anywhere. Furthermore, a humanised duration with negative values wouldn't make sense. --- bot/utils/time.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index da56bcea8..190adf885 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -180,16 +180,12 @@ def format_infraction_with_duration( date_to: Optional[str], date_from: Optional[datetime.datetime] = None, max_units: int = 2, - absolute: bool = True ) -> Optional[str]: """ Return `date_to` formatted as a discord timestamp with the timestamp duration since `date_from`. `max_units` specifies the maximum number of units of time to include in the duration. For example, a value of 1 may include days but not hours. - - If `absolute` is True, the absolute value of the duration delta is used. This prevents negative - values in the case that `date_to` is in the past relative to `date_from`. """ if not date_to: return None @@ -199,10 +195,7 @@ def format_infraction_with_duration( date_from = date_from or datetime.datetime.now(datetime.timezone.utc) date_to = dateutil.parser.isoparse(date_to).replace(microsecond=0) - delta = relativedelta(date_to, date_from) - if absolute: - delta = abs(delta) - + delta = abs(relativedelta(date_to, date_from)) duration = humanize_delta(delta, max_units=max_units) duration_formatted = f" ({duration})" if duration else "" -- cgit v1.2.3 From 6ceb2c09114233f6db00e11ea85891adcdcf7f4f Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 5 Aug 2021 17:19:24 -0700 Subject: Time: remove broken enum type check in discord_timestamp First, the `args` attribute doesn't exist on enums. Even if it did, this check only works if the argument given is an enum member (of any enum). Such occurrence seems too rare to warrant an explicit check. --- bot/utils/time.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index 190adf885..ddcf5bac2 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -62,9 +62,6 @@ def _stringify_time_unit(value: int, unit: str) -> str: def discord_timestamp(timestamp: ValidTimestamp, format: TimestampFormats = TimestampFormats.DATE_TIME) -> str: """Create and format a Discord flavored markdown timestamp.""" - if format not in TimestampFormats: - raise ValueError(f"Format can only be one of {', '.join(TimestampFormats.args)}, not {format}.") - # Convert each possible timestamp class to an integer. if isinstance(timestamp, datetime.datetime): timestamp = (timestamp - arrow.get(0)).total_seconds() -- cgit v1.2.3 From 24b28e264719e5bf40f565d553f7e9e57041b0a2 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 5 Aug 2021 17:20:31 -0700 Subject: Time: remove timedelta and relativedelta support from discord_timestamp When a delta is given, it is unknown what it's relative to. The function has to assume it's relative to the POSIX Epoch. However, using a delta for this would be quite odd, and would more likely be a mistake if anything. relativedelta support was broken anyway since it wasn't using the total seconds represented by the delta. --- bot/utils/time.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index ddcf5bac2..60720031a 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -20,7 +20,7 @@ _DURATION_REGEX = re.compile( ) -ValidTimestamp = Union[int, datetime.datetime, datetime.date, datetime.timedelta, relativedelta] +ValidTimestamp = Union[int, datetime.datetime, datetime.date] class TimestampFormats(Enum): @@ -67,10 +67,6 @@ def discord_timestamp(timestamp: ValidTimestamp, format: TimestampFormats = Time timestamp = (timestamp - arrow.get(0)).total_seconds() elif isinstance(timestamp, datetime.date): timestamp = (timestamp - arrow.get(0)).total_seconds() - elif isinstance(timestamp, datetime.timedelta): - timestamp = timestamp.total_seconds() - elif isinstance(timestamp, relativedelta): - timestamp = timestamp.seconds return f"" -- cgit v1.2.3 From 93742d718dcb4aee72ef5d20ca570b6200f07d2d Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 5 Aug 2021 19:14:08 -0700 Subject: Time: rename format_infraction_with_duration It's not necessarily tied to infractions anymore. --- bot/exts/moderation/infraction/_scheduler.py | 4 ++-- bot/exts/moderation/infraction/management.py | 2 +- bot/exts/moderation/stream.py | 2 +- bot/utils/time.py | 18 +++++++++--------- tests/bot/utils/test_time.py | 18 +++++++++--------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 57aa2d9b6..9d4d58e2e 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -136,7 +136,7 @@ class InfractionScheduler: infr_type = infraction["type"] icon = _utils.INFRACTION_ICONS[infr_type][0] reason = infraction["reason"] - expiry = time.format_infraction_with_duration(infraction["expires_at"]) + expiry = time.format_with_duration(infraction["expires_at"]) id_ = infraction['id'] if user_reason is None: @@ -387,7 +387,7 @@ class InfractionScheduler: log.info(f"Marking infraction #{id_} as inactive (expired).") expiry = dateutil.parser.isoparse(expiry) if expiry else None - created = time.format_infraction_with_duration(inserted_at, expiry) + created = time.format_with_duration(inserted_at, expiry) log_content = None log_text = { diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index fb5af9eaa..dd994a2d2 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -150,7 +150,7 @@ class ModManagement(commands.Cog): confirm_messages.append("marked as permanent") elif duration is not None: request_data['expires_at'] = duration.isoformat() - expiry = time.format_infraction_with_duration(request_data['expires_at']) + expiry = time.format_with_duration(request_data['expires_at']) confirm_messages.append(f"set to expire on {expiry}") else: confirm_messages.append("expiry unchanged") diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index 5a7b12295..bc9d35714 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -133,7 +133,7 @@ class Stream(commands.Cog): await ctx.send(f"{Emojis.check_mark} {member.mention} can now stream until {time.discord_timestamp(duration)}.") # Convert here for nicer logging - revoke_time = time.format_infraction_with_duration(str(duration)) + revoke_time = time.format_with_duration(str(duration)) log.debug(f"Successfully gave {member} ({member.id}) permission to stream until {revoke_time}.") @commands.command(aliases=("pstream",)) diff --git a/bot/utils/time.py b/bot/utils/time.py index 60720031a..13dfc6fb7 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -169,26 +169,26 @@ def format_infraction(timestamp: str) -> str: return discord_timestamp(dateutil.parser.isoparse(timestamp)) -def format_infraction_with_duration( - date_to: Optional[str], - date_from: Optional[datetime.datetime] = None, +def format_with_duration( + timestamp: Optional[str], + other_timestamp: Optional[datetime.datetime] = None, max_units: int = 2, ) -> Optional[str]: """ - Return `date_to` formatted as a discord timestamp with the timestamp duration since `date_from`. + Return `timestamp` formatted as a discord timestamp with the timestamp duration since `other_timestamp`. `max_units` specifies the maximum number of units of time to include in the duration. For example, a value of 1 may include days but not hours. """ - if not date_to: + if not timestamp: return None - date_to_formatted = format_infraction(date_to) + date_to_formatted = format_infraction(timestamp) - date_from = date_from or datetime.datetime.now(datetime.timezone.utc) - date_to = dateutil.parser.isoparse(date_to).replace(microsecond=0) + other_timestamp = other_timestamp or datetime.datetime.now(datetime.timezone.utc) + timestamp = dateutil.parser.isoparse(timestamp).replace(microsecond=0) - delta = abs(relativedelta(date_to, date_from)) + delta = abs(relativedelta(timestamp, other_timestamp)) duration = humanize_delta(delta, max_units=max_units) duration_formatted = f" ({duration})" if duration else "" diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py index 9c52fed27..02b5f8c17 100644 --- a/tests/bot/utils/test_time.py +++ b/tests/bot/utils/test_time.py @@ -47,8 +47,8 @@ class TimeTests(unittest.TestCase): """Testing format_infraction.""" self.assertEqual(time.format_infraction('2019-12-12T00:01:00Z'), '') - def test_format_infraction_with_duration_none_expiry(self): - """format_infraction_with_duration should work for None expiry.""" + def test_format_with_duration_none_expiry(self): + """format_with_duration should work for None expiry.""" test_cases = ( (None, None, None, None), @@ -60,10 +60,10 @@ class TimeTests(unittest.TestCase): for expiry, date_from, max_units, expected in test_cases: with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected): - self.assertEqual(time.format_infraction_with_duration(expiry, date_from, max_units), expected) + self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected) - def test_format_infraction_with_duration_custom_units(self): - """format_infraction_with_duration should work for custom max_units.""" + def test_format_with_duration_custom_units(self): + """format_with_duration should work for custom max_units.""" test_cases = ( ('3000-12-12T00:01:00Z', datetime(3000, 12, 11, 12, 5, 5, tzinfo=timezone.utc), 6, ' (11 hours, 55 minutes and 55 seconds)'), @@ -73,10 +73,10 @@ class TimeTests(unittest.TestCase): for expiry, date_from, max_units, expected in test_cases: with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected): - self.assertEqual(time.format_infraction_with_duration(expiry, date_from, max_units), expected) + self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected) - def test_format_infraction_with_duration_normal_usage(self): - """format_infraction_with_duration should work for normal usage, across various durations.""" + def test_format_with_duration_normal_usage(self): + """format_with_duration should work for normal usage, across various durations.""" utc = timezone.utc test_cases = ( ('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5, tzinfo=utc), 2, @@ -98,7 +98,7 @@ class TimeTests(unittest.TestCase): for expiry, date_from, max_units, expected in test_cases: with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected): - self.assertEqual(time.format_infraction_with_duration(expiry, date_from, max_units), expected) + self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected) def test_until_expiration_with_duration_none_expiry(self): """until_expiration should work for None expiry.""" -- cgit v1.2.3 From ea7fc62ddc8d08b6acdb00ac2d9a024fee8ad634 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 5 Aug 2021 19:26:26 -0700 Subject: Time: support more timestamp formats as arguments Remove the burden of conversion from the caller to clean up and simplify the call sites. Handle timestamp conversions internally with arrow.get. Remove format_infraction and get_time_delta because they're now obsolete. Replace the former with discord_timestamp and the latter with format_relative. --- bot/exts/moderation/infraction/_scheduler.py | 7 +- bot/exts/moderation/infraction/management.py | 4 +- bot/exts/moderation/infraction/superstarify.py | 4 +- bot/exts/moderation/stream.py | 2 +- bot/exts/moderation/watchchannels/_watchchannel.py | 4 +- bot/exts/recruitment/talentpool/_cog.py | 8 +- bot/exts/recruitment/talentpool/_review.py | 4 +- bot/exts/utils/reminders.py | 5 +- bot/utils/time.py | 94 +++++++++++----------- tests/bot/utils/test_time.py | 4 - 10 files changed, 63 insertions(+), 73 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 9d4d58e2e..47b639421 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -381,20 +381,15 @@ class InfractionScheduler: actor = infraction["actor"] type_ = infraction["type"] id_ = infraction["id"] - inserted_at = infraction["inserted_at"] - expiry = infraction["expires_at"] log.info(f"Marking infraction #{id_} as inactive (expired).") - expiry = dateutil.parser.isoparse(expiry) if expiry else None - created = time.format_with_duration(inserted_at, expiry) - log_content = None log_text = { "Member": f"<@{user_id}>", "Actor": f"<@{actor}>", "Reason": infraction["reason"], - "Created": created, + "Created": time.format_with_duration(infraction["inserted_at"], infraction["expires_at"]), } try: diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index dd994a2d2..23c6e8b92 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -150,7 +150,7 @@ class ModManagement(commands.Cog): confirm_messages.append("marked as permanent") elif duration is not None: request_data['expires_at'] = duration.isoformat() - expiry = time.format_with_duration(request_data['expires_at']) + expiry = time.format_with_duration(duration) confirm_messages.append(f"set to expire on {expiry}") else: confirm_messages.append("expiry unchanged") @@ -351,7 +351,7 @@ class ModManagement(commands.Cog): active = infraction["active"] user = infraction["user"] expires_at = infraction["expires_at"] - created = time.format_infraction(infraction["inserted_at"]) + created = time.discord_timestamp(infraction["inserted_at"]) dm_sent = infraction["dm_sent"] # Format the user string. diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 2e272dbb0..a037ca1be 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -73,7 +73,7 @@ class Superstarify(InfractionScheduler, Cog): notified = await _utils.notify_infraction( user=after, infr_type="Superstarify", - expires_at=time.format_infraction(infraction["expires_at"]), + expires_at=time.discord_timestamp(infraction["expires_at"]), reason=( "You have tried to change your nickname on the **Python Discord** server " f"from **{before.display_name}** to **{after.display_name}**, but as you " @@ -150,7 +150,7 @@ class Superstarify(InfractionScheduler, Cog): id_ = infraction["id"] forced_nick = self.get_nick(id_, member.id) - expiry_str = time.format_infraction(infraction["expires_at"]) + expiry_str = time.discord_timestamp(infraction["expires_at"]) # Apply the infraction async def action() -> None: diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index bc9d35714..4dccc8a7e 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -133,7 +133,7 @@ class Stream(commands.Cog): await ctx.send(f"{Emojis.check_mark} {member.mention} can now stream until {time.discord_timestamp(duration)}.") # Convert here for nicer logging - revoke_time = time.format_with_duration(str(duration)) + revoke_time = time.format_with_duration(duration) log.debug(f"Successfully gave {member} ({member.id}) permission to stream until {revoke_time}.") @commands.command(aliases=("pstream",)) diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py index 106483527..ee9b6ba45 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -285,7 +285,7 @@ class WatchChannel(metaclass=CogABCMeta): actor = actor.display_name if actor else self.watched_users[user_id]['actor'] inserted_at = self.watched_users[user_id]['inserted_at'] - time_delta = time.get_time_delta(inserted_at) + time_delta = time.format_relative(inserted_at) reason = self.watched_users[user_id]['reason'] @@ -359,7 +359,7 @@ class WatchChannel(metaclass=CogABCMeta): if member: line += f" ({member.name}#{member.discriminator})" inserted_at = user_data['inserted_at'] - line += f", added {time.get_time_delta(inserted_at)}" + line += f", added {time.format_relative(inserted_at)}" if not member: # Cross off users who left the server. line = f"~~{line}~~" list_data["info"][user_id] = line diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 80274eaea..bbc135454 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -180,7 +180,7 @@ class TalentPool(Cog, name="Talentpool"): if member: line += f" ({member.name}#{member.discriminator})" inserted_at = user_data['inserted_at'] - line += f", added {time.get_time_delta(inserted_at)}" + line += f", added {time.format_relative(inserted_at)}" if not member: # Cross off users who left the server. line = f"~~{line}~~" if user_data['reviewed']: @@ -561,7 +561,7 @@ class TalentPool(Cog, name="Talentpool"): actor = await get_or_fetch_member(guild, actor_id) reason = site_entry["reason"] or "*None*" - created = time.format_infraction(site_entry["inserted_at"]) + created = time.discord_timestamp(site_entry["inserted_at"]) entries.append( f"Actor: {actor.mention if actor else actor_id}\nCreated: {created}\nReason: {reason}" ) @@ -570,7 +570,7 @@ class TalentPool(Cog, name="Talentpool"): active = nomination_object["active"] - start_date = time.format_infraction(nomination_object["inserted_at"]) + start_date = time.discord_timestamp(nomination_object["inserted_at"]) if active: lines = textwrap.dedent( f""" @@ -584,7 +584,7 @@ class TalentPool(Cog, name="Talentpool"): """ ) else: - end_date = time.format_infraction(nomination_object["ended_at"]) + end_date = time.discord_timestamp(nomination_object["ended_at"]) lines = textwrap.dedent( f""" =============== diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 474f669c6..b4d177622 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -321,7 +321,7 @@ class Reviewer: infractions += ", with the last infraction issued " # Infractions were ordered by time since insertion descending. - infractions += time.get_time_delta(infraction_list[0]['inserted_at']) + infractions += time.format_relative(infraction_list[0]['inserted_at']) return f"They have {infractions}." @@ -365,7 +365,7 @@ class Reviewer: nomination_times = f"{num_entries} times" if num_entries > 1 else "once" rejection_times = f"{len(history)} times" if len(history) > 1 else "once" - end_time = time.format_relative(isoparse(history[0]['ended_at'])) + end_time = time.format_relative(history[0]['ended_at']) review = ( f"They were nominated **{nomination_times}** before" diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index bfa294809..289d00356 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -168,7 +168,7 @@ class Reminders(Cog): self.schedule_reminder(reminder) @lock_arg(LOCK_NAMESPACE, "reminder", itemgetter("id"), raise_error=True) - async def send_reminder(self, reminder: dict, expected_time: datetime = None) -> None: + async def send_reminder(self, reminder: dict, expected_time: t.Optional[time.Timestamp] = None) -> None: """Send the reminder.""" is_valid, user, channel = self.ensure_valid_reminder(reminder) if not is_valid: @@ -347,8 +347,7 @@ class Reminders(Cog): for content, remind_at, id_, mentions in reminders: # Parse and humanize the time, make it pretty :D - remind_datetime = isoparse(remind_at) - expiry = time.format_relative(remind_datetime) + expiry = time.format_relative(remind_at) mentions = ", ".join([ # Both Role and User objects have the `name` attribute diff --git a/bot/utils/time.py b/bot/utils/time.py index 13dfc6fb7..e927a5e63 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -1,10 +1,10 @@ import datetime import re from enum import Enum +from time import struct_time from typing import Optional, Union import arrow -import dateutil.parser from dateutil.relativedelta import relativedelta DISCORD_TIMESTAMP_REGEX = re.compile(r"") @@ -19,8 +19,18 @@ _DURATION_REGEX = re.compile( r"((?P\d+?) ?(seconds|second|S|s))?" ) - -ValidTimestamp = Union[int, datetime.datetime, datetime.date] +# All supported types for the single-argument overload of arrow.get(). tzinfo is excluded because +# it's too implicit of a way for the caller to specify that they want the current time. +Timestamp = Union[ + arrow.Arrow, + datetime.datetime, + datetime.date, + struct_time, + int, # POSIX timestamp + float, # POSIX timestamp + str, # ISO 8601-formatted string + tuple[int, int, int], # ISO calendar tuple +] class TimestampFormats(Enum): @@ -60,15 +70,14 @@ def _stringify_time_unit(value: int, unit: str) -> str: return f"{value} {unit}" -def discord_timestamp(timestamp: ValidTimestamp, format: TimestampFormats = TimestampFormats.DATE_TIME) -> str: - """Create and format a Discord flavored markdown timestamp.""" - # Convert each possible timestamp class to an integer. - if isinstance(timestamp, datetime.datetime): - timestamp = (timestamp - arrow.get(0)).total_seconds() - elif isinstance(timestamp, datetime.date): - timestamp = (timestamp - arrow.get(0)).total_seconds() +def discord_timestamp(timestamp: Timestamp, format: TimestampFormats = TimestampFormats.DATE_TIME) -> str: + """ + Format a timestamp as a Discord-flavored Markdown timestamp. - return f"" + `timestamp` can be any type supported by the single-arg `arrow.get()`, except for a `tzinfo`. + """ + timestamp = int(arrow.get(timestamp).timestamp()) + return f"" def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units: int = 6) -> str: @@ -115,14 +124,6 @@ def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units: return humanized -def get_time_delta(time_string: str) -> str: - """Returns the time in human-readable time delta format.""" - date_time = dateutil.parser.isoparse(time_string) - time_delta = format_relative(date_time) - - return time_delta - - def parse_duration_string(duration: str) -> Optional[relativedelta]: """ Converts a `duration` string to a relativedelta object. @@ -154,64 +155,63 @@ def relativedelta_to_timedelta(delta: relativedelta) -> datetime.timedelta: return utcnow + delta - utcnow -def format_relative(timestamp: ValidTimestamp) -> str: +def format_relative(timestamp: Timestamp) -> str: """ Format `timestamp` as a relative Discord timestamp. A relative timestamp describes how much time has elapsed since `timestamp` or how much time - remains until `timestamp` is reached. See `time.discord_timestamp`. + remains until `timestamp` is reached. + + `timestamp` can be any type supported by the single-arg `arrow.get()`, except for a `tzinfo`. """ return discord_timestamp(timestamp, TimestampFormats.RELATIVE) -def format_infraction(timestamp: str) -> str: - """Format an infraction timestamp to a discord timestamp.""" - return discord_timestamp(dateutil.parser.isoparse(timestamp)) - - def format_with_duration( - timestamp: Optional[str], - other_timestamp: Optional[datetime.datetime] = None, + timestamp: Optional[Timestamp], + other_timestamp: Optional[Timestamp] = None, max_units: int = 2, ) -> Optional[str]: """ Return `timestamp` formatted as a discord timestamp with the timestamp duration since `other_timestamp`. + `timestamp` and `other_timestamp` can be any type supported by the single-arg `arrow.get()`, + except for a `tzinfo`. Use the current time if `other_timestamp` is falsy or unspecified. + `max_units` specifies the maximum number of units of time to include in the duration. For example, a value of 1 may include days but not hours. + + Return None if `timestamp` is falsy. """ if not timestamp: return None - date_to_formatted = format_infraction(timestamp) - - other_timestamp = other_timestamp or datetime.datetime.now(datetime.timezone.utc) - timestamp = dateutil.parser.isoparse(timestamp).replace(microsecond=0) + timestamp = arrow.get(timestamp) + if not other_timestamp: + other_timestamp = arrow.utcnow() + else: + other_timestamp = arrow.get(other_timestamp) - delta = abs(relativedelta(timestamp, other_timestamp)) + formatted_timestamp = discord_timestamp(timestamp) + delta = abs(relativedelta(timestamp.datetime, other_timestamp.datetime)) duration = humanize_delta(delta, max_units=max_units) - duration_formatted = f" ({duration})" if duration else "" - return f"{date_to_formatted}{duration_formatted}" + return f"{formatted_timestamp} ({duration})" -def until_expiration( - expiry: Optional[str] -) -> Optional[str]: +def until_expiration(expiry: Optional[Timestamp]) -> Optional[str]: """ - Get the remaining time until infraction's expiration, in a discord timestamp. + Get the remaining time until an infraction's expiration as a Discord timestamp. - Returns a human-readable version of the remaining duration between arrow.utcnow() and an expiry. - Similar to format_relative, except that this function doesn't error on a null input - and return null if the expiry is in the paste + `expiry` can be any type supported by the single-arg `arrow.get()`, except for a `tzinfo`. + + Return None if `expiry` is falsy or is in the past. """ if not expiry: return None - now = arrow.utcnow() - since = dateutil.parser.isoparse(expiry).replace(microsecond=0) - - if since < now: + expiry = arrow.get(expiry) + if expiry < arrow.utcnow(): return None - return format_relative(since) + return format_relative(expiry) diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py index 02b5f8c17..027e2052e 100644 --- a/tests/bot/utils/test_time.py +++ b/tests/bot/utils/test_time.py @@ -43,10 +43,6 @@ class TimeTests(unittest.TestCase): time.humanize_delta(relativedelta(days=2, hours=2), 'hours', max_units) self.assertEqual(str(error.exception), 'max_units must be positive') - def test_format_infraction(self): - """Testing format_infraction.""" - self.assertEqual(time.format_infraction('2019-12-12T00:01:00Z'), '') - def test_format_with_duration_none_expiry(self): """format_with_duration should work for None expiry.""" test_cases = ( -- cgit v1.2.3 From 75fabfc0f1a9d95a1167dd6f3d94b741768b72e8 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Thu, 5 Aug 2021 21:26:36 -0700 Subject: Time: remove DISCORD_TIMESTAMP_REGEX There's a saner way to parse the timestamp that relied on this regex. --- bot/exts/moderation/infraction/management.py | 15 ++++++--------- bot/utils/time.py | 2 -- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 23c6e8b92..fa1ebdadc 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -1,8 +1,7 @@ import textwrap import typing as t -from datetime import datetime, timezone -import dateutil.parser +import arrow import discord from dateutil.relativedelta import relativedelta from discord.ext import commands @@ -351,7 +350,8 @@ class ModManagement(commands.Cog): active = infraction["active"] user = infraction["user"] expires_at = infraction["expires_at"] - created = time.discord_timestamp(infraction["inserted_at"]) + inserted_at = infraction["inserted_at"] + created = time.discord_timestamp(inserted_at) dm_sent = infraction["dm_sent"] # Format the user string. @@ -371,12 +371,9 @@ class ModManagement(commands.Cog): if expires_at is None: duration = "*Permanent*" else: - date_from = datetime.fromtimestamp( - float(time.DISCORD_TIMESTAMP_REGEX.match(created).group(1)), - timezone.utc - ) - date_to = dateutil.parser.isoparse(expires_at) - duration = time.humanize_delta(relativedelta(date_to, date_from)) + start = arrow.get(inserted_at).datetime + end = arrow.get(expires_at).datetime + duration = time.humanize_delta(relativedelta(start, end)) # Format `dm_sent` if dm_sent is None: diff --git a/bot/utils/time.py b/bot/utils/time.py index e927a5e63..21d26db7d 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -7,8 +7,6 @@ from typing import Optional, Union import arrow from dateutil.relativedelta import relativedelta -DISCORD_TIMESTAMP_REGEX = re.compile(r"") - _DURATION_REGEX = re.compile( r"((?P\d+?) ?(years|year|Y|y) ?)?" r"((?P\d+?) ?(months|month|m) ?)?" -- cgit v1.2.3 From 2004477e12c72e4739ea1b1f192fb2c12eac69d0 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 6 Aug 2021 14:20:52 -0700 Subject: Time: add overload to pass 2 timestamps to humanize_delta Remove the need for the caller to create a `relativedelta` from 2 timestamps before calling `humanize_delta`. This is especially convenient for cases where the original inputs aren't `datetime`s since `relativedelta` only accepts those. --- bot/exts/moderation/defcon.py | 4 +- bot/exts/moderation/infraction/management.py | 6 +- bot/exts/moderation/modlog.py | 2 +- bot/utils/time.py | 110 ++++++++++++++++++++++----- tests/bot/utils/test_time.py | 13 ++-- 5 files changed, 105 insertions(+), 30 deletions(-) diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 263e8136e..178be734d 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -254,8 +254,8 @@ class Defcon(Cog): expiry_message = "" if expiry: - activity_duration = relativedelta(expiry, arrow.utcnow().datetime) - expiry_message = f" for the next {time.humanize_delta(activity_duration, max_units=2)}" + formatted_expiry = time.humanize_delta(expiry, max_units=2) + expiry_message = f" for the next {formatted_expiry}" if self.threshold: channel_message = ( diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index fa1ebdadc..0dfd2d759 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -1,9 +1,7 @@ import textwrap import typing as t -import arrow import discord -from dateutil.relativedelta import relativedelta from discord.ext import commands from discord.ext.commands import Context from discord.utils import escape_markdown @@ -371,9 +369,7 @@ class ModManagement(commands.Cog): if expires_at is None: duration = "*Permanent*" else: - start = arrow.get(inserted_at).datetime - end = arrow.get(expires_at).datetime - duration = time.humanize_delta(relativedelta(start, end)) + duration = time.humanize_delta(inserted_at, expires_at) # Format `dm_sent` if dm_sent is None: diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index d5e209d81..2c01a4a21 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -713,7 +713,7 @@ class ModLog(Cog, name="ModLog"): # datetime as the baseline and create a human-readable delta between this edit event # and the last time the message was edited timestamp = msg_before.edited_at - delta = time.humanize_delta(relativedelta(msg_after.edited_at, msg_before.edited_at)) + delta = time.humanize_delta(msg_after.edited_at, msg_before.edited_at) footer = f"Last edited {delta} ago" else: # Message was not previously edited, use the created_at datetime as the baseline, no diff --git a/bot/utils/time.py b/bot/utils/time.py index 21d26db7d..7e314a870 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -2,7 +2,7 @@ import datetime import re from enum import Enum from time import struct_time -from typing import Optional, Union +from typing import Optional, Union, overload import arrow from dateutil.relativedelta import relativedelta @@ -78,15 +78,99 @@ def discord_timestamp(timestamp: Timestamp, format: TimestampFormats = Timestamp return f"" -def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units: int = 6) -> str: +@overload +def humanize_delta( + arg1: Union[relativedelta, Timestamp], + /, + *, + precision: str = "seconds", + max_units: int = 6, + absolute: bool = True, +) -> str: + ... + + +@overload +def humanize_delta( + end: Timestamp, + start: Timestamp, + /, + *, + precision: str = "seconds", + max_units: int = 6, + absolute: bool = True, +) -> str: + ... + + +def humanize_delta( + *args, + precision: str = "seconds", + max_units: int = 6, + absolute: bool = True, +) -> str: """ - Returns a human-readable version of the relativedelta. + Return a human-readable version of a time duration. - precision specifies the smallest unit of time to include (e.g. "seconds", "minutes"). - max_units specifies the maximum number of units of time to include (e.g. 1 may include days but not hours). + `precision` is the smallest unit of time to include (e.g. "seconds", "minutes"). + + `max_units` is the maximum number of units of time to include. + Count units from largest to smallest (e.g. count days before months). + + Use the absolute value of the duration if `absolute` is True. + + Usage: + + **One** `relativedelta` object, to humanize the duration represented by it: + + >>> humanize_delta(relativedelta(years=12, months=6)) + '12 years and 6 months' + + Note that `leapdays` and absolute info (singular names) will be ignored during humanization. + + **One** timestamp of a type supported by the single-arg `arrow.get()`, except for `tzinfo`, + to humanize the duration between it and the current time: + + >>> humanize_delta('2021-08-06T12:43:01Z', absolute=True) # now = 2021-08-06T12:33:33Z + '9 minutes and 28 seconds' + + >>> humanize_delta('2021-08-06T12:43:01Z', absolute=False) # now = 2021-08-06T12:33:33Z + '-9 minutes and -28 seconds' + + **Two** timestamps, each of a type supported by the single-arg `arrow.get()`, except for + `tzinfo`, to humanize the duration between them: + + >>> humanize_delta(datetime.datetime(2020, 1, 1), '2021-01-01T12:00:00Z', absolute=False) + '1 year and 12 hours' + + >>> humanize_delta('2021-01-01T12:00:00Z', datetime.datetime(2020, 1, 1), absolute=False) + '-1 years and -12 hours' + + Note that order of the arguments can result in a different output even if `absolute` is True: + + >>> x = datetime.datetime(3000, 11, 1) + >>> y = datetime.datetime(3000, 9, 2) + >>> humanize_delta(y, x, absolute=True), humanize_delta(x, y, absolute=True) + ('1 month and 30 days', '1 month and 29 days') + + This is due to the nature of `relativedelta`; it does not represent a fixed period of time. + Instead, it's relative to the `datetime` to which it's added to get the other `datetime`. + In the example, the difference arises because all months don't have the same number of days. """ + if len(args) == 1 and isinstance(args[0], relativedelta): + delta = args[0] + elif 1 <= len(args) <= 2: + end = arrow.get(args[0]) + start = arrow.get(args[1]) if len(args) == 2 else arrow.utcnow() + + delta = relativedelta(end.datetime, start.datetime) + if absolute: + delta = abs(delta) + else: + raise ValueError(f"Received {len(args)} positional arguments, but expected 1 or 2.") + if max_units <= 0: - raise ValueError("max_units must be positive") + raise ValueError("max_units must be positive.") units = ( ("years", delta.years), @@ -174,25 +258,17 @@ def format_with_duration( Return `timestamp` formatted as a discord timestamp with the timestamp duration since `other_timestamp`. `timestamp` and `other_timestamp` can be any type supported by the single-arg `arrow.get()`, - except for a `tzinfo`. Use the current time if `other_timestamp` is falsy or unspecified. + except for a `tzinfo`. Use the current time if `other_timestamp` is None or unspecified. - `max_units` specifies the maximum number of units of time to include in the duration. For - example, a value of 1 may include days but not hours. + `max_units` is forwarded to `time.humanize_delta`. See its documentation for more information. Return None if `timestamp` is falsy. """ if not timestamp: return None - timestamp = arrow.get(timestamp) - if not other_timestamp: - other_timestamp = arrow.utcnow() - else: - other_timestamp = arrow.get(other_timestamp) - formatted_timestamp = discord_timestamp(timestamp) - delta = abs(relativedelta(timestamp.datetime, other_timestamp.datetime)) - duration = humanize_delta(delta, max_units=max_units) + duration = humanize_delta(timestamp, other_timestamp, max_units=max_units) return f"{formatted_timestamp} ({duration})" diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py index 027e2052e..e235f9b70 100644 --- a/tests/bot/utils/test_time.py +++ b/tests/bot/utils/test_time.py @@ -13,13 +13,15 @@ class TimeTests(unittest.TestCase): """humanize_delta should be able to handle unknown units, and will not abort.""" # Does not abort for unknown units, as the unit name is checked # against the attribute of the relativedelta instance. - self.assertEqual(time.humanize_delta(relativedelta(days=2, hours=2), 'elephants', 2), '2 days and 2 hours') + actual = time.humanize_delta(relativedelta(days=2, hours=2), precision='elephants', max_units=2) + self.assertEqual(actual, '2 days and 2 hours') def test_humanize_delta_handle_high_units(self): """humanize_delta should be able to handle very high units.""" # Very high maximum units, but it only ever iterates over # each value the relativedelta might have. - self.assertEqual(time.humanize_delta(relativedelta(days=2, hours=2), 'hours', 20), '2 days and 2 hours') + actual = time.humanize_delta(relativedelta(days=2, hours=2), precision='hours', max_units=20) + self.assertEqual(actual, '2 days and 2 hours') def test_humanize_delta_should_normal_usage(self): """Testing humanize delta.""" @@ -32,7 +34,8 @@ class TimeTests(unittest.TestCase): for delta, precision, max_units, expected in test_cases: with self.subTest(delta=delta, precision=precision, max_units=max_units, expected=expected): - self.assertEqual(time.humanize_delta(delta, precision, max_units), expected) + actual = time.humanize_delta(delta, precision=precision, max_units=max_units) + self.assertEqual(actual, expected) def test_humanize_delta_raises_for_invalid_max_units(self): """humanize_delta should raises ValueError('max_units must be positive') for invalid max_units.""" @@ -40,8 +43,8 @@ class TimeTests(unittest.TestCase): for max_units in test_cases: with self.subTest(max_units=max_units), self.assertRaises(ValueError) as error: - time.humanize_delta(relativedelta(days=2, hours=2), 'hours', max_units) - self.assertEqual(str(error.exception), 'max_units must be positive') + time.humanize_delta(relativedelta(days=2, hours=2), precision='hours', max_units=max_units) + self.assertEqual(str(error.exception), 'max_units must be positive.') def test_format_with_duration_none_expiry(self): """format_with_duration should work for None expiry.""" -- cgit v1.2.3 From 07b345eed59e775977da202602ed1c9568cca494 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 6 Aug 2021 15:25:17 -0700 Subject: Time: add overload to pass relativedelta kwargs to humanize_delta --- bot/exts/moderation/slowmode.py | 3 +-- bot/utils/time.py | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py index da04d1e98..b6a771441 100644 --- a/bot/exts/moderation/slowmode.py +++ b/bot/exts/moderation/slowmode.py @@ -39,8 +39,7 @@ class Slowmode(Cog): if channel is None: channel = ctx.channel - delay = relativedelta(seconds=channel.slowmode_delay) - humanized_delay = time.humanize_delta(delay) + humanized_delay = time.humanize_delta(seconds=channel.slowmode_delay) await ctx.send(f'The slowmode delay for {channel.mention} is {humanized_delay}.') diff --git a/bot/utils/time.py b/bot/utils/time.py index 7e314a870..6fc43ef6a 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -103,11 +103,29 @@ def humanize_delta( ... +@overload +def humanize_delta( + *, + years: int = 0, + months: int = 0, + weeks: float = 0, + days: float = 0, + hours: float = 0, + minutes: float = 0, + seconds: float = 0, + precision: str = "seconds", + max_units: int = 6, + absolute: bool = True, +) -> str: + ... + + def humanize_delta( *args, precision: str = "seconds", max_units: int = 6, absolute: bool = True, + **kwargs, ) -> str: """ Return a human-readable version of a time duration. @@ -121,6 +139,12 @@ def humanize_delta( Usage: + Keyword arguments specifying values for time units, to construct a `relativedelta` and humanize + the duration represented by it: + + >>> humanize_delta(days=2, hours=16, seconds=23) + '2 days, 16 hours and 23 seconds' + **One** `relativedelta` object, to humanize the duration represented by it: >>> humanize_delta(relativedelta(years=12, months=6)) @@ -157,9 +181,14 @@ def humanize_delta( Instead, it's relative to the `datetime` to which it's added to get the other `datetime`. In the example, the difference arises because all months don't have the same number of days. """ - if len(args) == 1 and isinstance(args[0], relativedelta): + if args and kwargs: + raise ValueError("Unsupported combination of positional and keyword arguments.") + + if len(args) == 0: + delta = relativedelta(**kwargs) + elif len(args) == 1 and isinstance(args[0], relativedelta): delta = args[0] - elif 1 <= len(args) <= 2: + elif len(args) <= 2: end = arrow.get(args[0]) start = arrow.get(args[1]) if len(args) == 2 else arrow.utcnow() -- cgit v1.2.3 From 40a2f71c41e5420b22e71a9d234bb83fd97729a5 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Fri, 6 Aug 2021 15:38:48 -0700 Subject: Time: use typing.Literal for precision param of humanize_delta --- bot/utils/time.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index 6fc43ef6a..8ba49a455 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -2,7 +2,7 @@ import datetime import re from enum import Enum from time import struct_time -from typing import Optional, Union, overload +from typing import Literal, Optional, Union, overload import arrow from dateutil.relativedelta import relativedelta @@ -29,6 +29,7 @@ Timestamp = Union[ str, # ISO 8601-formatted string tuple[int, int, int], # ISO calendar tuple ] +_Precision = Literal["years", "months", "days", "hours", "minutes", "seconds"] class TimestampFormats(Enum): @@ -83,7 +84,7 @@ def humanize_delta( arg1: Union[relativedelta, Timestamp], /, *, - precision: str = "seconds", + precision: _Precision = "seconds", max_units: int = 6, absolute: bool = True, ) -> str: @@ -96,7 +97,7 @@ def humanize_delta( start: Timestamp, /, *, - precision: str = "seconds", + precision: _Precision = "seconds", max_units: int = 6, absolute: bool = True, ) -> str: @@ -113,7 +114,7 @@ def humanize_delta( hours: float = 0, minutes: float = 0, seconds: float = 0, - precision: str = "seconds", + precision: _Precision = "seconds", max_units: int = 6, absolute: bool = True, ) -> str: @@ -122,7 +123,7 @@ def humanize_delta( def humanize_delta( *args, - precision: str = "seconds", + precision: _Precision = "seconds", max_units: int = 6, absolute: bool = True, **kwargs, -- cgit v1.2.3 From 2209b9f1b95cbe2366ea2b316046ddd35ff6d3a9 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 7 Aug 2021 10:48:57 -0700 Subject: Fix create_user_embed tests Mock User.created_at and User.joined_at because `arrow.get()` doesn't work with Mock objects. The old implementation of `time.discord_timestamp` accepted mocks because it just did `int()` on any type it didn't explicitly check for. --- tests/bot/exts/info/test_information.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index 30e5258fb..d896b7652 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -1,6 +1,7 @@ import textwrap import unittest import unittest.mock +from datetime import datetime import discord @@ -288,6 +289,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): user.nick = None user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock") user.colour = 0 + user.created_at = user.joined_at = datetime.utcnow() embed = await self.cog.create_user_embed(ctx, user, False) @@ -309,6 +311,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): user.nick = "Cat lover" user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock") user.colour = 0 + user.created_at = user.joined_at = datetime.utcnow() embed = await self.cog.create_user_embed(ctx, user, False) @@ -329,6 +332,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): # A `MockMember` has the @Everyone role by default; we add the Admins to that. user = helpers.MockMember(roles=[admins_role], colour=100) + user.created_at = user.joined_at = datetime.utcnow() embed = await self.cog.create_user_embed(ctx, user, False) @@ -355,6 +359,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): nomination_counts.return_value = ("Nominations", "nomination info") user = helpers.MockMember(id=314, roles=[moderators_role], colour=100) + user.created_at = user.joined_at = datetime.utcfromtimestamp(1) embed = await self.cog.create_user_embed(ctx, user, False) infraction_counts.assert_called_once_with(user) @@ -394,6 +399,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): user_messages.return_value = ("Messages", "user message counts") user = helpers.MockMember(id=314, roles=[moderators_role], colour=100) + user.created_at = user.joined_at = datetime.utcfromtimestamp(1) embed = await self.cog.create_user_embed(ctx, user, False) infraction_counts.assert_called_once_with(user) @@ -440,6 +446,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): moderators_role = helpers.MockRole(name='Moderators') user = helpers.MockMember(id=314, roles=[moderators_role], colour=100) + user.created_at = user.joined_at = datetime.utcnow() embed = await self.cog.create_user_embed(ctx, user, False) self.assertEqual(embed.colour, discord.Colour(100)) @@ -457,6 +464,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): ctx = helpers.MockContext() user = helpers.MockMember(id=217, colour=discord.Colour.default()) + user.created_at = user.joined_at = datetime.utcnow() embed = await self.cog.create_user_embed(ctx, user, False) self.assertEqual(embed.colour, discord.Colour.og_blurple()) @@ -474,6 +482,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): ctx = helpers.MockContext() user = helpers.MockMember(id=217, colour=0) + user.created_at = user.joined_at = datetime.utcnow() user.display_avatar.url = "avatar url" embed = await self.cog.create_user_embed(ctx, user, False) -- cgit v1.2.3 From 1af466753975b70effd5e600d0afc8b21f272dd0 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 7 Aug 2021 10:57:24 -0700 Subject: Time: return strings from until_expiration instead of ambiguous None None was returned for two separate cases: permanent infractions and expired infractions. This resulted in an ambiguity. --- bot/exts/moderation/infraction/management.py | 6 +++--- bot/utils/time.py | 8 ++++---- tests/bot/utils/test_time.py | 5 ++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index 0dfd2d759..dda3fadae 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -179,8 +179,8 @@ class ModManagement(commands.Cog): self.infractions_cog.schedule_expiration(new_infraction) log_text += f""" - Previous expiry: {time.until_expiration(infraction['expires_at']) or "Permanent"} - New expiry: {time.until_expiration(new_infraction['expires_at']) or "Permanent"} + Previous expiry: {time.until_expiration(infraction['expires_at'])} + New expiry: {time.until_expiration(new_infraction['expires_at'])} """.rstrip() changes = ' & '.join(confirm_messages) @@ -362,7 +362,7 @@ class ModManagement(commands.Cog): user_str = f"<@{user['id']}> ({name}#{user['discriminator']:04})" if active: - remaining = time.until_expiration(expires_at) or "Expired" + remaining = time.until_expiration(expires_at) else: remaining = "Inactive" diff --git a/bot/utils/time.py b/bot/utils/time.py index 8ba49a455..4b2fbae2c 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -303,19 +303,19 @@ def format_with_duration( return f"{formatted_timestamp} ({duration})" -def until_expiration(expiry: Optional[Timestamp]) -> Optional[str]: +def until_expiration(expiry: Optional[Timestamp]) -> str: """ Get the remaining time until an infraction's expiration as a Discord timestamp. `expiry` can be any type supported by the single-arg `arrow.get()`, except for a `tzinfo`. - Return None if `expiry` is falsy or is in the past. + Return "Permanent" if `expiry` is falsy. Return "Expired" if `expiry` is in the past. """ if not expiry: - return None + return "Permanent" expiry = arrow.get(expiry) if expiry < arrow.utcnow(): - return None + return "Expired" return format_relative(expiry) diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py index e235f9b70..120d65176 100644 --- a/tests/bot/utils/test_time.py +++ b/tests/bot/utils/test_time.py @@ -100,8 +100,8 @@ class TimeTests(unittest.TestCase): self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected) def test_until_expiration_with_duration_none_expiry(self): - """until_expiration should work for None expiry.""" - self.assertEqual(time.until_expiration(None), None) + """until_expiration should return "Permanent" is expiry is None.""" + self.assertEqual(time.until_expiration(None), "Permanent") def test_until_expiration_with_duration_custom_units(self): """until_expiration should work for custom max_units.""" @@ -122,7 +122,6 @@ class TimeTests(unittest.TestCase): ('3000-12-12T00:00:00Z', ''), ('3000-11-23T20:09:00Z', ''), ('3000-11-23T20:09:00Z', ''), - (None, None), ) for expiry, expected in test_cases: -- cgit v1.2.3 From 38e0789890ce9d3c7307de158c9277f6aa20b848 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sat, 7 Aug 2021 11:01:30 -0700 Subject: Time: check timestamp for None only rather than if it's falsy Integers and floats which are 0 are considered valid timestamps, but are falsy. --- bot/utils/time.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index 4b2fbae2c..29fc46d56 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -292,9 +292,9 @@ def format_with_duration( `max_units` is forwarded to `time.humanize_delta`. See its documentation for more information. - Return None if `timestamp` is falsy. + Return None if `timestamp` is None. """ - if not timestamp: + if timestamp is None: return None formatted_timestamp = discord_timestamp(timestamp) @@ -309,9 +309,9 @@ def until_expiration(expiry: Optional[Timestamp]) -> str: `expiry` can be any type supported by the single-arg `arrow.get()`, except for a `tzinfo`. - Return "Permanent" if `expiry` is falsy. Return "Expired" if `expiry` is in the past. + Return "Permanent" if `expiry` is None. Return "Expired" if `expiry` is in the past. """ - if not expiry: + if expiry is None: return "Permanent" expiry = arrow.get(expiry) -- cgit v1.2.3 From 6b5431017c0c49deb689390ceccfe344662b8d30 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 12 Aug 2021 19:38:30 +0200 Subject: Store paths on Tags instead of only accepting the file contents --- bot/exts/info/tags.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index beabade58..ddd372fe7 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -81,8 +81,9 @@ class TagIdentifier(NamedTuple): class Tag: """Provide an interface to a tag from resources with `file_content`.""" - def __init__(self, file_content: str): - post = frontmatter.loads(file_content) + def __init__(self, content_path: Path): + post = frontmatter.loads(content_path.read_text("utf8")) + self.file_path = content_path self.content = post.content self.metadata = post.metadata self._restricted_to: set[int] = set(self.metadata.get("restricted_to", ())) @@ -147,7 +148,7 @@ class Tags(Cog): tag_name = file.stem tag_group = parent_dir.name if parent_dir.name else None - self._tags[TagIdentifier(tag_group, tag_name)] = Tag(file.read_text("utf-8")) + self._tags[TagIdentifier(tag_group, tag_name)] = Tag(file) def _get_suggestions(self, tag_identifier: TagIdentifier) -> list[tuple[TagIdentifier, Tag]]: """Return a list of suggested tags for `tag_identifier`.""" -- cgit v1.2.3 From f92e07104c7ac0a1abaed4fb49a7469872d726cb Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 12 Aug 2021 19:40:04 +0200 Subject: make the tags attribute public the tags need to be accessed by the source cog --- bot/exts/info/tags.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index ddd372fe7..8dcfa279e 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -134,11 +134,11 @@ class Tags(Cog): def __init__(self, bot: Bot): self.bot = bot - self._tags: dict[TagIdentifier, Tag] = {} + self.tags: dict[TagIdentifier, Tag] = {} self.initialize_tags() def initialize_tags(self) -> None: - """Load all tags from resources into `self._tags`.""" + """Load all tags from resources into `self.tags`.""" base_path = Path("bot", "resources", "tags") for file in base_path.glob("**/*"): @@ -148,14 +148,14 @@ class Tags(Cog): tag_name = file.stem tag_group = parent_dir.name if parent_dir.name else None - self._tags[TagIdentifier(tag_group, tag_name)] = Tag(file) + self.tags[TagIdentifier(tag_group, tag_name)] = Tag(file) def _get_suggestions(self, tag_identifier: TagIdentifier) -> list[tuple[TagIdentifier, Tag]]: """Return a list of suggested tags for `tag_identifier`.""" for threshold in [100, 90, 80, 70, 60]: suggestions = [ (identifier, tag) - for identifier, tag in self._tags.items() + for identifier, tag in self.tags.items() if identifier.get_fuzzy_score(tag_identifier) >= threshold ] if suggestions: @@ -206,7 +206,7 @@ class Tags(Cog): keywords_processed = [keywords] matching_tags = [] - for identifier, tag in self._tags.items(): + for identifier, tag in self.tags.items(): matches = (query in tag.content.casefold() for query in keywords_processed) if tag.accessible_by(user) and check(matches): matching_tags.append((identifier, tag)) @@ -285,7 +285,7 @@ class Tags(Cog): if tag.accessible_by(ctx.author) ] - tag = self._tags.get(tag_identifier) + tag = self.tags.get(tag_identifier) if tag is None and len(filtered_tags) == 1: tag_identifier = filtered_tags[0][0] tag = filtered_tags[0][1] @@ -330,7 +330,7 @@ class Tags(Cog): current_group = object() group_accessible = True - for identifier, tag in sorted(self._tags.items(), key=tag_sort_key): + for identifier, tag in sorted(self.tags.items(), key=tag_sort_key): if identifier.group != current_group: if not group_accessible: @@ -356,7 +356,7 @@ class Tags(Cog): embed = Embed(title=f"Tags under *{group}*") tag_lines = sorted( f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}" - for identifier, tag in self._tags.items() + for identifier, tag in self.tags.items() if identifier.group == group and tag.accessible_by(ctx.author) ) await LinePaginator.paginate(tag_lines, ctx, embed, footer_text=FOOTER_TEXT, empty=False, max_lines=15) @@ -378,7 +378,7 @@ class Tags(Cog): Returns False if no message was sent. """ # noqa: D205, D415 if tag_name_or_group is None and tag_name is None: - if self._tags: + if self.tags: await self.list_all_tags(ctx) return True else: @@ -388,7 +388,7 @@ class Tags(Cog): elif tag_name is None: if any( tag_name_or_group == identifier.group and tag.accessible_by(ctx.author) - for identifier, tag in self._tags.items() + for identifier, tag in self.tags.items() ): await self.list_tags_in_group(ctx, tag_name_or_group) return True -- cgit v1.2.3 From f83dee308201ed6bddb4650f3a46e7b7d924ea54 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Thu, 12 Aug 2021 20:09:39 +0200 Subject: Use new Tags cog structure in source.py --- bot/exts/info/source.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/bot/exts/info/source.py b/bot/exts/info/source.py index ef07c77a1..723ae5aba 100644 --- a/bot/exts/info/source.py +++ b/bot/exts/info/source.py @@ -7,8 +7,9 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import URLs +from bot.exts.info.tags import TagIdentifier -SourceType = Union[commands.HelpCommand, commands.Command, commands.Cog, str, commands.ExtensionNotLoaded] +SourceType = Union[commands.HelpCommand, commands.Command, commands.Cog, TagIdentifier, commands.ExtensionNotLoaded] class SourceConverter(commands.Converter): @@ -33,8 +34,10 @@ class SourceConverter(commands.Converter): if not tags_cog: show_tag = False - elif argument.lower() in tags_cog._cache: - return argument.lower() + else: + identifier = TagIdentifier.from_string(argument.lower()) + if identifier in tags_cog.tags: + return identifier escaped_arg = utils.escape_markdown(argument) @@ -72,9 +75,9 @@ class BotSource(commands.Cog): source_item = inspect.unwrap(source_item.callback) src = source_item.__code__ filename = src.co_filename - elif isinstance(source_item, str): + elif isinstance(source_item, TagIdentifier): tags_cog = self.bot.get_cog("Tags") - filename = tags_cog._cache[source_item]["location"] + filename = tags_cog.tags[source_item].file_path else: src = type(source_item) try: @@ -82,7 +85,7 @@ class BotSource(commands.Cog): except TypeError: raise commands.BadArgument("Cannot get source for a dynamically-created object.") - if not isinstance(source_item, str): + if not isinstance(source_item, TagIdentifier): try: lines, first_line_no = inspect.getsourcelines(src) except OSError: @@ -95,7 +98,7 @@ class BotSource(commands.Cog): # Handle tag file location differently than others to avoid errors in some cases if not first_line_no: - file_location = Path(filename).relative_to("/bot/") + file_location = Path(filename).relative_to("bot/") else: file_location = Path(filename).relative_to(Path.cwd()).as_posix() @@ -113,7 +116,7 @@ class BotSource(commands.Cog): elif isinstance(source_object, commands.Command): description = source_object.short_doc title = f"Command: {source_object.qualified_name}" - elif isinstance(source_object, str): + elif isinstance(source_object, TagIdentifier): title = f"Tag: {source_object}" description = "" else: -- cgit v1.2.3 From c9e85dd5b7104bc652861c1d9623ce4da4122ae8 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Sat, 14 Aug 2021 16:59:50 +0200 Subject: use an empty string as the initial group value --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 8dcfa279e..68ab5f6bb 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -327,7 +327,7 @@ class Tags(Cog): return group + name result_lines = [] - current_group = object() + current_group = "" group_accessible = True for identifier, tag in sorted(self.tags.items(), key=tag_sort_key): -- cgit v1.2.3 From 0e2491dd7d9e3023284a9f7b3cc450bc9dbe9b25 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Sun, 15 Aug 2021 21:29:13 +0200 Subject: Reword `tag get` help --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 68ab5f6bb..d80ca448d 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -369,7 +369,7 @@ class Tags(Cog): ) -> bool: """ When arguments are passed in: - If a single argument is given and it matches a group name, list accessible all tags from that group. + If a single argument matching a group name is given, list all accessible tags from that group Otherwise display the tag if one was found for the given arguments, or try to display suggestions for that name With no arguments, list all accessible tags -- cgit v1.2.3 From 4724853d8155444ae1db50c24c5424c4d25f3dec Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Sun, 15 Aug 2021 21:31:47 +0200 Subject: Fix incorrect annotation Co-authored-by: Bluenix --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index d80ca448d..5632f2959 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -318,7 +318,7 @@ class Tags(Cog): async def list_all_tags(self, ctx: Context) -> None: """Send a paginator with all loaded tags accessible by `ctx.author`, groups first, and alphabetically sorted.""" - def tag_sort_key(tag_item: tuple[TagIdentifier, tag]) -> str: + def tag_sort_key(tag_item: tuple[TagIdentifier, Tag]) -> str: group, name = tag_item[0] if group is None: # Max codepoint character to force tags without a group to the end -- cgit v1.2.3 From 54cb20ea9af50c52fda40fe468470f4e7d351fed Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Sun, 15 Aug 2021 22:08:26 +0200 Subject: refactor fuzzy_search to use conventional iteration Co-authored-by: Bluenix --- bot/exts/info/tags.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 5632f2959..d659be8c4 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -117,15 +117,13 @@ def _fuzzy_search(search: str, target: str) -> float: current, index = 0, 0 _search = REGEX_NON_ALPHABET.sub("", search.lower()) _targets = iter(REGEX_NON_ALPHABET.split(target.lower())) - _target = next(_targets) - try: - while True: - while index < len(_target) and _search[current] == _target[index]: - current += 1 - index += 1 - index, _target = 0, next(_targets) - except (StopIteration, IndexError): - pass + + for _target in _targets: + while index < len(_target) and _search[current] == _target[index]: + current += 1 + index += 1 + index = 0 + return current / len(_search) -- cgit v1.2.3 From 2b8a5b6f5b275c40af1139fab07461e6e96bdeb4 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Sun, 15 Aug 2021 22:09:32 +0200 Subject: Move definition of loop vars next to loop --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index d659be8c4..8bb682366 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -114,10 +114,10 @@ class Tag: def _fuzzy_search(search: str, target: str) -> float: """A simple scoring algorithm based on how many letters are found / total, with order in mind.""" - current, index = 0, 0 _search = REGEX_NON_ALPHABET.sub("", search.lower()) _targets = iter(REGEX_NON_ALPHABET.split(target.lower())) + current, index = 0, 0 for _target in _targets: while index < len(_target) and _search[current] == _target[index]: current += 1 -- cgit v1.2.3 From 9dc4b3e26e1c355c2626a4fca3bc6327c2e9d132 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 23 Aug 2021 21:13:08 +0200 Subject: remove redundant index assignments Co-authored-by: Bluenix --- bot/exts/info/tags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 8bb682366..520089e19 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -117,12 +117,12 @@ def _fuzzy_search(search: str, target: str) -> float: _search = REGEX_NON_ALPHABET.sub("", search.lower()) _targets = iter(REGEX_NON_ALPHABET.split(target.lower())) - current, index = 0, 0 + current = 0 for _target in _targets: + index = 0 while index < len(_target) and _search[current] == _target[index]: current += 1 index += 1 - index = 0 return current / len(_search) -- cgit v1.2.3 From 0ead9a5e53548107d06ab8c69522359b9558061d Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 23 Aug 2021 21:16:48 +0200 Subject: Fix tag fuzzy matching when searching against a longer target --- bot/exts/info/tags.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 520089e19..884c76ec4 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -120,9 +120,13 @@ def _fuzzy_search(search: str, target: str) -> float: current = 0 for _target in _targets: index = 0 - while index < len(_target) and _search[current] == _target[index]: - current += 1 - index += 1 + try: + while index < len(_target) and _search[current] == _target[index]: + current += 1 + index += 1 + except IndexError: + # Exit when _search runs out + break return current / len(_search) -- cgit v1.2.3 From e34d4cacb903cb155236c1c1c945d6159869fb59 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Mon, 23 Aug 2021 20:20:32 -0700 Subject: Time: put region comments around overloads --- bot/utils/time.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/utils/time.py b/bot/utils/time.py index 29fc46d56..005608beb 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -79,6 +79,7 @@ def discord_timestamp(timestamp: Timestamp, format: TimestampFormats = Timestamp return f"" +# region humanize_delta overloads @overload def humanize_delta( arg1: Union[relativedelta, Timestamp], @@ -119,6 +120,7 @@ def humanize_delta( absolute: bool = True, ) -> str: ... +# endregion def humanize_delta( -- cgit v1.2.3 From 27e2666e7c8828f49c432ef29e0098c37bd5fc3a Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Sat, 28 Aug 2021 00:47:12 +0200 Subject: Remove unnecessary line in help Co-authored-by: Bluenix --- bot/exts/info/tags.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 884c76ec4..91046c654 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -370,7 +370,6 @@ class Tags(Cog): tag_name: TagNameConverter = None, ) -> bool: """ - When arguments are passed in: If a single argument matching a group name is given, list all accessible tags from that group Otherwise display the tag if one was found for the given arguments, or try to display suggestions for that name -- cgit v1.2.3 From 15205e11fa132e076f992bfdbad2dd95894d2216 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Sat, 28 Aug 2021 00:53:59 +0200 Subject: simplify assignment and add comment explaining its purpose --- bot/exts/info/tags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 91046c654..931c01e3a 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -146,9 +146,9 @@ class Tags(Cog): for file in base_path.glob("**/*"): if file.is_file(): parent_dir = file.relative_to(base_path).parent - tag_name = file.stem - tag_group = parent_dir.name if parent_dir.name else None + # Files directly under `base_path` have an empty string as the parent directory name + tag_group = parent_dir.name or None self.tags[TagIdentifier(tag_group, tag_name)] = Tag(file) -- cgit v1.2.3 From b9b19ba92cea6baa52c6db6f5eefd79cffbb7bf9 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Sun, 29 Aug 2021 17:43:57 +0200 Subject: Fix punctuation Co-authored-by: Bluenix --- bot/exts/info/tags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 931c01e3a..cd02f1768 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -371,9 +371,9 @@ class Tags(Cog): ) -> bool: """ If a single argument matching a group name is given, list all accessible tags from that group - Otherwise display the tag if one was found for the given arguments, or try to display suggestions for that name + Otherwise display the tag if one was found for the given arguments, or try to display suggestions for that name. - With no arguments, list all accessible tags + With no arguments, list all accessible tags. Returns True if a message was sent, or if the tag is on cooldown. Returns False if no message was sent. -- cgit v1.2.3 From 9c740dca0f7685a0e0e02b7a1de5557f900b84fc Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Sun, 29 Aug 2021 19:04:25 +0200 Subject: Simplify group_score definition Co-authored-by: Bluenix --- bot/exts/info/tags.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index cd02f1768..b7c361c78 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -43,19 +43,14 @@ class TagIdentifier(NamedTuple): def get_fuzzy_score(self, fuzz_tag_identifier: TagIdentifier) -> float: """Get fuzzy score, using `fuzz_tag_identifier` as the identifier to fuzzy match with.""" - if self.group is None: - if fuzz_tag_identifier.group is None: - # We're only fuzzy matching the name - group_score = 1 - else: - # Ignore tags without groups if the identifier contains a group - return .0 + if (self.group is None) != (fuzz_tag_identifier.group is None): + # Ignore tags without groups if the identifier has a group and vice versa + return .0 + if self.group == fuzz_tag_identifier.group: + # Completely identical, or both None + group_score = 1 else: - if fuzz_tag_identifier.group is None: - # Ignore tags with groups if the identifier does not have a group - return .0 - else: - group_score = _fuzzy_search(fuzz_tag_identifier.group, self.group) + group_score = _fuzzy_search(fuzz_tag_identifier.group, self.group) fuzzy_score = group_score * _fuzzy_search(fuzz_tag_identifier.name, self.name) * 100 if fuzzy_score: -- cgit v1.2.3 From ba2a74ac18f9e40fad917d51ffa2238a340416c0 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Sep 2021 01:02:54 +0200 Subject: simplify fuzzy suggestion func Co-authored-by: Bluenix --- bot/exts/info/tags.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index b7c361c78..909831bc7 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -162,21 +162,16 @@ class Tags(Cog): def get_fuzzy_matches(self, tag_identifier: TagIdentifier) -> list[tuple[TagIdentifier, Tag]]: """Get tags with identifiers similar to `tag_identifier`.""" - if tag_identifier.group is None: - if len(tag_identifier.name) < 3: - return [] - else: - return self._get_suggestions(tag_identifier) - else: - if len(tag_identifier.group) < 3: - suggestions = [] - else: - # Try fuzzy matching with only a name first - suggestions = self._get_suggestions(TagIdentifier(None, tag_identifier.group)) + suggestions = [] + + if tag_identifier.group is not None and len(tag_identifier.group) >= 3: + # Try fuzzy matching with only a name first + suggestions += self._get_suggestions(TagIdentifier(None, tag_identifier.group)) + + if len(tag_identifier.name) >= 3: + suggestions += self._get_suggestions(tag_identifier) - if len(tag_identifier.name) >= 3: - suggestions += self._get_suggestions(tag_identifier) - return suggestions + return suggestions def _get_tags_via_content( self, -- cgit v1.2.3 From f5db0b8e3d4e954a5e25737c27f4454c9b46a1c3 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Sep 2021 01:32:17 +0200 Subject: Remove TagNameConverter The converter was now only used to restrict requested names which can be handled by not matching a tag in the cog and not displaying output, this allows us to be a bit more generous with tag names during the command fallback when a name with invalid symbols is parsed after a group --- bot/converters.py | 35 ---------------------------- bot/exts/backend/error_handler.py | 26 +++++++-------------- bot/exts/info/tags.py | 9 ++++--- tests/bot/exts/backend/test_error_handler.py | 24 ------------------- tests/bot/test_converters.py | 29 ----------------------- 5 files changed, 13 insertions(+), 110 deletions(-) diff --git a/bot/converters.py b/bot/converters.py index 2a3943831..038d2a287 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -236,41 +236,6 @@ class Snowflake(IDConverter): return snowflake -class TagNameConverter(Converter): - """ - Ensure that a proposed tag name is valid. - - Valid tag names meet the following conditions: - * All ASCII characters - * Has at least one non-whitespace character - * Not solely numeric - * Shorter than 127 characters - """ - - @staticmethod - async def convert(ctx: Context, tag_name: str) -> str: - """Lowercase & strip whitespace from proposed tag_name & ensure it's valid.""" - tag_name = tag_name.lower().strip() - - # The tag name has at least one invalid character. - if ascii(tag_name)[1:-1] != tag_name: - raise BadArgument("Don't be ridiculous, you can't use that character!") - - # The tag name is either empty, or consists of nothing but whitespace. - elif not tag_name: - raise BadArgument("Tag names should not be empty, or filled with whitespace.") - - # The tag name is longer than 127 characters. - elif len(tag_name) > 127: - raise BadArgument("Are you insane? That's way too long!") - - # The tag name is ascii but does not contain any letters. - elif not any(character.isalpha() for character in tag_name): - raise BadArgument("Tag names must contain at least one letter.") - - return tag_name - - class TagContentConverter(Converter): """Ensure proposed tag content is not empty and contains at least one non-whitespace character.""" diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index 51b6bc660..f2e2a964c 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -9,7 +9,6 @@ from sentry_sdk import push_scope from bot.api import ResponseCodeError from bot.bot import Bot from bot.constants import Colours, Icons, MODERATION_ROLES -from bot.converters import TagNameConverter from bot.errors import InvalidInfractedUser, LockedResourceError from bot.exts.info import tags from bot.utils.checks import ContextCheckFailure @@ -154,23 +153,16 @@ class ErrorHandler(Cog): await self.on_command_error(ctx, tag_error) return - try: - tag_identifier = tags.TagIdentifier.from_string(ctx.message.content) - if tag_identifier.group is not None: - tag_name = await TagNameConverter.convert(ctx, tag_identifier.name) - tag_name_or_group = await TagNameConverter.convert(ctx, tag_identifier.group) - else: - tag_name = None - tag_name_or_group = await TagNameConverter.convert(ctx, tag_identifier.name) - - except errors.BadArgument: - log.debug( - f"{ctx.author} tried to use an invalid command " - f"and the fallback tag failed validation in TagNameConverter." - ) + tag_identifier = tags.TagIdentifier.from_string(ctx.message.content) + if tag_identifier.group is not None: + tag_name = tag_identifier.name + tag_name_or_group = tag_identifier.group else: - if await ctx.invoke(tags_get_command, tag_name_or_group, tag_name): - return + tag_name = None + tag_name_or_group = tag_identifier.name + + if await ctx.invoke(tags_get_command, tag_name_or_group, tag_name): + return if not any(role.id in MODERATION_ROLES for role in ctx.author.roles): await self.send_command_suggestion(ctx, ctx.invoked_with) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 909831bc7..0fc6e99d0 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -14,7 +14,6 @@ from discord.ext.commands import Cog, Context, group from bot import constants from bot.bot import Bot -from bot.converters import TagNameConverter from bot.pagination import LinePaginator from bot.utils.messages import wait_for_deletion @@ -235,8 +234,8 @@ class Tags(Cog): async def tags_group( self, ctx: Context, - tag_name_or_group: TagNameConverter = None, - tag_name: TagNameConverter = None, + tag_name_or_group: str = None, + tag_name: str = None, ) -> None: """Show all known tags, a single tag, or run a subcommand.""" await self.get_command(ctx, tag_name_or_group=tag_name_or_group, tag_name=tag_name) @@ -356,8 +355,8 @@ class Tags(Cog): @tags_group.command(name="get", aliases=("show", "g")) async def get_command( self, ctx: Context, - tag_name_or_group: TagNameConverter = None, - tag_name: TagNameConverter = None, + tag_name_or_group: str = None, + tag_name: str = None, ) -> bool: """ If a single argument matching a group name is given, list all accessible tags from that group diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index 4a466c22e..eafcbae6c 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -296,30 +296,6 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase): self.assertIsNone(await self.cog.try_get_tag(self.ctx)) self.cog.on_command_error.assert_awaited_once_with(self.ctx, err) - @patch("bot.exts.backend.error_handler.TagNameConverter") - async def test_try_get_tag_convert_success(self, tag_converter): - """Converting tag should successful.""" - self.ctx.message = MagicMock(content="foo") - tag_converter.convert = AsyncMock(return_value="foo") - self.assertIsNone(await self.cog.try_get_tag(self.ctx)) - tag_converter.convert.assert_awaited_once_with(self.ctx, "foo") - self.ctx.invoke.assert_awaited_once() - - self.ctx.reset_mock() - self.ctx.message = MagicMock(content="foo bar") - tag_converter.convert = AsyncMock(return_value="foo bar") - self.assertIsNone(await self.cog.try_get_tag(self.ctx)) - self.assertEqual(tag_converter.convert.call_count, 2) - self.ctx.invoke.assert_awaited_once() - - @patch("bot.exts.backend.error_handler.TagNameConverter") - async def test_try_get_tag_convert_fail(self, tag_converter): - """Converting tag should raise `BadArgument`.""" - self.ctx.reset_mock() - tag_converter.convert = AsyncMock(side_effect=errors.BadArgument()) - self.assertIsNone(await self.cog.try_get_tag(self.ctx)) - self.ctx.invoke.assert_not_awaited() - async def test_try_get_tag_ctx_invoke(self): """Should call `ctx.invoke` with proper args/kwargs.""" test_cases = ( diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py index 4af84dde5..d0d7af1ba 100644 --- a/tests/bot/test_converters.py +++ b/tests/bot/test_converters.py @@ -12,7 +12,6 @@ from bot.converters import ( ISODateTime, PackageName, TagContentConverter, - TagNameConverter, ) @@ -50,34 +49,6 @@ class ConverterTests(unittest.IsolatedAsyncioTestCase): with self.assertRaisesRegex(BadArgument, re.escape(exception_message)): await TagContentConverter.convert(self.context, value) - async def test_tag_name_converter_for_valid(self): - """TagNameConverter should return the correct values for valid tag names.""" - test_values = ( - ('tracebacks', 'tracebacks'), - ('Tracebacks', 'tracebacks'), - (' Tracebacks ', 'tracebacks'), - ) - - for name, expected_conversion in test_values: - with self.subTest(name=name, expected_conversion=expected_conversion): - conversion = await TagNameConverter.convert(self.context, name) - self.assertEqual(conversion, expected_conversion) - - async def test_tag_name_converter_for_invalid(self): - """TagNameConverter should raise the correct exception for invalid tag names.""" - test_values = ( - ('👋', "Don't be ridiculous, you can't use that character!"), - ('', "Tag names should not be empty, or filled with whitespace."), - (' ', "Tag names should not be empty, or filled with whitespace."), - ('42', "Tag names must contain at least one letter."), - ('x' * 128, "Are you insane? That's way too long!"), - ) - - for invalid_name, exception_message in test_values: - with self.subTest(invalid_name=invalid_name, exception_message=exception_message): - with self.assertRaisesRegex(BadArgument, re.escape(exception_message)): - await TagNameConverter.convert(self.context, invalid_name) - async def test_package_name_for_valid(self): """PackageName returns valid package names unchanged.""" test_values = ('foo', 'le_mon', 'num83r') -- cgit v1.2.3 From f3634f9dbcb6c7cc6952f7d9e40879518c4e6eb1 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Sep 2021 01:40:49 +0200 Subject: Return 0 if search string has no a-z characters --- bot/exts/info/tags.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 0fc6e99d0..3d222933a 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -109,6 +109,9 @@ class Tag: def _fuzzy_search(search: str, target: str) -> float: """A simple scoring algorithm based on how many letters are found / total, with order in mind.""" _search = REGEX_NON_ALPHABET.sub("", search.lower()) + if not _search: + return 0 + _targets = iter(REGEX_NON_ALPHABET.split(target.lower())) current = 0 -- cgit v1.2.3 From e07febdbde4815466b161b535f4a5eaf3593f755 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 6 Sep 2021 03:15:01 +0200 Subject: Use a -inf default for comparison to skip containment check --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 3d222933a..bcffb3b80 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -99,7 +99,7 @@ class Tag: def on_cooldown_in(self, channel: discord.TextChannel) -> bool: """Check whether the tag is on cooldown in `channel`.""" - return channel in self._cooldowns and self._cooldowns[channel] > time.time() + return self._cooldowns.get(channel, float("-inf")) > time.time() def set_cooldown_for(self, channel: discord.TextChannel) -> None: """Set the tag to be on cooldown in `channel` for `constants.Cooldowns.tags` seconds.""" -- cgit v1.2.3 From 26a15dcdf6de5b9c0d73760e88d61523e5562690 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 13 Sep 2021 01:48:06 +0200 Subject: Return formatted list instead of paginating directly in tag list methods --- bot/exts/info/tags.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index bcffb3b80..d11782d03 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -131,6 +131,8 @@ def _fuzzy_search(search: str, target: str) -> float: class Tags(Cog): """Fetch tags by name or content.""" + PAGINATOR_DEFAULTS = dict(max_lines=15, empty=False, footer_text=FOOTER_TEXT) + def __init__(self, bot: Bot): self.bot = bot self.tags: dict[TagIdentifier, Tag] = {} @@ -228,9 +230,7 @@ class Tags(Cog): ), ctx, embed, - footer_text=FOOTER_TEXT, - empty=False, - max_lines=15 + **self.PAGINATOR_DEFAULTS, ) @group(name="tags", aliases=("tag", "t"), invoke_without_command=True) @@ -310,8 +310,8 @@ class Tags(Cog): description=suggested_tags_text ) - async def list_all_tags(self, ctx: Context) -> None: - """Send a paginator with all loaded tags accessible by `ctx.author`, groups first, and alphabetically sorted.""" + def list_all_tags(self, user: Member) -> list[str]: + """Return a formatted list of tags that are accessible by `user`; groups first, and alphabetically sorted.""" def tag_sort_key(tag_item: tuple[TagIdentifier, Tag]) -> str: group, name = tag_item[0] if group is None: @@ -338,22 +338,19 @@ class Tags(Cog): else: result_lines.append("\n\N{BULLET}") - if tag.accessible_by(ctx.author): + if tag.accessible_by(user): result_lines.append(f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier.name}") group_accessible = True - embed = Embed(title="Current tags") - await LinePaginator.paginate(result_lines, ctx, embed, max_lines=15, empty=False, footer_text=FOOTER_TEXT) + return result_lines - async def list_tags_in_group(self, ctx: Context, group: str) -> None: - """Send a paginator with all tags under `group`, that are accessible by `ctx.author`.""" - embed = Embed(title=f"Tags under *{group}*") - tag_lines = sorted( + def list_tags_in_group(self, group: str, user: discord.Member) -> list[str]: + """Return a formatted list of tags in `group`, that are accessible by `user`.""" + return sorted( f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}" for identifier, tag in self.tags.items() - if identifier.group == group and tag.accessible_by(ctx.author) + if identifier.group == group and tag.accessible_by(user) ) - await LinePaginator.paginate(tag_lines, ctx, embed, footer_text=FOOTER_TEXT, empty=False, max_lines=15) @tags_group.command(name="get", aliases=("show", "g")) async def get_command( @@ -372,18 +369,19 @@ class Tags(Cog): """ # noqa: D205, D415 if tag_name_or_group is None and tag_name is None: if self.tags: - await self.list_all_tags(ctx) + await LinePaginator.paginate( + self.list_all_tags(ctx.author), ctx, Embed(title="Current tags"), **self.PAGINATOR_DEFAULTS + ) return True else: await ctx.send(embed=Embed(description="**There are no tags!**")) return True elif tag_name is None: - if any( - tag_name_or_group == identifier.group and tag.accessible_by(ctx.author) - for identifier, tag in self.tags.items() - ): - await self.list_tags_in_group(ctx, tag_name_or_group) + if group_tags := self.list_tags_in_group(tag_name_or_group, ctx.author): + await LinePaginator.paginate( + group_tags, ctx, Embed(title=f"Tags under *{tag_name_or_group}*"), **self.PAGINATOR_DEFAULTS + ) return True else: tag_name = tag_name_or_group -- cgit v1.2.3 From c12c0f7240b877cab0978f1e08d9230e5d04f55e Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 13 Sep 2021 01:48:40 +0200 Subject: remove redundant returns on both branches --- bot/exts/info/tags.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index d11782d03..d474d65be 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -372,10 +372,9 @@ class Tags(Cog): await LinePaginator.paginate( self.list_all_tags(ctx.author), ctx, Embed(title="Current tags"), **self.PAGINATOR_DEFAULTS ) - return True else: await ctx.send(embed=Embed(description="**There are no tags!**")) - return True + return True elif tag_name is None: if group_tags := self.list_tags_in_group(tag_name_or_group, ctx.author): -- cgit v1.2.3 From 1cfeaa649e4e8fab7b3508d061cdc1a30aa70c3f Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 13 Sep 2021 01:49:38 +0200 Subject: Rename methods to better reflect their new behaviour --- bot/exts/info/tags.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index d474d65be..56a952f97 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -310,7 +310,7 @@ class Tags(Cog): description=suggested_tags_text ) - def list_all_tags(self, user: Member) -> list[str]: + def accessible_tags(self, user: Member) -> list[str]: """Return a formatted list of tags that are accessible by `user`; groups first, and alphabetically sorted.""" def tag_sort_key(tag_item: tuple[TagIdentifier, Tag]) -> str: group, name = tag_item[0] @@ -344,7 +344,7 @@ class Tags(Cog): return result_lines - def list_tags_in_group(self, group: str, user: discord.Member) -> list[str]: + def accessible_tags_in_group(self, group: str, user: discord.Member) -> list[str]: """Return a formatted list of tags in `group`, that are accessible by `user`.""" return sorted( f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}" @@ -370,14 +370,14 @@ class Tags(Cog): if tag_name_or_group is None and tag_name is None: if self.tags: await LinePaginator.paginate( - self.list_all_tags(ctx.author), ctx, Embed(title="Current tags"), **self.PAGINATOR_DEFAULTS + self.accessible_tags(ctx.author), ctx, Embed(title="Current tags"), **self.PAGINATOR_DEFAULTS ) else: await ctx.send(embed=Embed(description="**There are no tags!**")) return True elif tag_name is None: - if group_tags := self.list_tags_in_group(tag_name_or_group, ctx.author): + if group_tags := self.accessible_tags_in_group(tag_name_or_group, ctx.author): await LinePaginator.paginate( group_tags, ctx, Embed(title=f"Tags under *{tag_name_or_group}*"), **self.PAGINATOR_DEFAULTS ) -- cgit v1.2.3 From 43b18506d0dde27469a136676737826482e07fd7 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 13 Sep 2021 01:50:21 +0200 Subject: Reword all tags embed title --- bot/exts/info/tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 56a952f97..f098d56c9 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -370,7 +370,7 @@ class Tags(Cog): if tag_name_or_group is None and tag_name is None: if self.tags: await LinePaginator.paginate( - self.accessible_tags(ctx.author), ctx, Embed(title="Current tags"), **self.PAGINATOR_DEFAULTS + self.accessible_tags(ctx.author), ctx, Embed(title="Available tags"), **self.PAGINATOR_DEFAULTS ) else: await ctx.send(embed=Embed(description="**There are no tags!**")) -- cgit v1.2.3 From b56c3405c14537c85b1496977d6c3c89cc2debcb Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 13 Sep 2021 02:24:20 +0200 Subject: Handle argument parsing through identifier from_string instead of d.py This lets us skip on the logic of figuring out whether we received a tag name alone, or both a name and a group --- bot/exts/info/tags.py | 37 +++++++++++++------------------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index f098d56c9..06b0d4d5a 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -233,15 +233,10 @@ class Tags(Cog): **self.PAGINATOR_DEFAULTS, ) - @group(name="tags", aliases=("tag", "t"), invoke_without_command=True) - async def tags_group( - self, - ctx: Context, - tag_name_or_group: str = None, - tag_name: str = None, - ) -> None: + @group(name="tags", aliases=("tag", "t"), invoke_without_command=True, usage="[tag_group] [tag_name]") + async def tags_group(self, ctx: Context, *, argument_string: Optional[str]) -> None: """Show all known tags, a single tag, or run a subcommand.""" - await self.get_command(ctx, tag_name_or_group=tag_name_or_group, tag_name=tag_name) + await self.get_command(ctx, argument_string=argument_string) @tags_group.group(name="search", invoke_without_command=True) async def search_tag_content(self, ctx: Context, *, keywords: str) -> None: @@ -352,12 +347,8 @@ class Tags(Cog): if identifier.group == group and tag.accessible_by(user) ) - @tags_group.command(name="get", aliases=("show", "g")) - async def get_command( - self, ctx: Context, - tag_name_or_group: str = None, - tag_name: str = None, - ) -> bool: + @tags_group.command(name="get", aliases=("show", "g"), usage="[tag_group] [tag_name]") + async def get_command(self, ctx: Context, *, argument_string: Optional[str]) -> bool: """ If a single argument matching a group name is given, list all accessible tags from that group Otherwise display the tag if one was found for the given arguments, or try to display suggestions for that name. @@ -367,7 +358,7 @@ class Tags(Cog): Returns True if a message was sent, or if the tag is on cooldown. Returns False if no message was sent. """ # noqa: D205, D415 - if tag_name_or_group is None and tag_name is None: + if not argument_string: if self.tags: await LinePaginator.paginate( self.accessible_tags(ctx.author), ctx, Embed(title="Available tags"), **self.PAGINATOR_DEFAULTS @@ -376,19 +367,17 @@ class Tags(Cog): await ctx.send(embed=Embed(description="**There are no tags!**")) return True - elif tag_name is None: - if group_tags := self.accessible_tags_in_group(tag_name_or_group, ctx.author): + identifier = TagIdentifier.from_string(argument_string) + + if identifier.group is None: + # Try to find accessible tags from a group matching the identifier's name. + if group_tags := self.accessible_tags_in_group(identifier.name, ctx.author): await LinePaginator.paginate( - group_tags, ctx, Embed(title=f"Tags under *{tag_name_or_group}*"), **self.PAGINATOR_DEFAULTS + group_tags, ctx, Embed(title=f"Tags under *{identifier.name}*"), **self.PAGINATOR_DEFAULTS ) return True - else: - tag_name = tag_name_or_group - tag_group = None - else: - tag_group = tag_name_or_group - embed = await self.get_tag_embed(ctx, TagIdentifier(tag_group, tag_name)) + embed = await self.get_tag_embed(ctx, identifier) if embed is None: return False -- cgit v1.2.3 From caa3bba3227e2343a86d98a95e5d647547d154bc Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 13 Sep 2021 02:27:35 +0200 Subject: Use new command interface that accepts direct content --- bot/exts/backend/error_handler.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index 128e72c84..cf0bd3e12 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -10,7 +10,6 @@ from bot.api import ResponseCodeError from bot.bot import Bot from bot.constants import Colours, Icons, MODERATION_ROLES from bot.errors import InvalidInfractedUserError, LockedResourceError -from bot.exts.info import tags from bot.utils.checks import ContextCheckFailure log = logging.getLogger(__name__) @@ -174,15 +173,7 @@ class ErrorHandler(Cog): await self.on_command_error(ctx, tag_error) return - tag_identifier = tags.TagIdentifier.from_string(ctx.message.content) - if tag_identifier.group is not None: - tag_name = tag_identifier.name - tag_name_or_group = tag_identifier.group - else: - tag_name = None - tag_name_or_group = tag_identifier.name - - if await ctx.invoke(tags_get_command, tag_name_or_group, tag_name): + if await ctx.invoke(tags_get_command, argument_string=ctx.message.content): return if not any(role.id in MODERATION_ROLES for role in ctx.author.roles): -- cgit v1.2.3 From 056b144a29b73b93e4eaa884edc86f7e1b09d74e Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 13 Sep 2021 02:47:34 +0200 Subject: Remove try_get_tag ctx args test The arguments are now parsed by the command itself so the test would only check if the mocked message was passed in. The only case where the errors would fail would be a change to the passed args, so it'd only restrict development as the tests would need to be changed anyway --- tests/bot/exts/backend/test_error_handler.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index ce59ee5fa..382194a63 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -360,18 +360,6 @@ class TryGetTagTests(unittest.IsolatedAsyncioTestCase): self.assertIsNone(await self.cog.try_get_tag(self.ctx)) self.cog.on_command_error.assert_awaited_once_with(self.ctx, err) - async def test_try_get_tag_ctx_invoke(self): - """Should call `ctx.invoke` with proper args/kwargs.""" - test_cases = ( - ("foo", ("foo", None)), - ("foo bar", ("foo", "bar")), - ) - for message_content, args in test_cases: - self.ctx.reset_mock() - self.ctx.message = MagicMock(content=message_content) - self.assertIsNone(await self.cog.try_get_tag(self.ctx)) - self.ctx.invoke.assert_awaited_once_with(self.tag.get_command, *args) - async def test_dont_call_suggestion_tag_sent(self): """Should never call command suggestion if tag is already sent.""" self.ctx.message = MagicMock(content="foo") -- cgit v1.2.3 From f6382a3eea81c1dd97b2e12fc81d42f8c77a4ae4 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 21 Sep 2021 21:51:20 -0700 Subject: Time: fix format_with_duration's 2nd arg's default It wasn't passing the current time when `other_timestamp` was None. --- bot/utils/time.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/utils/time.py b/bot/utils/time.py index 005608beb..dfe65369e 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -299,6 +299,9 @@ def format_with_duration( if timestamp is None: return None + if other_timestamp is None: + other_timestamp = arrow.utcnow() + formatted_timestamp = discord_timestamp(timestamp) duration = humanize_delta(timestamp, other_timestamp, max_units=max_units) -- cgit v1.2.3 From 9f32831110ffa1d7c6cb9313c5bb56fa1c9f4d0b Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Tue, 21 Sep 2021 22:02:16 -0700 Subject: TalentPool: fix typo in error message --- bot/exts/recruitment/talentpool/_cog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index bbc135454..0554bf37a 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -259,7 +259,7 @@ class TalentPool(Cog, name="Talentpool"): return if len(reason) > REASON_MAX_CHARS: - await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.") + await ctx.send(f":x: Maximum allowed characters for the reason is {REASON_MAX_CHARS}.") return # Manual request with `raise_for_status` as False because we want the actual response @@ -444,7 +444,7 @@ class TalentPool(Cog, name="Talentpool"): async def edit_end_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None: """Edits the unnominate reason for the nomination with the given `id`.""" if len(reason) > REASON_MAX_CHARS: - await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.") + await ctx.send(f":x: Maximum allowed characters for the end reason is {REASON_MAX_CHARS}.") return try: -- cgit v1.2.3 From b53bf178db221740c36c920a6ca95f53ccdcff83 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Tue, 19 Oct 2021 06:03:38 +0530 Subject: Use discord timestamps for showing worktime --- bot/exts/moderation/modpings.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 6cc46ad26..65372c312 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -12,6 +12,7 @@ from bot.converters import Expiry from bot.log import get_logger from bot.utils import scheduling from bot.utils.scheduling import Scheduler +from bot.utils.time import TimestampFormats, discord_timestamp log = get_logger(__name__) @@ -83,7 +84,7 @@ class ModPings(Cog): start_timestamp, work_time = schedule.split("|") start = datetime.datetime.fromtimestamp(float(start_timestamp)) - mod = self.bot.fetch_user(mod_id) + mod = await self.bot.fetch_user(mod_id) self._modpings_scheduler.schedule_at( start, mod_id, @@ -114,7 +115,7 @@ class ModPings(Cog): log.trace(f"Skipping adding moderator role to mod with ID {mod.id} - found in pings off cache.") else: log.trace(f"Applying moderator role to mod with ID {mod.id}") - await mod.add_roles(self.moderators_role, reason="Moderator schedule time started!") + await mod.add_roles(self.moderators_role, reason="Moderator scheduled time started!") log.trace(f"Sleeping for {work_time} seconds, worktime for mod with ID {mod.id}") await asyncio.sleep(work_time) @@ -216,7 +217,6 @@ class ModPings(Cog): # otherwise the scheduler would schedule it immediately start += datetime.timedelta(days=1) - start, end = start.replace(tzinfo=None), end.replace(tzinfo=None) work_time = (end - start).total_seconds() await self.modpings_schedule.set(ctx.author.id, f"{start.timestamp()}|{work_time}") @@ -232,7 +232,8 @@ class ModPings(Cog): await ctx.send( f"{Emojis.ok_hand} {ctx.author.mention} Scheduled mod pings from " - f"{start: %H:%M} to {end: %H:%M} UTC Timing!" + f"{discord_timestamp(start, TimestampFormats.TIME)} to " + f"{discord_timestamp(end, TimestampFormats.TIME)}!" ) @schedule_modpings.command(name='delete', aliases=('del', 'd')) -- cgit v1.2.3 From 88c407077ba7eba709d4d53455bd46bd9607f8e8 Mon Sep 17 00:00:00 2001 From: Shivansh-007 Date: Mon, 1 Nov 2021 19:22:50 +0530 Subject: Make 'parse' imported function name explicit --- bot/exts/moderation/modpings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 65372c312..47bc5e283 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -2,7 +2,7 @@ import asyncio import datetime from async_rediscache import RedisCache -from dateutil.parser import isoparse, parse +from dateutil.parser import isoparse, parse as dateutil_parse from discord import Embed, Member from discord.ext.commands import Cog, Context, group, has_any_role @@ -200,7 +200,7 @@ class ModPings(Cog): @has_any_role(*MODERATION_ROLES) async def schedule_modpings(self, ctx: Context, start: str, end: str) -> None: """Schedule modpings role to be added at and removed at everyday at UTC time!""" - start, end = parse(start), parse(end) + start, end = dateutil_parse(start), dateutil_parse(end) if end < start: end += datetime.timedelta(days=1) -- cgit v1.2.3 From 94936e499f303deeae785d4b643dbf598ae0a4cc Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 12 Nov 2021 16:52:19 +0000 Subject: Display whether DM was sent to user when listing infraction(s). --- bot/exts/moderation/infraction/_scheduler.py | 4 +++- bot/exts/moderation/infraction/_utils.py | 19 ++++++++++++++++--- bot/exts/moderation/infraction/management.py | 8 ++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 762eb6afa..52dd79791 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -238,7 +238,9 @@ class InfractionScheduler: dm_log_text = "\nDM: **Failed**" # Accordingly update whether the user was successfully notified via DM. - if await _utils.notify_infraction(user, infr_type.replace("_", " ").title(), expiry, user_reason, icon): + if await _utils.notify_infraction( + ctx.bot, user, infraction["id"], infr_type.replace("_", " ").title(), expiry, user_reason, icon + ): dm_result = ":incoming_envelope: " dm_log_text = "\nDM: Sent" diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index c0ef80e3d..433aa0b05 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -5,6 +5,7 @@ import discord from discord.ext.commands import Context from bot.api import ResponseCodeError +from bot.bot import Bot from bot.constants import Colours, Icons from bot.converters import MemberOrUser from bot.errors import InvalidInfractedUserError @@ -78,7 +79,8 @@ async def post_infraction( reason: str, expires_at: datetime = None, hidden: bool = False, - active: bool = True + active: bool = True, + dm_sent: bool = False, ) -> t.Optional[dict]: """Posts an infraction to the API.""" if isinstance(user, (discord.Member, discord.User)) and user.bot: @@ -93,7 +95,8 @@ async def post_infraction( "reason": reason, "type": infr_type, "user": user.id, - "active": active + "active": active, + "dm_sent": dm_sent } if expires_at: payload['expires_at'] = expires_at.isoformat() @@ -156,7 +159,9 @@ async def send_active_infraction_message(ctx: Context, infraction: Infraction) - async def notify_infraction( + bot: Bot, user: MemberOrUser, + infr_id: id, infr_type: str, expires_at: t.Optional[str] = None, reason: t.Optional[str] = None, @@ -186,7 +191,15 @@ async def notify_infraction( embed.title = INFRACTION_TITLE embed.url = RULES_URL - return await send_private_embed(user, embed) + dm_sent = await send_private_embed(user, embed) + if dm_sent: + await bot.api_client.patch( + f"bot/infractions/{infr_id}", + json={"dm_sent": True} + ) + log.debug(f"Update infraction #{infr_id} dm_sent field to true.") + + return dm_sent async def notify_pardon( diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index a833eb227..b77c20434 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -352,6 +352,7 @@ class ModManagement(commands.Cog): user = infraction["user"] expires_at = infraction["expires_at"] created = time.format_infraction(infraction["inserted_at"]) + dm_sent = infraction["dm_sent"] # Format the user string. if user_obj := self.bot.get_user(user["id"]): @@ -377,11 +378,18 @@ class ModManagement(commands.Cog): date_to = dateutil.parser.isoparse(expires_at) duration = humanize_delta(relativedelta(date_to, date_from)) + # Format `dm_sent` + if dm_sent is None: + dm_sent_text = "N/A" + else: + dm_sent_text = "Yes" if dm_sent else "No" + lines = textwrap.dedent(f""" {"**===============**" if active else "==============="} Status: {"__**Active**__" if active else "Inactive"} User: {user_str} Type: **{infraction["type"]}** + DM Sent: {dm_sent_text} Shadow: {infraction["hidden"]} Created: {created} Expires: {remaining} -- cgit v1.2.3 From 1f327a54640a781026dc223597f8e2a306751460 Mon Sep 17 00:00:00 2001 From: Izan Date: Tue, 16 Nov 2021 09:40:31 +0000 Subject: Fix tests --- tests/bot/exts/moderation/infraction/test_utils.py | 27 +++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 72eebb254..999dbd1c6 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -19,6 +19,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.member = MockMember(id=1234) self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) + self.maxDiff = None async def test_post_user(self): """Should POST a new user and return the response if successful or otherwise send an error message.""" @@ -132,7 +133,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): """ test_cases = [ { - "args": (self.user, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), + "args": (self.bot, self.user, 0, "ban", "2020-02-26 09:20 (23 hours and 59 minutes)"), "expected_output": Embed( title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( @@ -150,7 +151,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "send_result": True }, { - "args": (self.user, "warning", None, "Test reason."), + "args": (self.bot, self.user, 0, "warning", None, "Test reason."), "expected_output": Embed( title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( @@ -170,7 +171,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): # Note that this test case asserts that the DM that *would* get sent to the user is formatted # correctly, even though that message is deliberately never sent. { - "args": (self.user, "note", None, None, Icons.defcon_denied), + "args": (self.bot, self.user, 0, "note", None, None, Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( @@ -188,7 +189,15 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "send_result": False }, { - "args": (self.user, "mute", "2020-02-26 09:20 (23 hours and 59 minutes)", "Test", Icons.defcon_denied), + "args": ( + self.bot, + self.user, + 0, + "mute", + "2020-02-26 09:20 (23 hours and 59 minutes)", + "Test", + Icons.defcon_denied + ), "expected_output": Embed( title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( @@ -206,7 +215,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): "send_result": False }, { - "args": (self.user, "mute", None, "foo bar" * 4000, Icons.defcon_denied), + "args": (self.bot, self.user, 0, "mute", None, "foo bar" * 4000, Icons.defcon_denied), "expected_output": Embed( title=utils.INFRACTION_TITLE, description=utils.INFRACTION_DESCRIPTION_TEMPLATE.format( @@ -238,7 +247,7 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(embed.to_dict(), case["expected_output"].to_dict()) - send_private_embed_mock.assert_awaited_once_with(case["args"][0], embed) + send_private_embed_mock.assert_awaited_once_with(case["args"][1], embed) @patch("bot.exts.moderation.infraction._utils.send_private_embed") async def test_notify_pardon(self, send_private_embed_mock): @@ -313,7 +322,8 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "type": "ban", "user": self.member.id, "active": False, - "expires_at": now.isoformat() + "expires_at": now.isoformat(), + "dm_sent": False } self.ctx.bot.api_client.post.return_value = "foo" @@ -350,7 +360,8 @@ class TestPostInfraction(unittest.IsolatedAsyncioTestCase): "reason": "Test reason", "type": "mute", "user": self.user.id, - "active": True + "active": True, + "dm_sent": False } self.bot.api_client.post.side_effect = [ResponseCodeError(MagicMock(status=400), {"user": "foo"}), "foo"] -- cgit v1.2.3 From a6be95385edc1caccd84dc83a8d11ece86847c8b Mon Sep 17 00:00:00 2001 From: Izan Date: Thu, 25 Nov 2021 19:55:33 +0000 Subject: Remove debug `maxDiff` assignment. --- tests/bot/exts/moderation/infraction/test_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 999dbd1c6..350274ecd 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -19,7 +19,6 @@ class ModerationUtilsTests(unittest.IsolatedAsyncioTestCase): self.member = MockMember(id=1234) self.user = MockUser(id=1234) self.ctx = MockContext(bot=self.bot, author=self.member) - self.maxDiff = None async def test_post_user(self): """Should POST a new user and return the response if successful or otherwise send an error message.""" -- cgit v1.2.3 From 7e8ecb4f2acc7e1e88d4c053091926c07965293d Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Sat, 27 Nov 2021 16:18:26 -0700 Subject: Add missing space in text shortening placeholder --- bot/exts/info/site.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/site.py b/bot/exts/info/site.py index bcb04c909..c622441bd 100644 --- a/bot/exts/info/site.py +++ b/bot/exts/info/site.py @@ -126,7 +126,7 @@ class Site(Cog): # Remove duplicates and sort the rule indices rules = sorted(set(rules)) invalid = shorten(', '.join(str(index) for index in rules if index - < 1 or index > len(full_rules)), 50, placeholder='...') + < 1 or index > len(full_rules)), 50, placeholder=' ...') if invalid: await ctx.send(f":x: Invalid rule indices: {invalid}") -- cgit v1.2.3 From d870f28027c708fef3f0e1cc035196e727485cce Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Sat, 27 Nov 2021 16:33:42 -0700 Subject: Refactor long line Doing this similar to how the docs command works for shortening --- bot/exts/info/site.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/site.py b/bot/exts/info/site.py index c622441bd..f6499ecce 100644 --- a/bot/exts/info/site.py +++ b/bot/exts/info/site.py @@ -125,11 +125,11 @@ class Site(Cog): # Remove duplicates and sort the rule indices rules = sorted(set(rules)) - invalid = shorten(', '.join(str(index) for index in rules if index - < 1 or index > len(full_rules)), 50, placeholder=' ...') + + invalid = ', '.join(str(index) for index in rules if index < 1 or index > len(full_rules)) if invalid: - await ctx.send(f":x: Invalid rule indices: {invalid}") + await ctx.send(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=' ...')) return for rule in rules: -- cgit v1.2.3 From 8680df24222dc4b4828cd2df78f8f2b44d0b1e27 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 12 Oct 2021 20:30:40 +0100 Subject: Move handle_role_change to a util file --- bot/exts/help_channels/_cog.py | 30 +++++++++--------------------- bot/utils/members.py | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 0c411df04..60209ba6e 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -66,6 +66,9 @@ class HelpChannels(commands.Cog): self.bot = bot self.scheduler = scheduling.Scheduler(self.__class__.__name__) + self.guild: discord.Guild = None + self.cooldown_role: discord.Role = None + # Categories self.available_category: discord.CategoryChannel = None self.in_use_category: discord.CategoryChannel = None @@ -95,24 +98,6 @@ class HelpChannels(commands.Cog): self.scheduler.cancel_all() - async def _handle_role_change(self, member: discord.Member, coro: t.Callable[..., t.Coroutine]) -> None: - """ - Change `member`'s cooldown role via awaiting `coro` and handle errors. - - `coro` is intended to be `discord.Member.add_roles` or `discord.Member.remove_roles`. - """ - try: - await coro(self.bot.get_guild(constants.Guild.id).get_role(constants.Roles.help_cooldown)) - except discord.NotFound: - log.debug(f"Failed to change role for {member} ({member.id}): member not found") - except discord.Forbidden: - log.debug( - f"Forbidden to change role for {member} ({member.id}); " - f"possibly due to role hierarchy" - ) - except discord.HTTPException as e: - log.error(f"Failed to change role for {member} ({member.id}): {e.status} {e.code}") - @lock.lock_arg(NAMESPACE, "message", attrgetter("channel.id")) @lock.lock_arg(NAMESPACE, "message", attrgetter("author.id")) @lock.lock_arg(f"{NAMESPACE}.unclaim", "message", attrgetter("author.id"), wait=True) @@ -130,7 +115,7 @@ class HelpChannels(commands.Cog): if not isinstance(message.author, discord.Member): log.debug(f"{message.author} ({message.author.id}) isn't a member. Not giving cooldown role or sending DM.") else: - await self._handle_role_change(message.author, message.author.add_roles) + await members.handle_role_change(message.author, message.author.add_roles, self.cooldown_role) try: await _message.dm_on_open(message) @@ -302,6 +287,9 @@ class HelpChannels(commands.Cog): await self.bot.wait_until_guild_available() log.trace("Initialising the cog.") + self.guild = self.bot.get_guild(constants.Guild.id) + self.cooldown_role = self.guild.get_role(constants.Roles.help_cooldown) + await self.init_categories() self.channel_queue = self.create_channel_queue() @@ -445,11 +433,11 @@ class HelpChannels(commands.Cog): await _caches.claimants.delete(channel.id) await _caches.session_participants.delete(channel.id) - claimant = await members.get_or_fetch_member(self.bot.get_guild(constants.Guild.id), claimant_id) + claimant = await members.get_or_fetch_member(self.guild, claimant_id) if claimant is None: log.info(f"{claimant_id} left the guild during their help session; the cooldown role won't be removed") else: - await self._handle_role_change(claimant, claimant.remove_roles) + await members.handle_role_change(claimant, claimant.remove_roles, self.cooldown_role) await _message.unpin(channel) await _stats.report_complete_session(channel.id, closed_on) diff --git a/bot/utils/members.py b/bot/utils/members.py index 77ddf1696..693286045 100644 --- a/bot/utils/members.py +++ b/bot/utils/members.py @@ -23,3 +23,26 @@ async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> t.Optiona return None log.trace("%s fetched from API.", member) return member + + +async def handle_role_change( + member: discord.Member, + coro: t.Callable[..., t.Coroutine], + role: discord.Role +) -> None: + """ + Change `member`'s cooldown role via awaiting `coro` and handle errors. + + `coro` is intended to be `discord.Member.add_roles` or `discord.Member.remove_roles`. + """ + try: + await coro(role) + except discord.NotFound: + log.debug(f"Failed to change role for {member} ({member.id}): member not found") + except discord.Forbidden: + log.debug( + f"Forbidden to change role for {member} ({member.id}); " + f"possibly due to role hierarchy" + ) + except discord.HTTPException as e: + log.error(f"Failed to change role for {member} ({member.id}): {e.status} {e.code}") -- cgit v1.2.3 From 0465db98be1d739eea69e8a2f7cf4b939c65c96d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 12 Oct 2021 20:31:25 +0100 Subject: Remove the subscribe command from the verification cog --- bot/exts/moderation/verification.py | 71 +++---------------------------------- 1 file changed, 4 insertions(+), 67 deletions(-) diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index ed5571d2a..37338d19c 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -5,9 +5,7 @@ from discord.ext.commands import Cog, Context, command, has_any_role from bot import constants from bot.bot import Bot -from bot.decorators import in_whitelist from bot.log import get_logger -from bot.utils.checks import InWhitelistCheckFailure log = get_logger(__name__) @@ -29,11 +27,11 @@ You can find a copy of our rules for reference at -from time to time, you can send `!subscribe` to <#{constants.Channels.bot_commands}> at any time \ +from time to time, you can send `{constants.Bot.prefix}subscribe` to <#{constants.Channels.bot_commands}> at any time \ to assign yourself the **Announcements** role. We'll mention this role every time we make an announcement. -If you'd like to unsubscribe from the announcement notifications, simply send `!unsubscribe` to \ -<#{constants.Channels.bot_commands}>. +If you'd like to unsubscribe from the announcement notifications, simply send `{constants.Bot.prefix}subscribe` to \ +<#{constants.Channels.bot_commands}> and click the role again!. To introduce you to our community, we've made the following video: https://youtu.be/ZH26PuX3re0 @@ -61,11 +59,9 @@ async def safe_dm(coro: t.Coroutine) -> None: class Verification(Cog): """ - User verification and role management. + User verification. Statistics are collected in the 'verification.' namespace. - - Additionally, this cog offers the !subscribe and !unsubscribe commands, """ def __init__(self, bot: Bot) -> None: @@ -107,68 +103,9 @@ class Verification(Cog): except discord.HTTPException: log.exception("DM dispatch failed on unexpected error code") - # endregion - # region: subscribe commands - - @command(name='subscribe') - @in_whitelist(channels=(constants.Channels.bot_commands,)) - async def subscribe_command(self, ctx: Context, *_) -> None: # We don't actually care about the args - """Subscribe to announcement notifications by assigning yourself the role.""" - has_role = False - - for role in ctx.author.roles: - if role.id == constants.Roles.announcements: - has_role = True - break - - if has_role: - await ctx.send(f"{ctx.author.mention} You're already subscribed!") - return - - log.debug(f"{ctx.author} called !subscribe. Assigning the 'Announcements' role.") - await ctx.author.add_roles(discord.Object(constants.Roles.announcements), reason="Subscribed to announcements") - - log.trace(f"Deleting the message posted by {ctx.author}.") - - await ctx.send( - f"{ctx.author.mention} Subscribed to <#{constants.Channels.announcements}> notifications.", - ) - - @command(name='unsubscribe') - @in_whitelist(channels=(constants.Channels.bot_commands,)) - async def unsubscribe_command(self, ctx: Context, *_) -> None: # We don't actually care about the args - """Unsubscribe from announcement notifications by removing the role from yourself.""" - has_role = False - - for role in ctx.author.roles: - if role.id == constants.Roles.announcements: - has_role = True - break - - if not has_role: - await ctx.send(f"{ctx.author.mention} You're already unsubscribed!") - return - - log.debug(f"{ctx.author} called !unsubscribe. Removing the 'Announcements' role.") - await ctx.author.remove_roles( - discord.Object(constants.Roles.announcements), reason="Unsubscribed from announcements" - ) - - log.trace(f"Deleting the message posted by {ctx.author}.") - - await ctx.send( - f"{ctx.author.mention} Unsubscribed from <#{constants.Channels.announcements}> notifications." - ) - # endregion # region: miscellaneous - # This cannot be static (must have a __func__ attribute). - async def cog_command_error(self, ctx: Context, error: Exception) -> None: - """Check for & ignore any InWhitelistCheckFailure.""" - if isinstance(error, InWhitelistCheckFailure): - error.handled = True - @command(name='verify') @has_any_role(*constants.MODERATION_ROLES) async def perform_manual_verification(self, ctx: Context, user: discord.Member) -> None: -- cgit v1.2.3 From 5df26bafa58ed036333eb1d4fa7438cf93c4b7c9 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 12 Oct 2021 20:38:13 +0100 Subject: Add self assignable roles to config --- bot/constants.py | 5 +++++ config-default.yml | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index 36b917734..36a92da1f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -484,7 +484,12 @@ class Roles(metaclass=YAMLGetter): section = "guild" subsection = "roles" + # Self-assignable roles, see the Subscribe cog + advent_of_code: int announcements: int + lovefest: int + pyweek_announcements: int + contributors: int help_cooldown: int muted: int diff --git a/config-default.yml b/config-default.yml index 7400cf200..0d3ddc005 100644 --- a/config-default.yml +++ b/config-default.yml @@ -264,7 +264,12 @@ guild: - *BLACK_FORMATTER roles: + # Self-assignable roles, see the Subscribe cog + advent_of_code: 518565788744024082 announcements: 463658397560995840 + lovefest: 542431903886606399 + pyweek_announcements: 897568414044938310 + contributors: 295488872404484098 help_cooldown: 699189276025421825 muted: &MUTED_ROLE 277914926603829249 -- cgit v1.2.3 From 4f7010912ccc75ea1415bc5e1e10fbce17c43b69 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 12 Oct 2021 20:38:54 +0100 Subject: Add an interactive subscribe command This command gives the users a set of buttons to click to add or remove pre-determined announcement roles. Adding or removing a role updates the button state to reflect the change and what would happen if the user clicks the button again. --- bot/exts/info/subscribe.py | 139 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 bot/exts/info/subscribe.py diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py new file mode 100644 index 000000000..edf8e8f9e --- /dev/null +++ b/bot/exts/info/subscribe.py @@ -0,0 +1,139 @@ +import logging + +import arrow +import discord +from discord.ext import commands +from discord.interactions import Interaction + +from bot import constants +from bot.bot import Bot +from bot.decorators import in_whitelist +from bot.utils import checks, members, scheduling + +# Tuple of tuples, where each inner tuple is a role id and a month number. +# The month number signifies what month the role should be assignable, +# use None for the month number if it should always be active. +ASSIGNABLE_ROLES = ( + (constants.Roles.announcements, None), + (constants.Roles.pyweek_announcements, None), + (constants.Roles.lovefest, 2), + (constants.Roles.advent_of_code, 12), +) +ITEMS_PER_ROW = 3 + +log = logging.getLogger(__name__) + + +class RoleButtonView(discord.ui.View): + """A list of SingleRoleButtons to show to the member.""" + + def __init__(self, member: discord.Member): + super().__init__() + self.interaction_owner = member + + async def interaction_check(self, interaction: Interaction) -> bool: + """Ensure that the user clicking the button is the member who invoked the command.""" + if interaction.user != self.interaction_owner: + await interaction.response.send_message( + ":x: This is not your command to react to!", + ephemeral=True + ) + return False + return True + + +class SingleRoleButton(discord.ui.Button): + """A button that adds or removes a role from the member depending on it's current state.""" + + ADD_STYLE = discord.ButtonStyle.success + REMOVE_STYLE = discord.ButtonStyle.secondary + LABEL_FORMAT = "{action} role {role_name}" + CUSTOM_ID_FORMAT = "subscribe-{role_id}" + + def __init__(self, role: discord.Role, assigned: bool, row: int): + super().__init__( + style=self.REMOVE_STYLE if assigned else self.ADD_STYLE, + label=self.LABEL_FORMAT.format(action="Remove" if assigned else "Add", role_name=role.name), + custom_id=self.CUSTOM_ID_FORMAT.format(role_id=role.id), + row=row, + ) + self.role = role + self.assigned = assigned + + async def callback(self, interaction: Interaction) -> None: + """Update the member's role and change button text to reflect current text.""" + await members.handle_role_change( + interaction.user, + interaction.user.remove_roles if self.assigned else interaction.user.add_roles, + self.role, + ) + + self.assigned = not self.assigned + await self.update_view(interaction) + await interaction.response.send_message( + self.LABEL_FORMAT.format(action="Added" if self.assigned else "Removed", role_name=self.role.name), + ephemeral=True, + ) + + async def update_view(self, interaction: Interaction) -> None: + """Updates the original interaction message with a new view object with the updated buttons.""" + self.style = self.REMOVE_STYLE if self.assigned else self.ADD_STYLE + self.label = self.LABEL_FORMAT.format(action="Remove" if self.assigned else "Add", role_name=self.role.name) + try: + await interaction.message.edit(view=self.view) + except discord.NotFound: + log.debug("Subscribe message for %s removed before buttons could be updated", interaction.user) + self.view.stop() + + +class Subscribe(commands.Cog): + """Cog to allow user to self-assign & remove the roles present in ASSIGNABLE_ROLES.""" + + def __init__(self, bot: Bot): + self.bot = bot + self.init_task = scheduling.create_task(self.init_cog(), event_loop=self.bot.loop) + self.assignable_roles: list[discord.Role] = [] + self.guild: discord.Guild = None + + async def init_cog(self) -> None: + """Initialise the cog by resolving the role IDs in ASSIGNABLE_ROLES to role names.""" + await self.bot.wait_until_guild_available() + + current_month = arrow.utcnow().month + self.guild = self.bot.get_guild(constants.Guild.id) + + for role_id, month_available in ASSIGNABLE_ROLES: + if month_available is not None and month_available != current_month: + continue + role = self.guild.get_role(role_id) + if role is None: + log.warning("Could not resolve %d to a role in the guild, skipping.", role_id) + continue + self.assignable_roles.append(role) + + @commands.command(name="subscribe") + @in_whitelist(channels=(constants.Channels.bot_commands,)) + async def subscribe_command(self, ctx: commands.Context, *_) -> None: # We don't actually care about the args + """Display the member's current state for each role, and allow them to add/remove the roles.""" + await self.init_task + + button_view = RoleButtonView(ctx.author) + for index, role in enumerate(self.assignable_roles): + row = index // ITEMS_PER_ROW + button_view.add_item(SingleRoleButton(role, role in ctx.author.roles, row)) + + await ctx.send("Click the buttons below to add or remove your roles!", view=button_view) + + # This cannot be static (must have a __func__ attribute). + async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None: + """Check for & ignore any InWhitelistCheckFailure.""" + if isinstance(error, checks.InWhitelistCheckFailure): + error.handled = True + + +def setup(bot: Bot) -> None: + """Load the Subscribe cog.""" + if len(ASSIGNABLE_ROLES) > ITEMS_PER_ROW*5: # Discord limits views to 5 rows of buttons. + log.error("Too many roles for 5 rows, not loading the Subscribe cog.") + else: + bot.add_cog(Subscribe(bot)) -- cgit v1.2.3 From 4c982870749f3545c971c20eb19a3c5eafe67668 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 13 Oct 2021 09:34:07 +0100 Subject: Ensure the user interacting is still in guild before changing roles --- bot/exts/info/subscribe.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index edf8e8f9e..bf3120a3a 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -62,6 +62,10 @@ class SingleRoleButton(discord.ui.Button): async def callback(self, interaction: Interaction) -> None: """Update the member's role and change button text to reflect current text.""" + if isinstance(interaction.user, discord.User): + log.trace("User %s is not a member", interaction.user) + await interaction.message.delete() + return await members.handle_role_change( interaction.user, interaction.user.remove_roles if self.assigned else interaction.user.add_roles, -- cgit v1.2.3 From 1d7765c5629efaccdd4741b8fd6640f7fd6dab09 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 13 Oct 2021 17:49:51 +0100 Subject: Add 10s member cooldown to subscribe command --- bot/exts/info/subscribe.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index bf3120a3a..121fa3685 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -115,6 +115,7 @@ class Subscribe(commands.Cog): continue self.assignable_roles.append(role) + @commands.cooldown(1, 10, commands.BucketType.member) @commands.command(name="subscribe") @in_whitelist(channels=(constants.Channels.bot_commands,)) async def subscribe_command(self, ctx: commands.Context, *_) -> None: # We don't actually care about the args -- cgit v1.2.3 From 9a3be9ee23df63792d942950ccb378750ddc3ac7 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 13 Oct 2021 17:51:28 +0100 Subject: Stop listening for events when message is deleted --- bot/exts/info/subscribe.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index 121fa3685..5dad013d1 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -65,7 +65,9 @@ class SingleRoleButton(discord.ui.Button): if isinstance(interaction.user, discord.User): log.trace("User %s is not a member", interaction.user) await interaction.message.delete() + self.view.stop() return + await members.handle_role_change( interaction.user, interaction.user.remove_roles if self.assigned else interaction.user.add_roles, -- cgit v1.2.3 From b748d1310b2c731ac46e0bbc864d4d28a5439b37 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 17 Oct 2021 09:34:18 +0100 Subject: Use new get_logger helper util --- bot/exts/info/subscribe.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index 5dad013d1..a2a0de976 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -1,5 +1,3 @@ -import logging - import arrow import discord from discord.ext import commands @@ -8,6 +6,7 @@ from discord.interactions import Interaction from bot import constants from bot.bot import Bot from bot.decorators import in_whitelist +from bot.log import get_logger from bot.utils import checks, members, scheduling # Tuple of tuples, where each inner tuple is a role id and a month number. @@ -21,7 +20,7 @@ ASSIGNABLE_ROLES = ( ) ITEMS_PER_ROW = 3 -log = logging.getLogger(__name__) +log = get_logger(__name__) class RoleButtonView(discord.ui.View): -- cgit v1.2.3 From 8b109837e0cba62574ef4269e512d3fe23f6b37e Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 30 Nov 2021 10:30:31 +0000 Subject: Delete the subscribe message after 5 minutes --- bot/exts/info/subscribe.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index a2a0de976..17bb24dca 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -19,6 +19,7 @@ ASSIGNABLE_ROLES = ( (constants.Roles.advent_of_code, 12), ) ITEMS_PER_ROW = 3 +DELETE_MESSAGE_AFTER = 300 # Seconds log = get_logger(__name__) @@ -128,7 +129,11 @@ class Subscribe(commands.Cog): row = index // ITEMS_PER_ROW button_view.add_item(SingleRoleButton(role, role in ctx.author.roles, row)) - await ctx.send("Click the buttons below to add or remove your roles!", view=button_view) + await ctx.send( + "Click the buttons below to add or remove your roles!", + view=button_view, + delete_after=DELETE_MESSAGE_AFTER, + ) # This cannot be static (must have a __func__ attribute). async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None: -- cgit v1.2.3 From 7f22abfd3ec443cf0925f2c6e609be681c723799 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 30 Nov 2021 11:31:30 +0000 Subject: Allow roles to be assignable over multiple months This includes a refactor to use a dataclass for clearer implementation. Along with that, this changes the roles so that they're always available, but un-assignable roles are in red and give a different error. --- bot/exts/info/subscribe.py | 95 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 23 deletions(-) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index 17bb24dca..9b96e7ab2 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -1,3 +1,7 @@ +import calendar +import typing as t +from dataclasses import dataclass + import arrow import discord from discord.ext import commands @@ -9,15 +13,44 @@ from bot.decorators import in_whitelist from bot.log import get_logger from bot.utils import checks, members, scheduling -# Tuple of tuples, where each inner tuple is a role id and a month number. -# The month number signifies what month the role should be assignable, -# use None for the month number if it should always be active. + +@dataclass(frozen=True) +class AssignableRole: + """ + A role that can be assigned to a user. + + months_available is a tuple that signifies what months the role should be + self-assignable, using None for when it should always be available. + """ + + role_id: int + months_available: t.Optional[tuple[int]] + name: t.Optional[str] = None # This gets populated within Subscribe.init_cog() + + def is_currently_available(self) -> bool: + """Check if the role is available for the current month.""" + if self.months_available is None: + return True + return arrow.utcnow().month in self.months_available + + def get_readable_available_months(self) -> str: + """Get a readable string of the months the role is available.""" + if self.months_available is None: + return f"{self.name} is always available." + + # Join the months together with comma separators, but use "and" for the final seperator. + month_names = [calendar.month_name[month] for month in self.months_available] + available_months_str = ", ".join(month_names[:-1]) + f" and {month_names[-1]}" + return f"{self.name} can only be assigned during {available_months_str}." + + ASSIGNABLE_ROLES = ( - (constants.Roles.announcements, None), - (constants.Roles.pyweek_announcements, None), - (constants.Roles.lovefest, 2), - (constants.Roles.advent_of_code, 12), + AssignableRole(constants.Roles.announcements, None), + AssignableRole(constants.Roles.pyweek_announcements, None), + AssignableRole(constants.Roles.lovefest, (1, 2)), + AssignableRole(constants.Roles.advent_of_code, (11, 12)), ) + ITEMS_PER_ROW = 3 DELETE_MESSAGE_AFTER = 300 # Seconds @@ -47,14 +80,22 @@ class SingleRoleButton(discord.ui.Button): ADD_STYLE = discord.ButtonStyle.success REMOVE_STYLE = discord.ButtonStyle.secondary - LABEL_FORMAT = "{action} role {role_name}" + UNAVAILABLE_STYLE = discord.ButtonStyle.red + LABEL_FORMAT = "{action} role {role_name}." CUSTOM_ID_FORMAT = "subscribe-{role_id}" - def __init__(self, role: discord.Role, assigned: bool, row: int): + def __init__(self, role: AssignableRole, assigned: bool, row: int): + if role.is_currently_available(): + style = self.REMOVE_STYLE if assigned else self.ADD_STYLE + label = self.LABEL_FORMAT.format(action="Remove" if assigned else "Add", role_name=role.name) + else: + style = self.UNAVAILABLE_STYLE + label = role.name + super().__init__( - style=self.REMOVE_STYLE if assigned else self.ADD_STYLE, - label=self.LABEL_FORMAT.format(action="Remove" if assigned else "Add", role_name=role.name), - custom_id=self.CUSTOM_ID_FORMAT.format(role_id=role.id), + style=style, + label=label, + custom_id=self.CUSTOM_ID_FORMAT.format(role_id=role.role_id), row=row, ) self.role = role @@ -68,10 +109,14 @@ class SingleRoleButton(discord.ui.Button): self.view.stop() return + if not self.role.is_currently_available(): + await interaction.response.send_message(self.role.get_readable_available_months(), ephemeral=True) + return + await members.handle_role_change( interaction.user, interaction.user.remove_roles if self.assigned else interaction.user.add_roles, - self.role, + discord.Object(self.role.role_id), ) self.assigned = not self.assigned @@ -98,24 +143,27 @@ class Subscribe(commands.Cog): def __init__(self, bot: Bot): self.bot = bot self.init_task = scheduling.create_task(self.init_cog(), event_loop=self.bot.loop) - self.assignable_roles: list[discord.Role] = [] + self.assignable_roles: list[AssignableRole] = [] self.guild: discord.Guild = None async def init_cog(self) -> None: """Initialise the cog by resolving the role IDs in ASSIGNABLE_ROLES to role names.""" await self.bot.wait_until_guild_available() - current_month = arrow.utcnow().month self.guild = self.bot.get_guild(constants.Guild.id) - for role_id, month_available in ASSIGNABLE_ROLES: - if month_available is not None and month_available != current_month: - continue - role = self.guild.get_role(role_id) - if role is None: - log.warning("Could not resolve %d to a role in the guild, skipping.", role_id) + for role in ASSIGNABLE_ROLES: + discord_role = self.guild.get_role(role.role_id) + if discord_role is None: + log.warning("Could not resolve %d to a role in the guild, skipping.", role.role_id) continue - self.assignable_roles.append(role) + self.assignable_roles.append( + AssignableRole( + role_id=role.role_id, + months_available=role.months_available, + name=discord_role.name, + ) + ) @commands.cooldown(1, 10, commands.BucketType.member) @commands.command(name="subscribe") @@ -125,9 +173,10 @@ class Subscribe(commands.Cog): await self.init_task button_view = RoleButtonView(ctx.author) + author_roles = [role.id for role in ctx.author.roles] for index, role in enumerate(self.assignable_roles): row = index // ITEMS_PER_ROW - button_view.add_item(SingleRoleButton(role, role in ctx.author.roles, row)) + button_view.add_item(SingleRoleButton(role, role.role_id in author_roles, row)) await ctx.send( "Click the buttons below to add or remove your roles!", -- cgit v1.2.3 From 19eef3ed7135572ad52bbf145278efcdd142b0c0 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 30 Nov 2021 11:36:15 +0000 Subject: Sort unavailable self-assignable roles to the end of the list --- bot/exts/info/subscribe.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index 9b96e7ab2..d24e8716e 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -1,4 +1,5 @@ import calendar +import operator import typing as t from dataclasses import dataclass @@ -164,6 +165,8 @@ class Subscribe(commands.Cog): name=discord_role.name, ) ) + # Sort unavailable roles to the end of the list + self.assignable_roles.sort(key=operator.methodcaller("is_currently_available"), reverse=True) @commands.cooldown(1, 10, commands.BucketType.member) @commands.command(name="subscribe") -- cgit v1.2.3 From 005af3bc34310d9374bfd1deeaf37da080c7fee1 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 30 Nov 2021 14:06:28 +0000 Subject: Swap remove and unavailable colours for subscribe command --- bot/exts/info/subscribe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index d24e8716e..d097e6290 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -80,8 +80,8 @@ class SingleRoleButton(discord.ui.Button): """A button that adds or removes a role from the member depending on it's current state.""" ADD_STYLE = discord.ButtonStyle.success - REMOVE_STYLE = discord.ButtonStyle.secondary - UNAVAILABLE_STYLE = discord.ButtonStyle.red + REMOVE_STYLE = discord.ButtonStyle.red + UNAVAILABLE_STYLE = discord.ButtonStyle.secondary LABEL_FORMAT = "{action} role {role_name}." CUSTOM_ID_FORMAT = "subscribe-{role_id}" -- cgit v1.2.3 From 57c1b8e6bbadf8139597e7105d6681a13781b69a Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 30 Nov 2021 14:06:53 +0000 Subject: Add lock emoji to highlight unavailable self-assignable roles --- bot/exts/info/subscribe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index d097e6290..16379d2b2 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -91,7 +91,7 @@ class SingleRoleButton(discord.ui.Button): label = self.LABEL_FORMAT.format(action="Remove" if assigned else "Add", role_name=role.name) else: style = self.UNAVAILABLE_STYLE - label = role.name + label = f"🔒 {role.name}" super().__init__( style=style, -- cgit v1.2.3 From aecb093afc95d28b85a63714cde9ae33e9068ae8 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 30 Nov 2021 14:07:16 +0000 Subject: Subscribe command replies to invocation to keep context --- bot/exts/info/subscribe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index 16379d2b2..2e6101d27 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -181,7 +181,7 @@ class Subscribe(commands.Cog): row = index // ITEMS_PER_ROW button_view.add_item(SingleRoleButton(role, role.role_id in author_roles, row)) - await ctx.send( + await ctx.reply( "Click the buttons below to add or remove your roles!", view=button_view, delete_after=DELETE_MESSAGE_AFTER, -- cgit v1.2.3 From edb18d5f5be3d1dfcfdcfa72bcbf0915e321b895 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 1 Nov 2021 17:16:05 +0000 Subject: Add thread archive time enum to constants --- bot/constants.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/bot/constants.py b/bot/constants.py index 36b917734..93da6a906 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -683,10 +683,22 @@ class VideoPermission(metaclass=YAMLGetter): default_permission_duration: int +class ThreadArchiveTimes(Enum): + HOUR = 60 + DAY = 1440 + THREE_DAY = 4230 + WEEK = 10080 + + # Debug mode DEBUG_MODE: bool = _CONFIG_YAML["debug"] == "true" FILE_LOGS: bool = _CONFIG_YAML["file_logs"].lower() == "true" +if DEBUG_MODE: + DEFAULT_THREAD_ARCHIVE_TIME = ThreadArchiveTimes.HOUR.value +else: + DEFAULT_THREAD_ARCHIVE_TIME = ThreadArchiveTimes.WEEK.value + # Paths BOT_DIR = os.path.dirname(__file__) PROJECT_ROOT = os.path.abspath(os.path.join(BOT_DIR, os.pardir)) -- cgit v1.2.3 From 292a500d9ebb51b8efc023baf39b76d98d05cae0 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 1 Nov 2021 17:19:36 +0000 Subject: Refactor make_review to return nominee too --- bot/exts/recruitment/talentpool/_cog.py | 2 +- bot/exts/recruitment/talentpool/_review.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 2fafaec97..699d60f42 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -483,7 +483,7 @@ class TalentPool(Cog, name="Talentpool"): @has_any_role(*MODERATION_ROLES) async def get_review(self, ctx: Context, user_id: int) -> None: """Get the user's review as a markdown file.""" - review = (await self.reviewer.make_review(user_id))[0] + review, _, _ = await self.reviewer.make_review(user_id) if review: file = discord.File(StringIO(review), f"{user_id}_review.md") await ctx.send(file=file) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index d880c524c..6b5fae3b1 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -78,14 +78,14 @@ class Reviewer: async def post_review(self, user_id: int, update_database: bool) -> None: """Format the review of a user and post it to the nomination voting channel.""" - review, reviewed_emoji = await self.make_review(user_id) + review, reviewed_emoji, nominee = await self.make_review(user_id) if not review: return guild = self.bot.get_guild(Guild.id) channel = guild.get_channel(Channels.nomination_voting) - log.trace(f"Posting the review of {user_id}") + log.trace(f"Posting the review of {nominee} ({nominee.id})") messages = await self._bulk_send(channel, review) await pin_no_system_message(messages[0]) @@ -113,14 +113,14 @@ class Reviewer: return "", None guild = self.bot.get_guild(Guild.id) - member = await get_or_fetch_member(guild, user_id) + nominee = await get_or_fetch_member(guild, user_id) - if not member: + if not nominee: return ( f"I tried to review the user with ID `{user_id}`, but they don't appear to be on the server :pensive:" - ), None + ), None, None - opening = f"{member.mention} ({member}) for Helper!" + opening = f"{nominee.mention} ({nominee}) for Helper!" current_nominations = "\n\n".join( f"**<@{entry['actor']}>:** {entry['reason'] or '*no reason given*'}" @@ -128,7 +128,7 @@ class Reviewer: ) current_nominations = f"**Nominated by:**\n{current_nominations}" - review_body = await self._construct_review_body(member) + review_body = await self._construct_review_body(nominee) reviewed_emoji = self._random_ducky(guild) vote_request = ( @@ -138,7 +138,7 @@ class Reviewer: ) review = "\n\n".join((opening, current_nominations, review_body, vote_request)) - return review, reviewed_emoji + return review, reviewed_emoji, nominee async def archive_vote(self, message: PartialMessage, passed: bool) -> None: """Archive this vote to #nomination-archive.""" -- cgit v1.2.3 From c217c3ef658954f2d491529a2a5c2085a285c229 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 1 Nov 2021 17:20:42 +0000 Subject: Manage nomination threads This change creates a thread while posting the nomination, and then archives it once the nomination is concluded. --- bot/exts/recruitment/talentpool/_review.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 6b5fae3b1..bc5cccda1 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -15,7 +15,7 @@ from discord.ext.commands import Context from bot.api import ResponseCodeError from bot.bot import Bot -from bot.constants import Channels, Colours, Emojis, Guild +from bot.constants import Channels, Colours, DEFAULT_THREAD_ARCHIVE_TIME, Emojis, Guild, Roles from bot.log import get_logger from bot.utils.members import get_or_fetch_member from bot.utils.messages import count_unique_users_reaction, pin_no_system_message @@ -95,6 +95,12 @@ class Reviewer: for reaction in (reviewed_emoji, "\N{THUMBS UP SIGN}", "\N{THUMBS DOWN SIGN}"): await last_message.add_reaction(reaction) + thread = await last_message.create_thread( + name=f"Nomination - {nominee}", + auto_archive_duration=DEFAULT_THREAD_ARCHIVE_TIME + ) + await thread.send(fr"<@&{Roles.mod_team}> <@&{Roles.admins}>") + if update_database: nomination = self._pool.cache.get(user_id) await self.bot.api_client.patch(f"bot/nominations/{nomination['id']}", json={"reviewed": True}) @@ -210,6 +216,13 @@ class Reviewer: colour=colour )) + # Thread channel IDs are the same as the message ID of the parent message. + nomination_thread = message.guild.get_thread(message.id) + if not nomination_thread: + log.warning(f"Could not find a thread linked to {message.channel.id}-{message.id}") + return + await nomination_thread.edit(archived=True) + for message_ in messages: await message_.delete() -- cgit v1.2.3 From 6bd2a56d43d70476d18c5fd66da20d8cf1518373 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 1 Nov 2021 18:52:03 +0000 Subject: Update nomination message regex --- bot/exts/recruitment/talentpool/_review.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index bc5cccda1..8b61a0eb5 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -36,9 +36,8 @@ MAX_MESSAGE_SIZE = 2000 MAX_EMBED_SIZE = 4000 # Regex for finding the first message of a nomination, and extracting the nominee. -# Historic nominations will have 2 role mentions at the start, new ones won't, optionally match for this. NOMINATION_MESSAGE_REGEX = re.compile( - r"(?:<@&\d+> <@&\d+>\n)*?<@!?(\d+?)> \(.+#\d{4}\) for Helper!\n\n\*\*Nominated by:\*\*", + r"<@!?(\d+)> \(.+#\d{4}\) for Helper!\n\n", re.MULTILINE ) -- cgit v1.2.3 From 0a4ba0b5d6341bc8cef13a30e35af5b4dc24248b Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 4 Nov 2021 15:50:14 +0000 Subject: Supress NotFound when archiving a nomination This supresses both the mesage deleteions and the thread archive, so that if they are removed before the code can get to them, it does not raise an error. --- bot/exts/recruitment/talentpool/_review.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 8b61a0eb5..fab126408 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -10,7 +10,7 @@ from typing import List, Optional, Union import arrow from dateutil.parser import isoparse -from discord import Embed, Emoji, Member, Message, NoMoreItems, PartialMessage, TextChannel +from discord import Embed, Emoji, Member, Message, NoMoreItems, NotFound, PartialMessage, TextChannel from discord.ext.commands import Context from bot.api import ResponseCodeError @@ -220,10 +220,13 @@ class Reviewer: if not nomination_thread: log.warning(f"Could not find a thread linked to {message.channel.id}-{message.id}") return - await nomination_thread.edit(archived=True) for message_ in messages: - await message_.delete() + with contextlib.suppress(NotFound): + await message_.delete() + + with contextlib.suppress(NotFound): + await nomination_thread.edit(archived=True) async def _construct_review_body(self, member: Member) -> str: """Formats the body of the nomination, with details of activity, infractions, and previous nominations.""" -- cgit v1.2.3 From e62ff5b4d0cd811e40d54e94ae5ae6d48f934624 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 1 Dec 2021 00:19:42 +0000 Subject: Ensure a nomination archival emoji isn't from the bot This is most relevant in local dev testing where the Emojis.check_mark could be the same as the Emojis.incident_actioned or Emojis.incident_unactioned, which would cause the bot to attempt to archive the post_review invocation if it was posted in the nomination voting channel. --- bot/exts/recruitment/talentpool/_cog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 699d60f42..615a95d20 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -516,6 +516,9 @@ class TalentPool(Cog, name="Talentpool"): if payload.channel_id != Channels.nomination_voting: return + if payload.user_id == self.bot.user.id: + return + message: PartialMessage = self.bot.get_channel(payload.channel_id).get_partial_message(payload.message_id) emoji = str(payload.emoji) -- cgit v1.2.3 From 96911a9c9b6e833e68fb2ead081d12da4ca5ffd9 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 1 Dec 2021 00:54:31 +0000 Subject: Fix emoji reaction error in reviewer Using a :eyes: style emoji string in a ctx.add_reaciton call will error. Discord expects either a unicode emoji, or a custom emoji. --- bot/exts/recruitment/talentpool/_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index fab126408..eced33738 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -375,10 +375,10 @@ class Reviewer: @staticmethod def _random_ducky(guild: Guild) -> Union[Emoji, str]: - """Picks a random ducky emoji. If no duckies found returns :eyes:.""" + """Picks a random ducky emoji. If no duckies found returns 👀.""" duckies = [emoji for emoji in guild.emojis if emoji.name.startswith("ducky")] if not duckies: - return ":eyes:" + return "\N{EYES}" return random.choice(duckies) @staticmethod -- cgit v1.2.3 From 108bf3276b49de4e6153a2c7f96c731907e3ca37 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 1 Dec 2021 01:04:14 +0000 Subject: Always return a review string for a given nomination --- bot/exts/recruitment/talentpool/_cog.py | 7 ++----- bot/exts/recruitment/talentpool/_review.py | 6 +++--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 615a95d20..ce0b2862f 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -484,11 +484,8 @@ class TalentPool(Cog, name="Talentpool"): async def get_review(self, ctx: Context, user_id: int) -> None: """Get the user's review as a markdown file.""" review, _, _ = await self.reviewer.make_review(user_id) - if review: - file = discord.File(StringIO(review), f"{user_id}_review.md") - await ctx.send(file=file) - else: - await ctx.send(f"There doesn't appear to be an active nomination for {user_id}") + file = discord.File(StringIO(review), f"{user_id}_review.md") + await ctx.send(file=file) @nomination_group.command(aliases=('review',)) @has_any_role(*MODERATION_ROLES) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index eced33738..a68169351 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -104,8 +104,8 @@ class Reviewer: nomination = self._pool.cache.get(user_id) await self.bot.api_client.patch(f"bot/nominations/{nomination['id']}", json={"reviewed": True}) - async def make_review(self, user_id: int) -> typing.Tuple[str, Optional[Emoji]]: - """Format a generic review of a user and return it with the reviewed emoji.""" + async def make_review(self, user_id: int) -> typing.Tuple[str, Optional[Emoji], Optional[Member]]: + """Format a generic review of a user and return it with the reviewed emoji and the user themselves.""" log.trace(f"Formatting the review of {user_id}") # Since `cache` is a defaultdict, we should take care @@ -115,7 +115,7 @@ class Reviewer: nomination = self._pool.cache.get(user_id) if not nomination: log.trace(f"There doesn't appear to be an active nomination for {user_id}") - return "", None + return f"There doesn't appear to be an active nomination for {user_id}", None, None guild = self.bot.get_guild(Guild.id) nominee = await get_or_fetch_member(guild, user_id) -- cgit v1.2.3 From 8c89ef922c5445f93e26e69ea4a65e5a2ceaf79e Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 1 Dec 2021 01:05:17 +0000 Subject: Use presence of a nominee as check for pending reviews --- bot/exts/recruitment/talentpool/_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index a68169351..110ac47bc 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -78,7 +78,7 @@ class Reviewer: async def post_review(self, user_id: int, update_database: bool) -> None: """Format the review of a user and post it to the nomination voting channel.""" review, reviewed_emoji, nominee = await self.make_review(user_id) - if not review: + if not nominee: return guild = self.bot.get_guild(Guild.id) -- cgit v1.2.3 From 6af87373ee3b97509d67ab611780c7e7892f4545 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 1 Dec 2021 01:05:36 +0000 Subject: Remove redundant Union in a type hint --- bot/exts/recruitment/talentpool/_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index ce0b2862f..8fa0be5b1 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -498,7 +498,7 @@ class TalentPool(Cog, name="Talentpool"): await ctx.message.add_reaction(Emojis.check_mark) @Cog.listener() - async def on_member_ban(self, guild: Guild, user: Union[MemberOrUser]) -> None: + async def on_member_ban(self, guild: Guild, user: MemberOrUser) -> None: """Remove `user` from the talent pool after they are banned.""" await self.end_nomination(user.id, "User was banned.") -- cgit v1.2.3 From 8408fb5686a7af43ee9ee9f8c192574e34a5f931 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Thu, 2 Dec 2021 00:44:25 +0200 Subject: Dynamic views for command help embeds (#1939) Dynamic views for command help embeds Adds views for commands to navigate groups. For subcommands, a button is added to show the parent's help embed. For groups, buttons are added for each subcommand to show their help embeds. The views are not generated when help is invoked in the context of an error. --- bot/exts/backend/error_handler.py | 13 ++- bot/exts/info/help.py | 147 ++++++++++++++++++++++++--- tests/bot/exts/backend/test_error_handler.py | 32 ------ 3 files changed, 141 insertions(+), 51 deletions(-) diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index 6ab6634a6..5bef72808 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -1,5 +1,4 @@ import difflib -import typing as t from discord import Embed from discord.ext.commands import ChannelNotFound, Cog, Context, TextChannelConverter, VoiceChannelConverter, errors @@ -97,13 +96,14 @@ class ErrorHandler(Cog): # MaxConcurrencyReached, ExtensionError await self.handle_unexpected_error(ctx, e) - @staticmethod - def get_help_command(ctx: Context) -> t.Coroutine: + async def send_command_help(self, ctx: Context) -> None: """Return a prepared `help` command invocation coroutine.""" if ctx.command: - return ctx.send_help(ctx.command) + self.bot.help_command.context = ctx + await ctx.send_help(ctx.command) + return - return ctx.send_help() + await ctx.send_help() async def try_silence(self, ctx: Context) -> bool: """ @@ -245,7 +245,6 @@ class ErrorHandler(Cog): elif isinstance(e, errors.ArgumentParsingError): embed = self._get_error_embed("Argument parsing error", str(e)) await ctx.send(embed=embed) - self.get_help_command(ctx).close() self.bot.stats.incr("errors.argument_parsing_error") return else: @@ -256,7 +255,7 @@ class ErrorHandler(Cog): self.bot.stats.incr("errors.other_user_input_error") await ctx.send(embed=embed) - await self.get_help_command(ctx) + await self.send_command_help(ctx) @staticmethod async def handle_check_failure(ctx: Context, e: errors.CheckFailure) -> None: diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index 743dfdd3f..06799fb71 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -1,10 +1,12 @@ +from __future__ import annotations + import itertools import re from collections import namedtuple from contextlib import suppress -from typing import List, Union +from typing import List, Optional, Union -from discord import Colour, Embed +from discord import ButtonStyle, Colour, Embed, Emoji, Interaction, PartialEmoji, ui from discord.ext.commands import Bot, Cog, Command, CommandError, Context, DisabledCommand, Group, HelpCommand from rapidfuzz import fuzz, process from rapidfuzz.utils import default_process @@ -26,6 +28,119 @@ NOT_ALLOWED_TO_RUN_MESSAGE = "***You cannot run this command.***\n\n" Category = namedtuple("Category", ["name", "description", "cogs"]) +class SubcommandButton(ui.Button): + """ + A button shown in a group's help embed. + + The button represents a subcommand, and pressing it will edit the help embed to that of the subcommand. + """ + + def __init__( + self, + help_command: CustomHelpCommand, + command: Command, + *, + style: ButtonStyle = ButtonStyle.primary, + label: Optional[str] = None, + disabled: bool = False, + custom_id: Optional[str] = None, + url: Optional[str] = None, + emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + row: Optional[int] = None + ): + super().__init__( + style=style, label=label, disabled=disabled, custom_id=custom_id, url=url, emoji=emoji, row=row + ) + + self.help_command = help_command + self.command = command + + async def callback(self, interaction: Interaction) -> None: + """Edits the help embed to that of the subcommand.""" + message = interaction.message + if not message: + return + + subcommand = self.command + if isinstance(subcommand, Group): + embed, subcommand_view = await self.help_command.format_group_help(subcommand) + else: + embed, subcommand_view = await self.help_command.command_formatting(subcommand) + await message.edit(embed=embed, view=subcommand_view) + + +class GroupButton(ui.Button): + """ + A button shown in a subcommand's help embed. + + The button represents the parent command, and pressing it will edit the help embed to that of the parent. + """ + + def __init__( + self, + help_command: CustomHelpCommand, + command: Command, + *, + style: ButtonStyle = ButtonStyle.secondary, + label: Optional[str] = None, + disabled: bool = False, + custom_id: Optional[str] = None, + url: Optional[str] = None, + emoji: Optional[Union[str, Emoji, PartialEmoji]] = None, + row: Optional[int] = None + ): + super().__init__( + style=style, label=label, disabled=disabled, custom_id=custom_id, url=url, emoji=emoji, row=row + ) + + self.help_command = help_command + self.command = command + + async def callback(self, interaction: Interaction) -> None: + """Edits the help embed to that of the parent.""" + message = interaction.message + if not message: + return + + embed, group_view = await self.help_command.format_group_help(self.command.parent) + await message.edit(embed=embed, view=group_view) + + +class CommandView(ui.View): + """ + The view added to any command's help embed. + + If the command has a parent, a button is added to the view to show that parent's help embed. + """ + + def __init__(self, help_command: CustomHelpCommand, command: Command): + super().__init__() + + if command.parent: + self.children.append(GroupButton(help_command, command, emoji="↩️")) + + +class GroupView(CommandView): + """ + The view added to a group's help embed. + + The view generates a SubcommandButton for every subcommand the group has. + """ + + MAX_BUTTONS_IN_ROW = 5 + MAX_ROWS = 5 + + def __init__(self, help_command: CustomHelpCommand, group: Group, subcommands: list[Command]): + super().__init__(help_command, group) + # Don't add buttons if only a portion of the subcommands can be shown. + if len(subcommands) + len(self.children) > self.MAX_ROWS * self.MAX_BUTTONS_IN_ROW: + log.trace(f"Attempted to add navigation buttons for `{group.qualified_name}`, but there was no space.") + return + + for subcommand in subcommands: + self.add_item(SubcommandButton(help_command, subcommand, label=subcommand.name)) + + class HelpQueryNotFound(ValueError): """ Raised when a HelpSession Query doesn't match a command or cog. @@ -148,7 +263,7 @@ class CustomHelpCommand(HelpCommand): await self.context.send(embed=embed) - async def command_formatting(self, command: Command) -> Embed: + async def command_formatting(self, command: Command) -> tuple[Embed, Optional[CommandView]]: """ Takes a command and turns it into an embed. @@ -186,12 +301,14 @@ class CustomHelpCommand(HelpCommand): command_details += f"*{formatted_doc or 'No details provided.'}*\n" embed.description = command_details - return embed + # If the help is invoked in the context of an error, don't show subcommand navigation. + view = CommandView(self, command) if not self.context.command_failed else None + return embed, view async def send_command_help(self, command: Command) -> None: """Send help for a single command.""" - embed = await self.command_formatting(command) - message = await self.context.send(embed=embed) + embed, view = await self.command_formatting(command) + message = await self.context.send(embed=embed, view=view) await wait_for_deletion(message, (self.context.author.id,)) @staticmethod @@ -212,25 +329,31 @@ class CustomHelpCommand(HelpCommand): else: return "".join(details) - async def send_group_help(self, group: Group) -> None: - """Sends help for a group command.""" + async def format_group_help(self, group: Group) -> tuple[Embed, Optional[CommandView]]: + """Formats help for a group command.""" subcommands = group.commands if len(subcommands) == 0: # no subcommands, just treat it like a regular command - await self.send_command_help(group) - return + return await self.command_formatting(group) # remove commands that the user can't run and are hidden, and sort by name commands_ = await self.filter_commands(subcommands, sort=True) - embed = await self.command_formatting(group) + embed, _ = await self.command_formatting(group) command_details = self.get_commands_brief_details(commands_) if command_details: embed.description += f"\n**Subcommands:**\n{command_details}" - message = await self.context.send(embed=embed) + # If the help is invoked in the context of an error, don't show subcommand navigation. + view = GroupView(self, group, commands_) if not self.context.command_failed else None + return embed, view + + async def send_group_help(self, group: Group) -> None: + """Sends help for a group command.""" + embed, view = await self.format_group_help(group) + message = await self.context.send(embed=embed, view=view) await wait_for_deletion(message, (self.context.author.id,)) async def send_cog_help(self, cog: Cog) -> None: diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index 462f718e6..d12329b1f 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -572,38 +572,6 @@ class IndividualErrorHandlerTests(unittest.IsolatedAsyncioTestCase): push_scope_mock.set_extra.has_calls(set_extra_calls) -class OtherErrorHandlerTests(unittest.IsolatedAsyncioTestCase): - """Other `ErrorHandler` tests.""" - - def setUp(self): - self.bot = MockBot() - self.ctx = MockContext() - - async def test_get_help_command_command_specified(self): - """Should return coroutine of help command of specified command.""" - self.ctx.command = "foo" - result = ErrorHandler.get_help_command(self.ctx) - expected = self.ctx.send_help("foo") - self.assertEqual(result.__qualname__, expected.__qualname__) - self.assertEqual(result.cr_frame.f_locals, expected.cr_frame.f_locals) - - # Await coroutines to avoid warnings - await result - await expected - - async def test_get_help_command_no_command_specified(self): - """Should return coroutine of help command.""" - self.ctx.command = None - result = ErrorHandler.get_help_command(self.ctx) - expected = self.ctx.send_help() - self.assertEqual(result.__qualname__, expected.__qualname__) - self.assertEqual(result.cr_frame.f_locals, expected.cr_frame.f_locals) - - # Await coroutines to avoid warnings - await result - await expected - - class ErrorHandlerSetupTests(unittest.TestCase): """Tests for `ErrorHandler` `setup` function.""" -- cgit v1.2.3 From 1f1ca41b172eda41a94e4ae556a923eee2d7cc26 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 1 Dec 2021 22:45:47 +0000 Subject: Sort subscribe roles alphabetically --- bot/exts/info/subscribe.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index 2e6101d27..4797f2347 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -165,7 +165,9 @@ class Subscribe(commands.Cog): name=discord_role.name, ) ) - # Sort unavailable roles to the end of the list + + # Sort by role name, then shift unavailable roles to the end of the list + self.assignable_roles.sort(key=operator.attrgetter("name")) self.assignable_roles.sort(key=operator.methodcaller("is_currently_available"), reverse=True) @commands.cooldown(1, 10, commands.BucketType.member) -- cgit v1.2.3 From 8265f206517ef1a35b03120993c8fab4e45bb88d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 1 Dec 2021 22:47:25 +0000 Subject: Redirect subscribe command output to bot commands Instead of silently failing in channels other than bot commands for non-staff, the bot now instead redirects the command output to bot commands and pings the user. To facilitate this, I had to change the ctx.reply to a ctx.send since the invocation message may be in a different channel. --- bot/exts/info/subscribe.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index 4797f2347..1299d5d59 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -10,9 +10,9 @@ from discord.interactions import Interaction from bot import constants from bot.bot import Bot -from bot.decorators import in_whitelist +from bot.decorators import redirect_output from bot.log import get_logger -from bot.utils import checks, members, scheduling +from bot.utils import members, scheduling @dataclass(frozen=True) @@ -172,7 +172,10 @@ class Subscribe(commands.Cog): @commands.cooldown(1, 10, commands.BucketType.member) @commands.command(name="subscribe") - @in_whitelist(channels=(constants.Channels.bot_commands,)) + @redirect_output( + destination_channel=constants.Channels.bot_commands, + bypass_roles=constants.STAFF_PARTNERS_COMMUNITY_ROLES, + ) async def subscribe_command(self, ctx: commands.Context, *_) -> None: # We don't actually care about the args """Display the member's current state for each role, and allow them to add/remove the roles.""" await self.init_task @@ -183,18 +186,12 @@ class Subscribe(commands.Cog): row = index // ITEMS_PER_ROW button_view.add_item(SingleRoleButton(role, role.role_id in author_roles, row)) - await ctx.reply( + await ctx.send( "Click the buttons below to add or remove your roles!", view=button_view, delete_after=DELETE_MESSAGE_AFTER, ) - # This cannot be static (must have a __func__ attribute). - async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None: - """Check for & ignore any InWhitelistCheckFailure.""" - if isinstance(error, checks.InWhitelistCheckFailure): - error.handled = True - def setup(bot: Bot) -> None: """Load the Subscribe cog.""" -- cgit v1.2.3 From e311048fb884738613201514991fb06f8403254b Mon Sep 17 00:00:00 2001 From: aru Date: Thu, 2 Dec 2021 13:30:51 -0500 Subject: set three_day to 4320, the number of minutes in 3 days --- bot/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/constants.py b/bot/constants.py index 3170c2915..52143132a 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -691,7 +691,7 @@ class VideoPermission(metaclass=YAMLGetter): class ThreadArchiveTimes(Enum): HOUR = 60 DAY = 1440 - THREE_DAY = 4230 + THREE_DAY = 4320 WEEK = 10080 -- cgit v1.2.3 From 88e65f60437dfa9caf2064487e7294b4f029e2f6 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 2 Dec 2021 21:06:27 +0200 Subject: Remove cleaning based on number of messages All clean commands now use the clean limit (message, time delta, ISO datetime) instead of `traverse`. Consequently, `clean all` has been removed as `clean until` now effectively fulfills that role. --- bot/exts/moderation/clean.py | 93 +++++++++++++++++++++----------------------- 1 file changed, 44 insertions(+), 49 deletions(-) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index 826265aa3..8ad7a56d8 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -5,7 +5,6 @@ import time from collections import defaultdict from contextlib import suppress from datetime import datetime -from itertools import islice from typing import Any, Callable, Iterable, Literal, Optional, TYPE_CHECKING, Union from discord import Colour, Message, NotFound, TextChannel, User, errors @@ -21,8 +20,6 @@ from bot.utils.channel import is_mod_channel log = logging.getLogger(__name__) -# Default number of messages to look at in each channel. -DEFAULT_TRAVERSE = 10 # Number of seconds before command invocations and responses are deleted in non-moderation channels. MESSAGE_DELETE_DELAY = 5 @@ -87,7 +84,6 @@ class Clean(Cog): @staticmethod def _validate_input( - traverse: int, channels: Optional[CleanChannels], bots_only: bool, users: Optional[list[User]], @@ -95,9 +91,9 @@ class Clean(Cog): second_limit: Optional[CleanLimit], ) -> None: """Raise errors if an argument value or a combination of values is invalid.""" - # Is this an acceptable amount of messages to traverse? - if traverse > CleanMessages.message_limit: - raise BadArgument(f"Cannot traverse more than {CleanMessages.message_limit} messages.") + if first_limit is None: + # This is an optional argument for the sake of the master command, but it's actually required. + raise BadArgument("Missing cleaning limit.") if (isinstance(first_limit, Message) or isinstance(second_limit, Message)) and channels: raise BadArgument("Both a message limit and channels specified.") @@ -195,11 +191,11 @@ class Clean(Cog): # Invocation message has already been deleted log.info("Tried to delete invocation message, but it was already deleted.") - def _get_messages_from_cache(self, traverse: int, to_delete: Predicate) -> tuple[defaultdict[Any, list], list[int]]: + def _get_messages_from_cache(self, to_delete: Predicate) -> tuple[defaultdict[Any, list], list[int]]: """Helper function for getting messages from the cache.""" message_mappings = defaultdict(list) message_ids = [] - for message in islice(self.bot.cached_messages, traverse): + for message in self.bot.cached_messages: if not self.cleaning: # Cleaning was canceled return message_mappings, message_ids @@ -212,17 +208,16 @@ class Clean(Cog): async def _get_messages_from_channels( self, - traverse: int, channels: Iterable[TextChannel], to_delete: Predicate, - before: Optional[datetime] = None, + before: datetime, after: Optional[datetime] = None ) -> tuple[defaultdict[Any, list], list]: message_mappings = defaultdict(list) message_ids = [] for channel in channels: - async for message in channel.history(limit=traverse, before=before, after=after): + async for message in channel.history(limit=CleanMessages.message_limit, before=before, after=after): if not self.cleaning: # Cleaning was canceled, return empty containers. @@ -343,7 +338,6 @@ class Clean(Cog): async def _clean_messages( self, ctx: Context, - traverse: int, channels: Optional[CleanChannels], bots_only: bool = False, users: Optional[list[User]] = None, @@ -353,7 +347,7 @@ class Clean(Cog): use_cache: Optional[bool] = True ) -> None: """A helper function that does the actual message cleaning.""" - self._validate_input(traverse, channels, bots_only, users, first_limit, second_limit) + self._validate_input(channels, bots_only, users, first_limit, second_limit) # Are we already performing a clean? if self.cleaning: @@ -387,13 +381,12 @@ class Clean(Cog): await self._delete_invocation(ctx) if channels == "*" and use_cache: - message_mappings, message_ids = self._get_messages_from_cache(traverse=traverse, to_delete=predicate) + message_mappings, message_ids = self._get_messages_from_cache(to_delete=predicate) else: deletion_channels = channels if channels == "*": deletion_channels = [channel for channel in ctx.guild.channels if isinstance(channel, TextChannel)] message_mappings, message_ids = await self._get_messages_from_channels( - traverse=traverse, channels=deletion_channels, to_delete=predicate, before=second_limit, @@ -422,7 +415,6 @@ class Clean(Cog): self, ctx: Context, users: Greedy[User] = None, - traverse: Optional[int] = None, first_limit: Optional[CleanLimit] = None, second_limit: Optional[CleanLimit] = None, use_cache: Optional[bool] = None, @@ -437,11 +429,9 @@ class Clean(Cog): If arguments are provided, will act as a master command from which all subcommands can be derived. \u2003• `users`: A series of user mentions, ID's, or names. - \u2003• `traverse`: The number of messages to look at in each channel. If using the cache, will look at the - first `traverse` messages in the cache. \u2003• `first_limit` and `second_limit`: A message, a duration delta, or an ISO datetime. + At least one limit is required. If a message is provided, cleaning will happen in that channel, and channels cannot be provided. - If a limit is provided, multiple channels cannot be provided. If only one of them is provided, acts as `clean until`. If both are provided, acts as `clean between`. \u2003• `use_cache`: Whether to use the message cache. If not provided, will default to False unless an asterisk is used for the channels. @@ -451,20 +441,15 @@ class Clean(Cog): If the pattern contains spaces, it still needs to be enclosed in double quotes on top of that. \u2003• `channels`: A series of channels to delete in, or an asterisk to delete from all channels. """ - if not any([traverse, users, first_limit, second_limit, regex, channels]): + if not any([users, first_limit, second_limit, regex, channels]): await ctx.send_help(ctx.command) return - if not traverse: - if first_limit: - traverse = CleanMessages.message_limit - else: - traverse = DEFAULT_TRAVERSE if use_cache is None: use_cache = channels == "*" await self._clean_messages( - ctx, traverse, channels, bots_only, users, regex, first_limit, second_limit, use_cache + ctx, channels, bots_only, users, regex, first_limit, second_limit, use_cache ) @clean_group.command(name="user", aliases=["users"]) @@ -472,56 +457,68 @@ class Clean(Cog): self, ctx: Context, user: User, - traverse: Optional[int] = DEFAULT_TRAVERSE, + message_or_time: CleanLimit, use_cache: Optional[bool] = True, *, channels: CleanChannels = None ) -> None: - """Delete messages posted by the provided user, stop cleaning after traversing `traverse` messages.""" - await self._clean_messages(ctx, traverse, users=[user], channels=channels, use_cache=use_cache) + """ + Delete messages posted by the provided user, stop cleaning after reaching `message_or_time`. - @clean_group.command(name="all", aliases=["everything"]) - async def clean_all( - self, - ctx: Context, - traverse: Optional[int] = DEFAULT_TRAVERSE, - use_cache: Optional[bool] = True, - *, - channels: CleanChannels = None - ) -> None: - """Delete all messages, regardless of poster, stop cleaning after traversing `traverse` messages.""" - await self._clean_messages(ctx, traverse, channels=channels, use_cache=use_cache) + `message_or_time` can be either a message to stop at (exclusive), a timedelta for max message age, or an ISO + datetime. + + If a message is specified, `channels` cannot be specified. + """ + await self._clean_messages( + ctx, users=[user], channels=channels, first_limit=message_or_time, use_cache=use_cache + ) @clean_group.command(name="bots", aliases=["bot"]) async def clean_bots( self, ctx: Context, - traverse: Optional[int] = DEFAULT_TRAVERSE, + message_or_time: CleanLimit, use_cache: Optional[bool] = True, *, channels: CleanChannels = None ) -> None: - """Delete all messages posted by a bot, stop cleaning after traversing `traverse` messages.""" - await self._clean_messages(ctx, traverse, bots_only=True, channels=channels, use_cache=use_cache) + """ + Delete all messages posted by a bot, stop cleaning after traversing `traverse` messages. + + `message_or_time` can be either a message to stop at (exclusive), a timedelta for max message age, or an ISO + datetime. + + If a message is specified, `channels` cannot be specified. + """ + await self._clean_messages( + ctx, bots_only=True, channels=channels, first_limit=message_or_time, use_cache=use_cache + ) @clean_group.command(name="regex", aliases=["word", "expression", "pattern"]) async def clean_regex( self, ctx: Context, regex: Regex, - traverse: Optional[int] = DEFAULT_TRAVERSE, + message_or_time: CleanLimit, use_cache: Optional[bool] = True, *, channels: CleanChannels = None ) -> None: """ - Delete all messages that match a certain regex, stop cleaning after traversing `traverse` messages. + Delete all messages that match a certain regex, stop cleaning after reaching `message_or_time`. + + `message_or_time` can be either a message to stop at (exclusive), a timedelta for max message age, or an ISO + datetime. + If a message is specified, `channels` cannot be specified. The pattern must be provided enclosed in backticks. If the pattern contains spaces, it still needs to be enclosed in double quotes on top of that. For example: `[0-9]` """ - await self._clean_messages(ctx, traverse, regex=regex, channels=channels, use_cache=use_cache) + await self._clean_messages( + ctx, regex=regex, channels=channels, first_limit=message_or_time, use_cache=use_cache + ) @clean_group.command(name="until") async def clean_until( @@ -538,7 +535,6 @@ class Clean(Cog): """ await self._clean_messages( ctx, - CleanMessages.message_limit, channels=[channel] if channel else None, first_limit=until, ) @@ -562,7 +558,6 @@ class Clean(Cog): """ await self._clean_messages( ctx, - CleanMessages.message_limit, channels=[channel] if channel else None, first_limit=first_limit, second_limit=second_limit, -- cgit v1.2.3 From fbd35131a31669b8aff72dd6bc176ea6ae84d333 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Thu, 2 Dec 2021 14:08:45 -0500 Subject: remove default thread archive time as discord.py supports that already --- bot/constants.py | 5 ----- bot/exts/recruitment/talentpool/_review.py | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 3170c2915..a0978fae2 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -699,11 +699,6 @@ class ThreadArchiveTimes(Enum): DEBUG_MODE: bool = _CONFIG_YAML["debug"] == "true" FILE_LOGS: bool = _CONFIG_YAML["file_logs"].lower() == "true" -if DEBUG_MODE: - DEFAULT_THREAD_ARCHIVE_TIME = ThreadArchiveTimes.HOUR.value -else: - DEFAULT_THREAD_ARCHIVE_TIME = ThreadArchiveTimes.WEEK.value - # Paths BOT_DIR = os.path.dirname(__file__) PROJECT_ROOT = os.path.abspath(os.path.join(BOT_DIR, os.pardir)) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index 110ac47bc..f6b81ae50 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -15,7 +15,7 @@ from discord.ext.commands import Context from bot.api import ResponseCodeError from bot.bot import Bot -from bot.constants import Channels, Colours, DEFAULT_THREAD_ARCHIVE_TIME, Emojis, Guild, Roles +from bot.constants import Channels, Colours, Emojis, Guild, Roles from bot.log import get_logger from bot.utils.members import get_or_fetch_member from bot.utils.messages import count_unique_users_reaction, pin_no_system_message @@ -96,7 +96,6 @@ class Reviewer: thread = await last_message.create_thread( name=f"Nomination - {nominee}", - auto_archive_duration=DEFAULT_THREAD_ARCHIVE_TIME ) await thread.send(fr"<@&{Roles.mod_team}> <@&{Roles.admins}>") -- cgit v1.2.3 From 0df94eac77703943221e39b9e9898515e266a9ef Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 2 Dec 2021 22:21:35 +0200 Subject: Simplify cache usage Removes the cache usage argument from the clean commands. Cache usage is now an implementation detail. The cache will be used if the age of the oldest message requested for cleaning is younger than the oldest message in the cache. Additionally fixes the logger to the one used in the rest of the bot (caused by a faulty merge). --- bot/exts/moderation/clean.py | 65 ++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index 8ad7a56d8..bb6e44d6f 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -1,11 +1,11 @@ import contextlib -import logging import re import time from collections import defaultdict from contextlib import suppress from datetime import datetime -from typing import Any, Callable, Iterable, Literal, Optional, TYPE_CHECKING, Union +from itertools import takewhile +from typing import Callable, Iterable, Literal, Optional, TYPE_CHECKING, Union from discord import Colour, Message, NotFound, TextChannel, User, errors from discord.ext.commands import Cog, Context, Converter, Greedy, group, has_any_role @@ -16,9 +16,10 @@ from bot.bot import Bot from bot.constants import Channels, CleanMessages, Colours, Emojis, Event, Icons, MODERATION_ROLES from bot.converters import Age, ISODateTime from bot.exts.moderation.modlog import ModLog +from bot.log import get_logger from bot.utils.channel import is_mod_channel -log = logging.getLogger(__name__) +log = get_logger(__name__) # Number of seconds before command invocations and responses are deleted in non-moderation channels. MESSAGE_DELETE_DELAY = 5 @@ -191,11 +192,19 @@ class Clean(Cog): # Invocation message has already been deleted log.info("Tried to delete invocation message, but it was already deleted.") - def _get_messages_from_cache(self, to_delete: Predicate) -> tuple[defaultdict[Any, list], list[int]]: + def _use_cache(self, limit: datetime) -> bool: + """Tell whether all messages to be cleaned can be found in the cache.""" + return self.bot.cached_messages[0].created_at <= limit + + def _get_messages_from_cache( + self, + to_delete: Predicate, + lower_limit: datetime + ) -> tuple[defaultdict[TextChannel, list], list[int]]: """Helper function for getting messages from the cache.""" message_mappings = defaultdict(list) message_ids = [] - for message in self.bot.cached_messages: + for message in takewhile(lambda m: m.created_at > lower_limit, reversed(self.bot.cached_messages)): if not self.cleaning: # Cleaning was canceled return message_mappings, message_ids @@ -212,7 +221,7 @@ class Clean(Cog): to_delete: Predicate, before: datetime, after: Optional[datetime] = None - ) -> tuple[defaultdict[Any, list], list]: + ) -> tuple[defaultdict[TextChannel, list], list]: message_mappings = defaultdict(list) message_ids = [] @@ -344,7 +353,6 @@ class Clean(Cog): regex: Optional[re.Pattern] = None, first_limit: Optional[CleanLimit] = None, second_limit: Optional[CleanLimit] = None, - use_cache: Optional[bool] = True ) -> None: """A helper function that does the actual message cleaning.""" self._validate_input(channels, bots_only, users, first_limit, second_limit) @@ -380,9 +388,11 @@ class Clean(Cog): # Delete the invocation first await self._delete_invocation(ctx) - if channels == "*" and use_cache: - message_mappings, message_ids = self._get_messages_from_cache(to_delete=predicate) + if self._use_cache(first_limit): + log.trace(f"Messages for cleaning by {ctx.author.id} will be searched in the cache.") + message_mappings, message_ids = self._get_messages_from_cache(to_delete=predicate, lower_limit=first_limit) else: + log.trace(f"Messages for cleaning by {ctx.author.id} will be searched in channel histories.") deletion_channels = channels if channels == "*": deletion_channels = [channel for channel in ctx.guild.channels if isinstance(channel, TextChannel)] @@ -417,9 +427,8 @@ class Clean(Cog): users: Greedy[User] = None, first_limit: Optional[CleanLimit] = None, second_limit: Optional[CleanLimit] = None, - use_cache: Optional[bool] = None, - bots_only: Optional[bool] = False, regex: Optional[Regex] = None, + bots_only: Optional[bool] = False, *, channels: CleanChannels = None # "Optional" with discord.py silently ignores incorrect input. ) -> None: @@ -433,24 +442,17 @@ class Clean(Cog): At least one limit is required. If a message is provided, cleaning will happen in that channel, and channels cannot be provided. If only one of them is provided, acts as `clean until`. If both are provided, acts as `clean between`. - \u2003• `use_cache`: Whether to use the message cache. - If not provided, will default to False unless an asterisk is used for the channels. - \u2003• `bots_only`: Whether to delete only bots. If specified, users cannot be specified. \u2003• `regex`: A regex pattern the message must contain to be deleted. The pattern must be provided enclosed in backticks. If the pattern contains spaces, it still needs to be enclosed in double quotes on top of that. + \u2003• `bots_only`: Whether to delete only bots. If specified, users cannot be specified. \u2003• `channels`: A series of channels to delete in, or an asterisk to delete from all channels. """ if not any([users, first_limit, second_limit, regex, channels]): await ctx.send_help(ctx.command) return - if use_cache is None: - use_cache = channels == "*" - - await self._clean_messages( - ctx, channels, bots_only, users, regex, first_limit, second_limit, use_cache - ) + await self._clean_messages(ctx, channels, bots_only, users, regex, first_limit, second_limit) @clean_group.command(name="user", aliases=["users"]) async def clean_user( @@ -458,7 +460,6 @@ class Clean(Cog): ctx: Context, user: User, message_or_time: CleanLimit, - use_cache: Optional[bool] = True, *, channels: CleanChannels = None ) -> None: @@ -470,19 +471,10 @@ class Clean(Cog): If a message is specified, `channels` cannot be specified. """ - await self._clean_messages( - ctx, users=[user], channels=channels, first_limit=message_or_time, use_cache=use_cache - ) + await self._clean_messages(ctx, users=[user], channels=channels, first_limit=message_or_time) @clean_group.command(name="bots", aliases=["bot"]) - async def clean_bots( - self, - ctx: Context, - message_or_time: CleanLimit, - use_cache: Optional[bool] = True, - *, - channels: CleanChannels = None - ) -> None: + async def clean_bots(self, ctx: Context, message_or_time: CleanLimit, *, channels: CleanChannels = None) -> None: """ Delete all messages posted by a bot, stop cleaning after traversing `traverse` messages. @@ -491,9 +483,7 @@ class Clean(Cog): If a message is specified, `channels` cannot be specified. """ - await self._clean_messages( - ctx, bots_only=True, channels=channels, first_limit=message_or_time, use_cache=use_cache - ) + await self._clean_messages(ctx, bots_only=True, channels=channels, first_limit=message_or_time) @clean_group.command(name="regex", aliases=["word", "expression", "pattern"]) async def clean_regex( @@ -501,7 +491,6 @@ class Clean(Cog): ctx: Context, regex: Regex, message_or_time: CleanLimit, - use_cache: Optional[bool] = True, *, channels: CleanChannels = None ) -> None: @@ -516,9 +505,7 @@ class Clean(Cog): If the pattern contains spaces, it still needs to be enclosed in double quotes on top of that. For example: `[0-9]` """ - await self._clean_messages( - ctx, regex=regex, channels=channels, first_limit=message_or_time, use_cache=use_cache - ) + await self._clean_messages(ctx, regex=regex, channels=channels, first_limit=message_or_time) @clean_group.command(name="until") async def clean_until( -- cgit v1.2.3 From 89f991374b1d0e9d9f7312c3c715129a8bba6ac2 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 2 Dec 2021 22:34:06 +0200 Subject: Update _build_predicate to require a limit --- bot/exts/moderation/clean.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index bb6e44d6f..a08788fd6 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -119,11 +119,11 @@ class Clean(Cog): @staticmethod def _build_predicate( + first_limit: datetime, + second_limit: Optional[datetime] = None, bots_only: bool = False, users: Optional[list[User]] = None, regex: Optional[re.Pattern] = None, - first_limit: Optional[datetime] = None, - second_limit: Optional[datetime] = None, ) -> Predicate: """Return the predicate that decides whether to delete a given message.""" def predicate_bots_only(message: Message) -> bool: @@ -164,20 +164,18 @@ class Clean(Cog): predicates = [] # Set up the correct predicate + if second_limit: + predicates.append(predicate_range) # Delete messages in the specified age range + else: + predicates.append(predicate_after) # Delete messages older than the specified age + if bots_only: predicates.append(predicate_bots_only) # Delete messages from bots if users: predicates.append(predicate_specific_users) # Delete messages from specific user if regex: predicates.append(predicate_regex) # Delete messages that match regex - # Add up to one of the following: - if second_limit: - predicates.append(predicate_range) # Delete messages in the specified age range - elif first_limit: - predicates.append(predicate_after) # Delete messages older than specific message - if not predicates: - return lambda m: True if len(predicates) == 1: return predicates[0] return lambda m: all(pred(m) for pred in predicates) @@ -383,7 +381,7 @@ class Clean(Cog): first_limit, second_limit = sorted([first_limit, second_limit]) # Needs to be called after standardizing the input. - predicate = self._build_predicate(bots_only, users, regex, first_limit, second_limit) + predicate = self._build_predicate(first_limit, second_limit, bots_only, users, regex) # Delete the invocation first await self._delete_invocation(ctx) -- cgit v1.2.3 From 0150914b469ae5a8e4b407b4ffc1a15e70bad614 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Fri, 3 Dec 2021 13:35:39 +0300 Subject: Update PEP Repo URL The PEP github repo changed branch from master, to main, breaking our code. Switch the ref from master to main in our code. --- bot/exts/info/pep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/pep.py b/bot/exts/info/pep.py index 259095b50..8c0db18bc 100644 --- a/bot/exts/info/pep.py +++ b/bot/exts/info/pep.py @@ -16,7 +16,7 @@ log = get_logger(__name__) ICON_URL = "https://www.python.org/static/opengraph-icon-200x200.png" BASE_PEP_URL = "http://www.python.org/dev/peps/pep-" -PEPS_LISTING_API_URL = "https://api.github.com/repos/python/peps/contents?ref=master" +PEPS_LISTING_API_URL = "https://api.github.com/repos/python/peps/contents?ref=main" pep_cache = AsyncCache() -- cgit v1.2.3 From 20eecf06513aaff02a6c8531d90cff0b3b7addce Mon Sep 17 00:00:00 2001 From: mbaruh Date: Fri, 3 Dec 2021 15:32:17 +0200 Subject: Remove now redundant input check. --- bot/exts/moderation/clean.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index a08788fd6..3def2a416 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -107,10 +107,6 @@ class Clean(Cog): if users and bots_only: raise BadArgument("Marked as bots only, but users were specified.") - # This is an implementation error rather than user error. - if second_limit and not first_limit: - raise ValueError("Second limit specified without the first.") - @staticmethod async def _send_expiring_message(ctx: Context, content: str) -> None: """Send `content` to the context channel. Automatically delete if it's not a mod channel.""" -- cgit v1.2.3 From db85e56baf7edbd204fae42572d01923ec398840 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 3 Dec 2021 14:56:09 +0000 Subject: Attepmt to fetch un-cached nomination threads on archive Fixes BOT-1R0 Fixes #1992 The time between a vote passing and the helper being helpered can sometimes be >7 days, meaning the thread may have auto-archived by then. We should deal with this by trying to fetch the threead from the API if it's not cached. --- bot/exts/recruitment/talentpool/_review.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index f6b81ae50..0e7194892 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -217,8 +217,11 @@ class Reviewer: # Thread channel IDs are the same as the message ID of the parent message. nomination_thread = message.guild.get_thread(message.id) if not nomination_thread: - log.warning(f"Could not find a thread linked to {message.channel.id}-{message.id}") - return + try: + nomination_thread = await message.guild.fetch_channel(message.id) + except NotFound: + log.warning(f"Could not find a thread linked to {message.channel.id}-{message.id}") + return for message_ in messages: with contextlib.suppress(NotFound): -- cgit v1.2.3 From 0a3e7ea31e430b9a1474fa321ed771358ad7d952 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 3 Dec 2021 00:03:29 +0000 Subject: Patch d.py's message convertor to infer channelID from the given context Discord.py's Message convertor is supposed to infer channelID based on ctx.channel if only a messageID is given. A 'refactor' (linked below) a few weeks before d.py's archival broke this, so that if only a messageID is given to the convertor, it will only find that message if it's in the bot's cache. Co-authored-by: Hassan Abouelela --- bot/__init__.py | 5 +++++ bot/monkey_patches.py | 23 +++++++++++++++++++++++ bot/utils/regex.py | 1 + 3 files changed, 29 insertions(+) diff --git a/bot/__init__.py b/bot/__init__.py index a1c4466f1..17d99105a 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -18,6 +18,11 @@ if os.name == "nt": monkey_patches.patch_typing() +# This patches any convertors that use PartialMessage, but not the PartialMessageConverter itself +# as library objects are made by this mapping. +# https://github.com/Rapptz/discord.py/blob/1a4e73d59932cdbe7bf2c281f25e32529fc7ae1f/discord/ext/commands/converter.py#L984-L1004 +commands.converter.PartialMessageConverter = monkey_patches.FixedPartialMessageConverter + # Monkey-patch discord.py decorators to use the Command subclass which supports root aliases. # Must be patched before any cogs are added. commands.command = partial(commands.command, cls=monkey_patches.Command) diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py index 23482f7c3..b5c0de8d9 100644 --- a/bot/monkey_patches.py +++ b/bot/monkey_patches.py @@ -5,6 +5,7 @@ from discord import Forbidden, http from discord.ext import commands from bot.log import get_logger +from bot.utils.regex import MESSAGE_ID_RE log = get_logger(__name__) @@ -50,3 +51,25 @@ def patch_typing() -> None: pass http.HTTPClient.send_typing = honeybadger_type + + +class FixedPartialMessageConverter(commands.PartialMessageConverter): + """ + Make the Message converter infer channelID from the given context if only a messageID is given. + + Discord.py's Message converter is supposed to infer channelID based + on ctx.channel if only a messageID is given. A refactor commit, linked below, + a few weeks before d.py's archival broke this defined behaviour of the converter. + Currently, if only a messageID is given to the converter, it will only find that message + if it's in the bot's cache. + + https://github.com/Rapptz/discord.py/commit/1a4e73d59932cdbe7bf2c281f25e32529fc7ae1f + """ + + @staticmethod + def _get_id_matches(ctx: commands.Context, argument: str) -> tuple[int, int, int]: + """Inserts ctx.channel.id before calling super method if argument is just a messageID.""" + match = MESSAGE_ID_RE.match(argument) + if match: + argument = f"{ctx.channel.id}-{match.group('message_id')}" + return commands.PartialMessageConverter._get_id_matches(ctx, argument) diff --git a/bot/utils/regex.py b/bot/utils/regex.py index d77f5950b..9dc1eba9d 100644 --- a/bot/utils/regex.py +++ b/bot/utils/regex.py @@ -12,3 +12,4 @@ INVITE_RE = re.compile( r"(?P[a-zA-Z0-9\-]+)", # the invite code itself flags=re.IGNORECASE ) +MESSAGE_ID_RE = re.compile(r'(?P[0-9]{15,20})$') -- cgit v1.2.3 From ed3f5aaec5ac87a6d92d8068d9e07190adc3a5d2 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sun, 5 Dec 2021 23:49:46 +0200 Subject: Properly check the channel when deleting from cache Previously the cache was only used to delete from all channels. I didn't add a channels check when I changed it. --- bot/exts/moderation/clean.py | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index 3def2a416..0b83fc7e0 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -113,6 +113,28 @@ class Clean(Cog): delete_after = None if is_mod_channel(ctx.channel) else MESSAGE_DELETE_DELAY await ctx.send(content, delete_after=delete_after) + @staticmethod + def _channels_set( + channels: CleanChannels, ctx: Context, first_limit: CleanLimit, second_limit: CleanLimit + ) -> set[TextChannel]: + """Standardize the input `channels` argument to a usable set of text channels.""" + # Default to using the invoking context's channel or the channel of the message limit(s). + if not channels: + # Input was validated - if first_limit is a message, second_limit won't point at a different channel. + if isinstance(first_limit, Message): + channels = {first_limit.channel} + elif isinstance(second_limit, Message): + channels = {second_limit.channel} + else: + channels = {ctx.channel} + else: + if channels == "*": + channels = {channel for channel in ctx.guild.channels if isinstance(channel, TextChannel)} + else: + channels = set(channels) + + return channels + @staticmethod def _build_predicate( first_limit: datetime, @@ -192,6 +214,7 @@ class Clean(Cog): def _get_messages_from_cache( self, + channels: set[TextChannel], to_delete: Predicate, lower_limit: datetime ) -> tuple[defaultdict[TextChannel, list], list[int]]: @@ -203,7 +226,7 @@ class Clean(Cog): # Cleaning was canceled return message_mappings, message_ids - if to_delete(message): + if message.channel in channels and to_delete(message): message_mappings[message.channel].append(message) message_ids.append(message.id) @@ -359,15 +382,7 @@ class Clean(Cog): return self.cleaning = True - # Default to using the invoking context's channel or the channel of the message limit(s). - if not channels: - # Input was validated - if first_limit is a message, second_limit won't point at a different channel. - if isinstance(first_limit, Message): - channels = [first_limit.channel] - elif isinstance(second_limit, Message): - channels = [second_limit.channel] - else: - channels = [ctx.channel] + deletion_channels = self._channels_set(channels, ctx, first_limit, second_limit) if isinstance(first_limit, Message): first_limit = first_limit.created_at @@ -384,12 +399,11 @@ class Clean(Cog): if self._use_cache(first_limit): log.trace(f"Messages for cleaning by {ctx.author.id} will be searched in the cache.") - message_mappings, message_ids = self._get_messages_from_cache(to_delete=predicate, lower_limit=first_limit) + message_mappings, message_ids = self._get_messages_from_cache( + channels=deletion_channels, to_delete=predicate, lower_limit=first_limit + ) else: log.trace(f"Messages for cleaning by {ctx.author.id} will be searched in channel histories.") - deletion_channels = channels - if channels == "*": - deletion_channels = [channel for channel in ctx.guild.channels if isinstance(channel, TextChannel)] message_mappings, message_ids = await self._get_messages_from_channels( channels=deletion_channels, to_delete=predicate, @@ -406,6 +420,8 @@ class Clean(Cog): deleted_messages = await self._delete_found(message_mappings) self.cleaning = False + if not channels: + channels = deletion_channels logged = await self._modlog_cleaned_messages(deleted_messages, channels, ctx) if logged and is_mod_channel(ctx.channel): -- cgit v1.2.3 From a738d05c3f46969a091b1ba2b0eaa14fcd00644a Mon Sep 17 00:00:00 2001 From: mbaruh Date: Mon, 6 Dec 2021 00:11:28 +0200 Subject: Skip private channels when deleting from all When specifying all channels, the command now skips private channels to optimize for speed. --- bot/exts/moderation/clean.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index 0b83fc7e0..e61ef7880 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -31,12 +31,12 @@ CleanLimit = Union[Message, Age, ISODateTime] class CleanChannels(Converter): - """A converter that turns the given string to a list of channels to clean, or the literal `*` for all channels.""" + """A converter to turn the string into a list of channels to clean, or the literal `*` for all public channels.""" _channel_converter = TextChannelConverter() async def convert(self, ctx: Context, argument: str) -> Union[Literal["*"], list[TextChannel]]: - """Converts a string to a list of channels to clean, or the literal `*` for all channels.""" + """Converts a string to a list of channels to clean, or the literal `*` for all public channels.""" if argument == "*": return "*" return [await self._channel_converter.convert(ctx, channel) for channel in argument.split()] @@ -129,7 +129,12 @@ class Clean(Cog): channels = {ctx.channel} else: if channels == "*": - channels = {channel for channel in ctx.guild.channels if isinstance(channel, TextChannel)} + channels = { + channel for channel in ctx.guild.channels + if isinstance(channel, TextChannel) + # Assume that non-public channels are not needed to optimize for speed. + and channel.permissions_for(ctx.guild.default_role).view_channel + } else: channels = set(channels) @@ -339,7 +344,7 @@ class Clean(Cog): # Build the embed and send it if channels == "*": - target_channels = "all channels" + target_channels = "all public channels" else: target_channels = ", ".join(channel.mention for channel in channels) @@ -456,7 +461,7 @@ class Clean(Cog): The pattern must be provided enclosed in backticks. If the pattern contains spaces, it still needs to be enclosed in double quotes on top of that. \u2003• `bots_only`: Whether to delete only bots. If specified, users cannot be specified. - \u2003• `channels`: A series of channels to delete in, or an asterisk to delete from all channels. + \u2003• `channels`: A series of channels to delete in, or an asterisk to delete from all public channels. """ if not any([users, first_limit, second_limit, regex, channels]): await ctx.send_help(ctx.command) -- cgit v1.2.3 From af534ce297f68aedf6aa5a59f82a539c6cbd8686 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Sun, 5 Dec 2021 19:12:10 -0500 Subject: fix: parse whitespace out of pep titles --- bot/exts/info/pep.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/pep.py b/bot/exts/info/pep.py index 8c0db18bc..67866620b 100644 --- a/bot/exts/info/pep.py +++ b/bot/exts/info/pep.py @@ -97,9 +97,12 @@ class PythonEnhancementProposals(Cog): def generate_pep_embed(self, pep_header: Dict, pep_nr: int) -> Embed: """Generate PEP embed based on PEP headers data.""" + # the parsed header can be wrapped to multiple lines, so we need to make sure that is removed + # for an example of a pep with this issue, see pep 500 + title = " ".join(pep_header["Title"].split()) # Assemble the embed pep_embed = Embed( - title=f"**PEP {pep_nr} - {pep_header['Title']}**", + title=f"**PEP {pep_nr} - {title}**", description=f"[Link]({BASE_PEP_URL}{pep_nr:04})", ) -- cgit v1.2.3 From aa08fe2258ce4205272c7f27e1e2380c37275552 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 18 Oct 2021 22:22:47 +0100 Subject: Normalise names before checking for matches --- bot/exts/filters/filtering.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 79b7abe9f..e51d2aad6 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -2,6 +2,7 @@ import asyncio import re from datetime import timedelta from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union +from unicodedata import normalize import arrow import dateutil.parser @@ -207,12 +208,19 @@ class Filtering(Cog): def get_name_matches(self, name: str) -> List[re.Match]: """Check bad words from passed string (name). Return list of matches.""" - name = self.clean_input(name) + normalised_name = normalize("NFKC", name) matches = [] + + # Run filters against normalized and original version, + # in case we have filters for one but not the other. + names_to_check = (name, normalised_name) + watchlist_patterns = self._get_filterlist_items('filter_token', allowed=False) for pattern in watchlist_patterns: - if match := re.search(pattern, name, flags=re.IGNORECASE): - matches.append(match) + for name in names_to_check: + if match := re.search(pattern, name, flags=re.IGNORECASE): + matches.append(match) + break # No need to see if other variations of this name match too. return matches async def check_send_alert(self, member: Member) -> bool: -- cgit v1.2.3 From baf8239be8c6a4f6da4bd7ce8f8b2abeaf55e58a Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 18 Oct 2021 22:51:31 +0100 Subject: Check if we recently alerted about a bad name before running all filter tokens again --- bot/exts/filters/filtering.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index e51d2aad6..4b1de9638 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -237,10 +237,14 @@ class Filtering(Cog): """Send a mod alert every 3 days if a username still matches a watchlist pattern.""" # Use lock to avoid race conditions async with self.name_lock: + # Check if we recently alerted about this user first, + # to avoid running all the filter tokens against their name again. + if not await self.check_send_alert(member): + return + # Check whether the users display name contains any words in our blacklist matches = self.get_name_matches(member.display_name) - - if not matches or not await self.check_send_alert(member): + if not matches: return log.info(f"Sending bad nickname alert for '{member.display_name}' ({member.id}).") -- cgit v1.2.3 From 8efbff61aa9a8697ddb140fa5978630a6c609054 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 18 Oct 2021 22:56:22 +0100 Subject: Return early when getting name matches Ss soon as we get a match for a bad name, return it, rather than running it against the rest of the filters. --- bot/exts/filters/filtering.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 4b1de9638..fb1d62e48 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -206,10 +206,9 @@ class Filtering(Cog): delta = relativedelta(after.edited_at, before.edited_at).microseconds await self._filter_message(after, delta) - def get_name_matches(self, name: str) -> List[re.Match]: - """Check bad words from passed string (name). Return list of matches.""" + def get_name_match(self, name: str) -> Optional[re.Match]: + """Check bad words from passed string (name). Return the first match found.""" normalised_name = normalize("NFKC", name) - matches = [] # Run filters against normalized and original version, # in case we have filters for one but not the other. @@ -219,9 +218,8 @@ class Filtering(Cog): for pattern in watchlist_patterns: for name in names_to_check: if match := re.search(pattern, name, flags=re.IGNORECASE): - matches.append(match) - break # No need to see if other variations of this name match too. - return matches + return match + return None async def check_send_alert(self, member: Member) -> bool: """When there is less than 3 days after last alert, return `False`, otherwise `True`.""" @@ -243,8 +241,8 @@ class Filtering(Cog): return # Check whether the users display name contains any words in our blacklist - matches = self.get_name_matches(member.display_name) - if not matches: + match = self.get_name_match(member.display_name) + if not match: return log.info(f"Sending bad nickname alert for '{member.display_name}' ({member.id}).") @@ -252,7 +250,7 @@ class Filtering(Cog): log_string = ( f"**User:** {format_user(member)}\n" f"**Display Name:** {escape_markdown(member.display_name)}\n" - f"**Bad Matches:** {', '.join(match.group() for match in matches)}" + f"**Bad Match:** {match.group()}" ) await self.mod_log.send_log_message( -- cgit v1.2.3 From 5901ac0ba4544f2bd479a74d5d6a345b3d31cb01 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 19 Oct 2021 17:00:30 +0100 Subject: Also run name filters against a cleaned version of the normalised name --- bot/exts/filters/filtering.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index fb1d62e48..21ed090ea 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -1,8 +1,8 @@ import asyncio import re +import unicodedata from datetime import timedelta from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union -from unicodedata import normalize import arrow import dateutil.parser @@ -208,11 +208,12 @@ class Filtering(Cog): def get_name_match(self, name: str) -> Optional[re.Match]: """Check bad words from passed string (name). Return the first match found.""" - normalised_name = normalize("NFKC", name) + normalised_name = unicodedata.normalize("NFKC", name) + cleaned_normalised_name = "".join(c for c in normalised_name if not unicodedata.combining(c)) - # Run filters against normalized and original version, + # Run filters against normalised, cleaned normalised and the original name, # in case we have filters for one but not the other. - names_to_check = (name, normalised_name) + names_to_check = (name, normalised_name, cleaned_normalised_name) watchlist_patterns = self._get_filterlist_items('filter_token', allowed=False) for pattern in watchlist_patterns: -- cgit v1.2.3 From d0dc7a0e4e3fc6618ae49d43b24938c84793dcf0 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 6 Dec 2021 22:59:35 +0000 Subject: Build an intermediate list for speed in filtering cog --- bot/exts/filters/filtering.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 21ed090ea..8accc61f8 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -209,7 +209,7 @@ class Filtering(Cog): def get_name_match(self, name: str) -> Optional[re.Match]: """Check bad words from passed string (name). Return the first match found.""" normalised_name = unicodedata.normalize("NFKC", name) - cleaned_normalised_name = "".join(c for c in normalised_name if not unicodedata.combining(c)) + cleaned_normalised_name = "".join([c for c in normalised_name if not unicodedata.combining(c)]) # Run filters against normalised, cleaned normalised and the original name, # in case we have filters for one but not the other. -- cgit v1.2.3 From 736c0c8e38ed33e23244e9a509820b519482eec6 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 7 Dec 2021 10:13:00 +0000 Subject: Make snekbox url an env var An issue with snekbox in our cluster has meant that we want to send requests to an external service temporarily while we get this fixed. Making this an env var means we can change this whenever needed in future without leaking the external service's url. --- config-default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-default.yml b/config-default.yml index 0d3ddc005..1e04f5844 100644 --- a/config-default.yml +++ b/config-default.yml @@ -377,7 +377,7 @@ urls: site_logs_view: !JOIN [*STAFF, "/bot/logs"] # Snekbox - snekbox_eval_api: "http://snekbox.default.svc.cluster.local/eval" + snekbox_eval_api: !ENV ["SNEKBOX_EVAL_API", "http://snekbox.default.svc.cluster.local/eval"] # Discord API URLs discord_api: &DISCORD_API "https://discordapp.com/api/v7/" -- cgit v1.2.3 From e8b47826860bcfd42ffd716e671a2e81a712dc63 Mon Sep 17 00:00:00 2001 From: Johannes Christ Date: Wed, 8 Dec 2021 12:27:41 +0100 Subject: Correct typo in logline --- bot/exts/moderation/modpings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index 47bc5e283..c44a16ff6 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -93,7 +93,7 @@ class ModPings(Cog): async def remove_role_schedule(self, mod: Member, work_time: int, schedule_start: datetime.datetime) -> None: """Removes the moderator's role to the given moderator.""" - log.trace(f"Removing moderator role to mod with ID {mod.id}") + log.trace(f"Removing moderator role from mod with ID {mod.id}") await mod.remove_roles(self.moderators_role, reason="Moderator schedule time expired.") # Remove the task before scheduling it again -- cgit v1.2.3 From c72d4944690e374e9ef396cae91094e2464e3f04 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 19 Apr 2021 22:08:29 +0200 Subject: Move the rules command to the Information cog --- bot/exts/info/information.py | 38 +++++++++++++++++++++++++++++++++++++- bot/exts/info/site.py | 37 +------------------------------------ 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 5b48495dc..a5e700678 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -2,16 +2,18 @@ import colorsys import pprint import textwrap from collections import defaultdict +from textwrap import shorten from typing import Any, DefaultDict, Mapping, Optional, Tuple, Union import rapidfuzz from discord import AllowedMentions, Colour, Embed, Guild, Message, Role -from discord.ext.commands import BucketType, Cog, Context, Paginator, command, group, has_any_role +from discord.ext.commands import BucketType, Cog, Context, Greedy, Paginator, command, group, has_any_role from discord.utils import escape_markdown from bot import constants from bot.api import ResponseCodeError from bot.bot import Bot +from bot.constants import URLs from bot.converters import MemberOrUser from bot.decorators import in_whitelist from bot.errors import NonExistentRoleError @@ -523,6 +525,40 @@ class Information(Cog): """Shows information about the raw API response in a copy-pasteable Python format.""" await self.send_raw_content(ctx, message, json=True) + @command(aliases=("rule",)) + async def rules(self, ctx: Context, rules: Greedy[int]) -> None: + """Provides a link to all rules or, if specified, displays specific rule(s).""" + rules_embed = Embed(title="Rules", color=Colour.og_blurple(), url=f"{URLs.site_schema}{URLs.site}/pages/rules") + + if not rules: + # Rules were not submitted. Return the default description. + rules_embed.description = ( + f"The rules and guidelines that apply to this community can be found on" + f" our [rules page]({URLs.site_schema}{URLs.site}/pages/rules). We expect" + f" all members of the community to have read and understood these." + ) + + await ctx.send(embed=rules_embed) + return + + full_rules = await self.bot.api_client.get("rules", params={"link_format": "md"}) + + # Remove duplicates and sort the rule indices + rules = sorted(set(rules)) + + invalid = ", ".join(str(index) for index in rules if index < 1 or index > len(full_rules)) + + if invalid: + await ctx.send(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=" ...")) + return + + for rule in rules: + self.bot.stats.incr(f"rule_uses.{rule}") + + final_rules = tuple(f"**{pick}.** {full_rules[pick - 1]}" for pick in rules) + + await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) + def setup(bot: Bot) -> None: """Load the Information cog.""" diff --git a/bot/exts/info/site.py b/bot/exts/info/site.py index f6499ecce..665bff3a8 100644 --- a/bot/exts/info/site.py +++ b/bot/exts/info/site.py @@ -1,12 +1,11 @@ from textwrap import shorten from discord import Colour, Embed -from discord.ext.commands import Cog, Context, Greedy, group +from discord.ext.commands import Cog, Context, group from bot.bot import Bot from bot.constants import URLs from bot.log import get_logger -from bot.pagination import LinePaginator log = get_logger(__name__) @@ -105,40 +104,6 @@ class Site(Cog): await ctx.send(embed=embed) - @site_group.command(name="rules", aliases=("r", "rule"), root_aliases=("rules", "rule")) - async def site_rules(self, ctx: Context, rules: Greedy[int]) -> None: - """Provides a link to all rules or, if specified, displays specific rule(s).""" - rules_embed = Embed(title='Rules', color=Colour.og_blurple(), url=f'{BASE_URL}/pages/rules') - - if not rules: - # Rules were not submitted. Return the default description. - rules_embed.description = ( - "The rules and guidelines that apply to this community can be found on" - f" our [rules page]({BASE_URL}/pages/rules). We expect" - " all members of the community to have read and understood these." - ) - - await ctx.send(embed=rules_embed) - return - - full_rules = await self.bot.api_client.get('rules', params={'link_format': 'md'}) - - # Remove duplicates and sort the rule indices - rules = sorted(set(rules)) - - invalid = ', '.join(str(index) for index in rules if index < 1 or index > len(full_rules)) - - if invalid: - await ctx.send(shorten(":x: Invalid rule indices: " + invalid, 75, placeholder=' ...')) - return - - for rule in rules: - self.bot.stats.incr(f"rule_uses.{rule}") - - final_rules = tuple(f"**{pick}.** {full_rules[pick - 1]}" for pick in rules) - - await LinePaginator.paginate(final_rules, ctx, rules_embed, max_lines=3) - def setup(bot: Bot) -> None: """Load the Site cog.""" -- cgit v1.2.3 From aa5485b5d91b1b1568a89c58853343a461e610f9 Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 19 Apr 2021 22:08:46 +0200 Subject: Remove the site help command --- bot/exts/info/site.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/bot/exts/info/site.py b/bot/exts/info/site.py index 665bff3a8..4b0b7649d 100644 --- a/bot/exts/info/site.py +++ b/bot/exts/info/site.py @@ -71,22 +71,6 @@ class Site(Cog): await ctx.send(embed=embed) - @site_group.command(name="help") - async def site_help(self, ctx: Context) -> None: - """Info about the site's Getting Help page.""" - url = f"{BASE_URL}/pages/guides/pydis-guides/asking-good-questions/" - - embed = Embed(title="Asking Good Questions") - embed.set_footer(text=url) - embed.colour = Colour.og_blurple() - embed.description = ( - "Asking the right question about something that's new to you can sometimes be tricky. " - f"To help with this, we've created a [guide to asking good questions]({url}) on our website. " - "It contains everything you need to get the very best help from our community." - ) - - await ctx.send(embed=embed) - @site_group.command(name="faq", root_aliases=("faq",)) async def site_faq(self, ctx: Context) -> None: """Info about the site's FAQ page.""" -- cgit v1.2.3 From fc84bcfb3ae749248e47aa805a40558ac678647b Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Mon, 19 Apr 2021 22:18:41 +0200 Subject: Move static content site commands to tags and remove the site cog --- bot/exts/info/site.py | 94 ----------------------------------------- bot/resources/tags/faq.md | 6 +++ bot/resources/tags/resources.md | 6 +++ bot/resources/tags/site.md | 6 +++ bot/resources/tags/tools.md | 6 +++ 5 files changed, 24 insertions(+), 94 deletions(-) delete mode 100644 bot/exts/info/site.py create mode 100644 bot/resources/tags/faq.md create mode 100644 bot/resources/tags/resources.md create mode 100644 bot/resources/tags/site.md create mode 100644 bot/resources/tags/tools.md diff --git a/bot/exts/info/site.py b/bot/exts/info/site.py deleted file mode 100644 index 4b0b7649d..000000000 --- a/bot/exts/info/site.py +++ /dev/null @@ -1,94 +0,0 @@ -from textwrap import shorten - -from discord import Colour, Embed -from discord.ext.commands import Cog, Context, group - -from bot.bot import Bot -from bot.constants import URLs -from bot.log import get_logger - -log = get_logger(__name__) - -BASE_URL = f"{URLs.site_schema}{URLs.site}" - - -class Site(Cog): - """Commands for linking to different parts of the site.""" - - def __init__(self, bot: Bot): - self.bot = bot - - @group(name="site", aliases=("s",), invoke_without_command=True) - async def site_group(self, ctx: Context) -> None: - """Commands for getting info about our website.""" - await ctx.send_help(ctx.command) - - @site_group.command(name="home", aliases=("about",), root_aliases=("home",)) - async def site_main(self, ctx: Context) -> None: - """Info about the website itself.""" - url = f"{URLs.site_schema}{URLs.site}/" - - embed = Embed(title="Python Discord website") - embed.set_footer(text=url) - embed.colour = Colour.og_blurple() - embed.description = ( - f"[Our official website]({url}) is an open-source community project " - "created with Python and Django. It contains information about the server " - "itself, lets you sign up for upcoming events, has its own wiki, contains " - "a list of valuable learning resources, and much more." - ) - - await ctx.send(embed=embed) - - @site_group.command(name="resources", root_aliases=("resources", "resource")) - async def site_resources(self, ctx: Context) -> None: - """Info about the site's Resources page.""" - learning_url = f"{BASE_URL}/resources" - - embed = Embed(title="Resources") - embed.set_footer(text=f"{learning_url}") - embed.colour = Colour.og_blurple() - embed.description = ( - f"The [Resources page]({learning_url}) on our website contains a " - "list of hand-selected learning resources that we regularly recommend " - f"to both beginners and experts." - ) - - await ctx.send(embed=embed) - - @site_group.command(name="tools", root_aliases=("tools",)) - async def site_tools(self, ctx: Context) -> None: - """Info about the site's Tools page.""" - tools_url = f"{BASE_URL}/resources/tools" - - embed = Embed(title="Tools") - embed.set_footer(text=f"{tools_url}") - embed.colour = Colour.og_blurple() - embed.description = ( - f"The [Tools page]({tools_url}) on our website contains a " - f"couple of the most popular tools for programming in Python." - ) - - await ctx.send(embed=embed) - - @site_group.command(name="faq", root_aliases=("faq",)) - async def site_faq(self, ctx: Context) -> None: - """Info about the site's FAQ page.""" - url = f"{BASE_URL}/pages/frequently-asked-questions" - - embed = Embed(title="FAQ") - embed.set_footer(text=url) - embed.colour = Colour.og_blurple() - embed.description = ( - "As the largest Python community on Discord, we get hundreds of questions every day. " - "Many of these questions have been asked before. We've compiled a list of the most " - "frequently asked questions along with their answers, which can be found on " - f"our [FAQ page]({url})." - ) - - await ctx.send(embed=embed) - - -def setup(bot: Bot) -> None: - """Load the Site cog.""" - bot.add_cog(Site(bot)) diff --git a/bot/resources/tags/faq.md b/bot/resources/tags/faq.md new file mode 100644 index 000000000..e1c57b3a0 --- /dev/null +++ b/bot/resources/tags/faq.md @@ -0,0 +1,6 @@ +--- +embed: + title: "Frequently asked questions" +--- + +As the largest Python community on Discord, we get hundreds of questions every day. Many of these questions have been asked before. We've compiled a list of the most frequently asked questions along with their answers, which can be found on our [FAQ page](https://www.pythondiscord.com/pages/frequently-asked-questions/). diff --git a/bot/resources/tags/resources.md b/bot/resources/tags/resources.md new file mode 100644 index 000000000..201e0eb1e --- /dev/null +++ b/bot/resources/tags/resources.md @@ -0,0 +1,6 @@ +--- +embed: + title: "Resources" +--- + +The [Resources page](https://www.pythondiscord.com/resources/) on our website contains a list of hand-selected learning resources that we regularly recommend to both beginners and experts. diff --git a/bot/resources/tags/site.md b/bot/resources/tags/site.md new file mode 100644 index 000000000..376f84742 --- /dev/null +++ b/bot/resources/tags/site.md @@ -0,0 +1,6 @@ +--- +embed: + title: "Python Discord Website" +--- + +[Our official website](https://www.pythondiscord.com/) is an open-source community project created with Python and Django. It contains information about the server itself, lets you sign up for upcoming events, has its own wiki, contains a list of valuable learning resources, and much more. diff --git a/bot/resources/tags/tools.md b/bot/resources/tags/tools.md new file mode 100644 index 000000000..3cae75552 --- /dev/null +++ b/bot/resources/tags/tools.md @@ -0,0 +1,6 @@ +--- +embed: + title: "Tools" +--- + +The [Tools page](https://www.pythondiscord.com/resources/tools/) on our website contains a couple of the most popular tools for programming in Python. -- cgit v1.2.3 From 1f2b60364d04ad8e1a21a9ee8b33704a1845f12d Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Wed, 8 Dec 2021 16:14:50 +0100 Subject: Use hardcoded rules url instead of constructing it from consts Discord does validation on the embed url which may fail for valid local urls --- bot/exts/info/information.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index a5e700678..fa22a4fe9 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -13,7 +13,6 @@ from discord.utils import escape_markdown from bot import constants from bot.api import ResponseCodeError from bot.bot import Bot -from bot.constants import URLs from bot.converters import MemberOrUser from bot.decorators import in_whitelist from bot.errors import NonExistentRoleError @@ -528,14 +527,14 @@ class Information(Cog): @command(aliases=("rule",)) async def rules(self, ctx: Context, rules: Greedy[int]) -> None: """Provides a link to all rules or, if specified, displays specific rule(s).""" - rules_embed = Embed(title="Rules", color=Colour.og_blurple(), url=f"{URLs.site_schema}{URLs.site}/pages/rules") + rules_embed = Embed(title="Rules", color=Colour.og_blurple(), url="https://www.pythondiscord.com/pages/rules") if not rules: # Rules were not submitted. Return the default description. rules_embed.description = ( - f"The rules and guidelines that apply to this community can be found on" - f" our [rules page]({URLs.site_schema}{URLs.site}/pages/rules). We expect" - f" all members of the community to have read and understood these." + "The rules and guidelines that apply to this community can be found on" + " our [rules page](https://www.pythondiscord.com/pages/rules). We expect" + " all members of the community to have read and understood these." ) await ctx.send(embed=rules_embed) -- cgit v1.2.3 From a55ed25126149d0dd092287c6e41415419f4927c Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 8 Dec 2021 17:21:36 +0000 Subject: Reduce threshold before fuzzy matching to 2 Commands such as !ot, !if, !xy are commonly used as shortcuts to their respective tags. We recently upped the threshold before fuzzy matching to 3 characters, which broke these shortcuts. This commit reduces that threshold down to 2, so users who are used to those commands can still use them. --- bot/exts/info/tags.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index 7c8d378a9..e5930a433 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -168,11 +168,11 @@ class Tags(Cog): """Get tags with identifiers similar to `tag_identifier`.""" suggestions = [] - if tag_identifier.group is not None and len(tag_identifier.group) >= 3: + if tag_identifier.group is not None and len(tag_identifier.group) >= 2: # Try fuzzy matching with only a name first suggestions += self._get_suggestions(TagIdentifier(None, tag_identifier.group)) - if len(tag_identifier.name) >= 3: + if len(tag_identifier.name) >= 2: suggestions += self._get_suggestions(tag_identifier) return suggestions -- cgit v1.2.3 From 0eca9ee39feb8b6a9038de23a69af1ce9a13785c Mon Sep 17 00:00:00 2001 From: PH-KDX <50588793+PH-KDX@users.noreply.github.com> Date: Thu, 9 Dec 2021 20:51:34 +0100 Subject: Remove deprecated server voice region Discord's current model for voice regions is setting it per server. Hence, the "Voice region" section in the server info tag will always display as "deprecated". This pull request removes it. --- bot/exts/info/information.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index fa22a4fe9..73357211e 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -174,7 +174,6 @@ class Information(Cog): embed = Embed(colour=Colour.og_blurple(), title="Server Information") created = discord_timestamp(ctx.guild.created_at, TimestampFormats.RELATIVE) - region = ctx.guild.region num_roles = len(ctx.guild.roles) - 1 # Exclude @everyone # Server Features are only useful in certain channels @@ -199,7 +198,6 @@ class Information(Cog): embed.description = ( f"Created: {created}" - f"\nVoice region: {region}" f"{features}" f"\nRoles: {num_roles}" f"\nMember status: {member_status}" -- cgit v1.2.3 From 850db8933713f80bc0879d5e398e1ba496b827f9 Mon Sep 17 00:00:00 2001 From: Matteo Bertucci Date: Fri, 10 Dec 2021 13:39:15 +0100 Subject: Remove myself from the code ownership --- .github/CODEOWNERS | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6dfe7e859..ea69f7677 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,10 +4,10 @@ **/bot/exts/moderation/*silence.py @MarkKoz bot/exts/info/codeblock/** @MarkKoz bot/exts/utils/extensions.py @MarkKoz -bot/exts/utils/snekbox.py @MarkKoz @Akarys42 @jb3 -bot/exts/help_channels/** @MarkKoz @Akarys42 -bot/exts/moderation/** @Akarys42 @mbaruh @Den4200 @ks129 @jb3 -bot/exts/info/** @Akarys42 @Den4200 @jb3 +bot/exts/utils/snekbox.py @MarkKoz @jb3 +bot/exts/help_channels/** @MarkKoz +bot/exts/moderation/** @mbaruh @Den4200 @ks129 @jb3 +bot/exts/info/** @Den4200 @jb3 bot/exts/info/information.py @mbaruh @jb3 bot/exts/filters/** @mbaruh @jb3 bot/exts/fun/** @ks129 @@ -21,22 +21,16 @@ bot/rules/** @mbaruh bot/utils/extensions.py @MarkKoz bot/utils/function.py @MarkKoz bot/utils/lock.py @MarkKoz -bot/utils/regex.py @Akarys42 bot/utils/scheduling.py @MarkKoz # Tests tests/_autospec.py @MarkKoz tests/bot/exts/test_cogs.py @MarkKoz -tests/** @Akarys42 # CI & Docker -.github/workflows/** @MarkKoz @Akarys42 @SebastiaanZ @Den4200 @jb3 -Dockerfile @MarkKoz @Akarys42 @Den4200 @jb3 -docker-compose.yml @MarkKoz @Akarys42 @Den4200 @jb3 - -# Tools -poetry.lock @Akarys42 -pyproject.toml @Akarys42 +.github/workflows/** @MarkKoz @SebastiaanZ @Den4200 @jb3 +Dockerfile @MarkKoz @Den4200 @jb3 +docker-compose.yml @MarkKoz @Den4200 @jb3 # Statistics bot/async_stats.py @jb3 -- cgit v1.2.3 From d36045993bb01ca044acc3dc090da8771bcb1d05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Dec 2021 21:01:14 +0000 Subject: Bump lxml from 4.6.3 to 4.6.5 Bumps [lxml](https://github.com/lxml/lxml) from 4.6.3 to 4.6.5. - [Release notes](https://github.com/lxml/lxml/releases) - [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt) - [Commits](https://github.com/lxml/lxml/compare/lxml-4.6.3...lxml-4.6.5) --- updated-dependencies: - dependency-name: lxml dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- poetry.lock | 113 ++++++++++++++++++++++++++++++++------------------------- pyproject.toml | 2 +- 2 files changed, 64 insertions(+), 51 deletions(-) diff --git a/poetry.lock b/poetry.lock index d91941d45..4155a57ff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -281,6 +281,7 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] [package.source] type = "url" url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" + [[package]] name = "distlib" version = "0.3.3" @@ -533,7 +534,7 @@ plugins = ["setuptools"] [[package]] name = "lxml" -version = "4.6.3" +version = "4.6.5" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -1114,7 +1115,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "da321f13297501e62dd1eb362eccb586ea1a9c21ddb395e11a91b93a2f92e9d4" +content-hash = "e6fe15a64ae57232a639149df793d6580a93f613425cae85c9892cf959710430" [metadata.files] aio-pika = [ @@ -1464,54 +1465,66 @@ isort = [ {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, ] lxml = [ - {file = "lxml-4.6.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:df7c53783a46febb0e70f6b05df2ba104610f2fb0d27023409734a3ecbb78fb2"}, - {file = "lxml-4.6.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:1b7584d421d254ab86d4f0b13ec662a9014397678a7c4265a02a6d7c2b18a75f"}, - {file = "lxml-4.6.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:079f3ae844f38982d156efce585bc540c16a926d4436712cf4baee0cce487a3d"}, - {file = "lxml-4.6.3-cp27-cp27m-win32.whl", hash = "sha256:bc4313cbeb0e7a416a488d72f9680fffffc645f8a838bd2193809881c67dd106"}, - {file = "lxml-4.6.3-cp27-cp27m-win_amd64.whl", hash = "sha256:8157dadbb09a34a6bd95a50690595e1fa0af1a99445e2744110e3dca7831c4ee"}, - {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7728e05c35412ba36d3e9795ae8995e3c86958179c9770e65558ec3fdfd3724f"}, - {file = "lxml-4.6.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:4bff24dfeea62f2e56f5bab929b4428ae6caba2d1eea0c2d6eb618e30a71e6d4"}, - {file = "lxml-4.6.3-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:64812391546a18896adaa86c77c59a4998f33c24788cadc35789e55b727a37f4"}, - {file = "lxml-4.6.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c1a40c06fd5ba37ad39caa0b3144eb3772e813b5fb5b084198a985431c2f1e8d"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:74f7d8d439b18fa4c385f3f5dfd11144bb87c1da034a466c5b5577d23a1d9b51"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f90ba11136bfdd25cae3951af8da2e95121c9b9b93727b1b896e3fa105b2f586"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:4c61b3a0db43a1607d6264166b230438f85bfed02e8cff20c22e564d0faff354"}, - {file = "lxml-4.6.3-cp35-cp35m-manylinux2014_x86_64.whl", hash = "sha256:5c8c163396cc0df3fd151b927e74f6e4acd67160d6c33304e805b84293351d16"}, - {file = "lxml-4.6.3-cp35-cp35m-win32.whl", hash = "sha256:f2380a6376dfa090227b663f9678150ef27543483055cc327555fb592c5967e2"}, - {file = "lxml-4.6.3-cp35-cp35m-win_amd64.whl", hash = "sha256:c4f05c5a7c49d2fb70223d0d5bcfbe474cf928310ac9fa6a7c6dddc831d0b1d4"}, - {file = "lxml-4.6.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d2e35d7bf1c1ac8c538f88d26b396e73dd81440d59c1ef8522e1ea77b345ede4"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:289e9ca1a9287f08daaf796d96e06cb2bc2958891d7911ac7cae1c5f9e1e0ee3"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:bccbfc27563652de7dc9bdc595cb25e90b59c5f8e23e806ed0fd623755b6565d"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d916d31fd85b2f78c76400d625076d9124de3e4bda8b016d25a050cc7d603f24"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:820628b7b3135403540202e60551e741f9b6d3304371712521be939470b454ec"}, - {file = "lxml-4.6.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:c47ff7e0a36d4efac9fd692cfa33fbd0636674c102e9e8d9b26e1b93a94e7617"}, - {file = "lxml-4.6.3-cp36-cp36m-win32.whl", hash = "sha256:5a0a14e264069c03e46f926be0d8919f4105c1623d620e7ec0e612a2e9bf1c04"}, - {file = "lxml-4.6.3-cp36-cp36m-win_amd64.whl", hash = "sha256:92e821e43ad382332eade6812e298dc9701c75fe289f2a2d39c7960b43d1e92a"}, - {file = "lxml-4.6.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:efd7a09678fd8b53117f6bae4fa3825e0a22b03ef0a932e070c0bdbb3a35e654"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:efac139c3f0bf4f0939f9375af4b02c5ad83a622de52d6dfa8e438e8e01d0eb0"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:0fbcf5565ac01dff87cbfc0ff323515c823081c5777a9fc7703ff58388c258c3"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:36108c73739985979bf302006527cf8a20515ce444ba916281d1c43938b8bb96"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:122fba10466c7bd4178b07dba427aa516286b846b2cbd6f6169141917283aae2"}, - {file = "lxml-4.6.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:cdaf11d2bd275bf391b5308f86731e5194a21af45fbaaaf1d9e8147b9160ea92"}, - {file = "lxml-4.6.3-cp37-cp37m-win32.whl", hash = "sha256:3439c71103ef0e904ea0a1901611863e51f50b5cd5e8654a151740fde5e1cade"}, - {file = "lxml-4.6.3-cp37-cp37m-win_amd64.whl", hash = "sha256:4289728b5e2000a4ad4ab8da6e1db2e093c63c08bdc0414799ee776a3f78da4b"}, - {file = "lxml-4.6.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b007cbb845b28db4fb8b6a5cdcbf65bacb16a8bd328b53cbc0698688a68e1caa"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:76fa7b1362d19f8fbd3e75fe2fb7c79359b0af8747e6f7141c338f0bee2f871a"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:26e761ab5b07adf5f555ee82fb4bfc35bf93750499c6c7614bd64d12aaa67927"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:e1cbd3f19a61e27e011e02f9600837b921ac661f0c40560eefb366e4e4fb275e"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:66e575c62792c3f9ca47cb8b6fab9e35bab91360c783d1606f758761810c9791"}, - {file = "lxml-4.6.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:1b38116b6e628118dea5b2186ee6820ab138dbb1e24a13e478490c7db2f326ae"}, - {file = "lxml-4.6.3-cp38-cp38-win32.whl", hash = "sha256:89b8b22a5ff72d89d48d0e62abb14340d9e99fd637d046c27b8b257a01ffbe28"}, - {file = "lxml-4.6.3-cp38-cp38-win_amd64.whl", hash = "sha256:2a9d50e69aac3ebee695424f7dbd7b8c6d6eb7de2a2eb6b0f6c7db6aa41e02b7"}, - {file = "lxml-4.6.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce256aaa50f6cc9a649c51be3cd4ff142d67295bfc4f490c9134d0f9f6d58ef0"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:7610b8c31688f0b1be0ef882889817939490a36d0ee880ea562a4e1399c447a1"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f8380c03e45cf09f8557bdaa41e1fa7c81f3ae22828e1db470ab2a6c96d8bc23"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:3082c518be8e97324390614dacd041bb1358c882d77108ca1957ba47738d9d59"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:884ab9b29feaca361f7f88d811b1eea9bfca36cf3da27768d28ad45c3ee6f969"}, - {file = "lxml-4.6.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:6f12e1427285008fd32a6025e38e977d44d6382cf28e7201ed10d6c1698d2a9a"}, - {file = "lxml-4.6.3-cp39-cp39-win32.whl", hash = "sha256:33bb934a044cf32157c12bfcfbb6649807da20aa92c062ef51903415c704704f"}, - {file = "lxml-4.6.3-cp39-cp39-win_amd64.whl", hash = "sha256:542d454665a3e277f76954418124d67516c5f88e51a900365ed54a9806122b83"}, - {file = "lxml-4.6.3.tar.gz", hash = "sha256:39b78571b3b30645ac77b95f7c69d1bffc4cf8c3b157c435a34da72e78c82468"}, + {file = "lxml-4.6.5-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2"}, + {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b"}, + {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808"}, + {file = "lxml-4.6.5-cp27-cp27m-win32.whl", hash = "sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c"}, + {file = "lxml-4.6.5-cp27-cp27m-win_amd64.whl", hash = "sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71"}, + {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93"}, + {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352"}, + {file = "lxml-4.6.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8"}, + {file = "lxml-4.6.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808"}, + {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210"}, + {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1"}, + {file = "lxml-4.6.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5"}, + {file = "lxml-4.6.5-cp310-cp310-win32.whl", hash = "sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952"}, + {file = "lxml-4.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0"}, + {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203"}, + {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4"}, + {file = "lxml-4.6.5-cp35-cp35m-win32.whl", hash = "sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b"}, + {file = "lxml-4.6.5-cp35-cp35m-win_amd64.whl", hash = "sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408"}, + {file = "lxml-4.6.5-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb"}, + {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c"}, + {file = "lxml-4.6.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3"}, + {file = "lxml-4.6.5-cp36-cp36m-win32.whl", hash = "sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04"}, + {file = "lxml-4.6.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120"}, + {file = "lxml-4.6.5-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd"}, + {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2"}, + {file = "lxml-4.6.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9"}, + {file = "lxml-4.6.5-cp37-cp37m-win32.whl", hash = "sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090"}, + {file = "lxml-4.6.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f"}, + {file = "lxml-4.6.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c"}, + {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44"}, + {file = "lxml-4.6.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9"}, + {file = "lxml-4.6.5-cp38-cp38-win32.whl", hash = "sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d"}, + {file = "lxml-4.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41"}, + {file = "lxml-4.6.5-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9"}, + {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace"}, + {file = "lxml-4.6.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542"}, + {file = "lxml-4.6.5-cp39-cp39-win32.whl", hash = "sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b"}, + {file = "lxml-4.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b"}, + {file = "lxml-4.6.5-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59"}, + {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c"}, + {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71"}, + {file = "lxml-4.6.5-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8"}, + {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"}, + {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e"}, + {file = "lxml-4.6.5.tar.gz", hash = "sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca"}, ] markdownify = [ {file = "markdownify-0.6.1-py3-none-any.whl", hash = "sha256:7489fd5c601536996a376c4afbcd1dd034db7690af807120681461e82fbc0acc"}, diff --git a/pyproject.toml b/pyproject.toml index 563bf4a27..44d09f89e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ deepdiff = "~=4.0" emoji = "~=0.6" feedparser = "~=6.0.2" rapidfuzz = "~=1.4" -lxml = "~=4.4" +lxml = "~=4.6" markdownify = "==0.6.1" more_itertools = "~=8.2" python-dateutil = "~=2.8" -- cgit v1.2.3 From 40631e98e651d6ef8c50274cb0d0b49cf693b348 Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Wed, 15 Dec 2021 07:11:46 -0700 Subject: Rename channels.discord_py to discord_bots (#1982) Co-authored-by: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/constants.py | 2 +- bot/exts/moderation/slowmode.py | 2 +- bot/exts/utils/utils.py | 2 +- config-default.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 078ab6912..1b713a7e3 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -432,7 +432,7 @@ class Channels(metaclass=YAMLGetter): black_formatter: int bot_commands: int - discord_py: int + discord_bots: int esoteric: int voice_gate: int code_jam_planning: int diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py index 9583597e0..da04d1e98 100644 --- a/bot/exts/moderation/slowmode.py +++ b/bot/exts/moderation/slowmode.py @@ -16,7 +16,7 @@ SLOWMODE_MAX_DELAY = 21600 # seconds COMMONLY_SLOWMODED_CHANNELS = { Channels.python_general: "python_general", - Channels.discord_py: "discordpy", + Channels.discord_bots: "discord_bots", Channels.off_topic_0: "ot0", } diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 821cebd8c..f76eea516 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -49,7 +49,7 @@ class Utils(Cog): self.bot = bot @command() - @in_whitelist(channels=(Channels.bot_commands, Channels.discord_py), roles=STAFF_PARTNERS_COMMUNITY_ROLES) + @in_whitelist(channels=(Channels.bot_commands, Channels.discord_bots), roles=STAFF_PARTNERS_COMMUNITY_ROLES) async def charinfo(self, ctx: Context, *, characters: str) -> None: """Shows you information on up to 50 unicode characters.""" match = re.match(r"<(a?):(\w+):(\d+)>", characters) diff --git a/config-default.yml b/config-default.yml index 1e04f5844..583733fda 100644 --- a/config-default.yml +++ b/config-default.yml @@ -174,7 +174,7 @@ guild: how_to_get_help: 704250143020417084 # Topical - discord_py: 343944376055103488 + discord_bots: 343944376055103488 # Logs attachment_log: &ATTACH_LOG 649243850006855680 -- cgit v1.2.3 From 2da8e6462c4bf8724a276858c117d5824bb684ef Mon Sep 17 00:00:00 2001 From: mina Date: Fri, 17 Dec 2021 17:39:41 -0500 Subject: Rename and reword off-topic tags Rename `off-topic` tag to `ot` and shorten description to only include mention of the less-occupied #ot2 off-topic channel. --- bot/resources/tags/off-topic.md | 10 ---------- bot/resources/tags/ot.md | 3 +++ 2 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 bot/resources/tags/off-topic.md create mode 100644 bot/resources/tags/ot.md diff --git a/bot/resources/tags/off-topic.md b/bot/resources/tags/off-topic.md deleted file mode 100644 index 287224d7f..000000000 --- a/bot/resources/tags/off-topic.md +++ /dev/null @@ -1,10 +0,0 @@ -**Off-topic channels** - -There are three off-topic channels: -• <#463035268514185226> -• <#463035241142026251> -• <#291284109232308226> - -Their names change randomly every 24 hours, but you can always find them under the `OFF-TOPIC/GENERAL` category in the channel list. - -Please read our [off-topic etiquette](https://pythondiscord.com/pages/resources/guides/off-topic-etiquette/) before participating in conversations. diff --git a/bot/resources/tags/ot.md b/bot/resources/tags/ot.md new file mode 100644 index 000000000..636e59110 --- /dev/null +++ b/bot/resources/tags/ot.md @@ -0,0 +1,3 @@ +**Off-topic channel:** <#463035268514185226> + +Please read our [off-topic etiquette](https://pythondiscord.com/pages/resources/guides/off-topic-etiquette/) before participating in conversations. -- cgit v1.2.3 From 842bfe63df4c0272b5739b1ae3850a667d3a1b7f Mon Sep 17 00:00:00 2001 From: mina Date: Fri, 17 Dec 2021 17:55:08 -0500 Subject: Create new tag explaining off-topic channels The new `off-topic-names` tag lists all off-topic channels (in their original order: 0, 1, 2), includes an explanation of the nightly channel name change, and links to the off-topic etiquette guide. --- bot/resources/tags/off-topic-names.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 bot/resources/tags/off-topic-names.md diff --git a/bot/resources/tags/off-topic-names.md b/bot/resources/tags/off-topic-names.md new file mode 100644 index 000000000..1570dc8fd --- /dev/null +++ b/bot/resources/tags/off-topic-names.md @@ -0,0 +1,10 @@ +**Off-topic channels** + +There are three off-topic channels: +• <#291284109232308226> +• <#463035241142026251> +• <#463035268514185226> + +The channel names change every night at midnight UTC and are fun meta references to jokes or conversations that happened on the server. + +See our [off-topic etiquette](https://pythondiscord.com/pages/resources/guides/off-topic-etiquette/) page for more guidance on how the channels should be used. -- cgit v1.2.3 From ecea76d5fc7384573c95c03787ec35bf6321da15 Mon Sep 17 00:00:00 2001 From: mina <75038675+minalike@users.noreply.github.com> Date: Fri, 17 Dec 2021 23:49:40 -0500 Subject: Adjust wording to embed content Co-authored-by: dawn <78233879+dawnofmidnight@users.noreply.github.com> --- bot/resources/tags/off-topic-names.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/off-topic-names.md b/bot/resources/tags/off-topic-names.md index 1570dc8fd..5d0614aaa 100644 --- a/bot/resources/tags/off-topic-names.md +++ b/bot/resources/tags/off-topic-names.md @@ -5,6 +5,6 @@ There are three off-topic channels: • <#463035241142026251> • <#463035268514185226> -The channel names change every night at midnight UTC and are fun meta references to jokes or conversations that happened on the server. +The channel names change every night at midnight UTC and are often fun meta references to jokes or conversations that happened on the server. See our [off-topic etiquette](https://pythondiscord.com/pages/resources/guides/off-topic-etiquette/) page for more guidance on how the channels should be used. -- cgit v1.2.3 From 5c8adb16e868a67d896b94e1e05132fbc5cc080a Mon Sep 17 00:00:00 2001 From: Numerlor <25886452+Numerlor@users.noreply.github.com> Date: Sat, 18 Dec 2021 22:28:26 +0100 Subject: Attempt a name only exact match if a tag with a group is searched --- bot/exts/info/tags.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index e5930a433..f66237c8e 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -275,6 +275,11 @@ class Tags(Cog): ] tag = self.tags.get(tag_identifier) + + if tag is None and tag_identifier.group is not None: + # Try exact match with only the name + tag = self.tags.get(TagIdentifier(None, tag_identifier.group)) + if tag is None and len(filtered_tags) == 1: tag_identifier = filtered_tags[0][0] tag = filtered_tags[0][1] -- cgit v1.2.3 From 4c3186401d8e19fa1edf9f1c7853dc816335e070 Mon Sep 17 00:00:00 2001 From: onerandomusername Date: Tue, 21 Dec 2021 14:40:46 -0500 Subject: fix: pass required argument closes GH-2024 --- bot/exts/info/doc/_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/doc/_cog.py b/bot/exts/info/doc/_cog.py index ebf5f5932..4dc5276d9 100644 --- a/bot/exts/info/doc/_cog.py +++ b/bot/exts/info/doc/_cog.py @@ -464,7 +464,7 @@ class DocCog(commands.Cog): ) -> None: """Clear the persistent redis cache for `package`.""" if await doc_cache.delete(package_name): - await self.item_fetcher.stale_inventory_notifier.symbol_counter.delete() + await self.item_fetcher.stale_inventory_notifier.symbol_counter.delete(package_name) await ctx.send(f"Successfully cleared the cache for `{package_name}`.") else: await ctx.send("No keys matching the package found.") -- cgit v1.2.3 From 15a846cd14feda8f1e114bbef37b282051882dfe Mon Sep 17 00:00:00 2001 From: Izan Date: Sun, 26 Dec 2021 01:12:44 +0000 Subject: Add missing `bot` parameter to call --- bot/exts/moderation/infraction/_scheduler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 52dd79791..e25acdbba 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -170,7 +170,9 @@ class InfractionScheduler: dm_log_text = "\nDM: **Failed**" # Accordingly update whether the user was successfully notified via DM. - if await _utils.notify_infraction(user, infr_type.replace("_", " ").title(), expiry, user_reason, icon): + if await _utils.notify_infraction( + ctx.bot, user, infr_type.replace("_", " ").title(), expiry, user_reason, icon + ): dm_result = ":incoming_envelope: " dm_log_text = "\nDM: Sent" -- cgit v1.2.3 From 89743e9db148d31891a4aa65bd1c9cb35eb67f4f Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Sun, 26 Dec 2021 01:37:28 +0000 Subject: Add missing infraction id parameter & change ctx.bot to self.bot (#2028) --- bot/exts/moderation/infraction/_scheduler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index e25acdbba..6f379a9a0 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -171,7 +171,7 @@ class InfractionScheduler: # Accordingly update whether the user was successfully notified via DM. if await _utils.notify_infraction( - ctx.bot, user, infr_type.replace("_", " ").title(), expiry, user_reason, icon + self.bot, user, infraction["id"], infr_type.replace("_", " ").title(), expiry, user_reason, icon ): dm_result = ":incoming_envelope: " dm_log_text = "\nDM: Sent" @@ -241,7 +241,7 @@ class InfractionScheduler: # Accordingly update whether the user was successfully notified via DM. if await _utils.notify_infraction( - ctx.bot, user, infraction["id"], infr_type.replace("_", " ").title(), expiry, user_reason, icon + self.bot, user, infraction["id"], infr_type.replace("_", " ").title(), expiry, user_reason, icon ): dm_result = ":incoming_envelope: " dm_log_text = "\nDM: Sent" -- cgit v1.2.3 From 296e5656242edda2eebe8777b631dc7cb1cd75e3 Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Sun, 17 Oct 2021 17:39:52 -0500 Subject: feat: added url parsing to filters with support for relative URLs --- bot/exts/filters/filtering.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 8accc61f8..a1362d791 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -2,6 +2,8 @@ import asyncio import re import unicodedata from datetime import timedelta +import urllib.parse +from datetime import timedelta from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union import arrow @@ -534,8 +536,14 @@ class Filtering(Cog): domain_blacklist = self._get_filterlist_items("domain_name", allowed=False) for match in URL_RE.finditer(text): for url in domain_blacklist: - if url.lower() in match.group(1).lower(): - return True, self._get_filterlist_value("domain_name", url, allowed=False)["comment"] + blacklisted_parsed = urllib.parse.urlparse(url) + url_parsed = urllib.parse.urlparse(match.group(1).lower()) + if blacklisted_parsed.netloc != "": + if url_parsed.netloc in (f"www.{blacklisted_parsed.netloc}", blacklisted_parsed.netloc): + return True, self._get_filterlist_value("domain_name", url, allowed=False)["comment"] + else: + if url_parsed.netloc in (f"www.{blacklisted_parsed.path}", blacklisted_parsed.path): + return True, self._get_filterlist_value("domain_name", url, allowed=False)["comment"] return False, None @staticmethod -- cgit v1.2.3 From 546aee92fef6b88ecfb76a6d98e11cedc02656fb Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Fri, 3 Dec 2021 02:58:37 +0000 Subject: feat: changed to tldextract --- bot/exts/filters/filtering.py | 14 +- poetry.lock | 665 ++++++++++++++++++++++++------------------ pyproject.toml | 1 + 3 files changed, 394 insertions(+), 286 deletions(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index a1362d791..ad904d147 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -2,14 +2,13 @@ import asyncio import re import unicodedata from datetime import timedelta -import urllib.parse -from datetime import timedelta from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union import arrow import dateutil.parser import discord.errors import regex +import tldextract from async_rediscache import RedisCache from dateutil.relativedelta import relativedelta from discord import Colour, HTTPException, Member, Message, NotFound, TextChannel @@ -536,13 +535,10 @@ class Filtering(Cog): domain_blacklist = self._get_filterlist_items("domain_name", allowed=False) for match in URL_RE.finditer(text): for url in domain_blacklist: - blacklisted_parsed = urllib.parse.urlparse(url) - url_parsed = urllib.parse.urlparse(match.group(1).lower()) - if blacklisted_parsed.netloc != "": - if url_parsed.netloc in (f"www.{blacklisted_parsed.netloc}", blacklisted_parsed.netloc): - return True, self._get_filterlist_value("domain_name", url, allowed=False)["comment"] - else: - if url_parsed.netloc in (f"www.{blacklisted_parsed.path}", blacklisted_parsed.path): + if url.lower() in match.group(1).lower(): + blacklisted_parsed = tldextract.extract(url.lower()) + url_parsed = tldextract.extract(match.group(1).lower()) + if blacklisted_parsed.registered_domain == url_parsed.registered_domain: return True, self._get_filterlist_value("domain_name", url, allowed=False)["comment"] return False, None diff --git a/poetry.lock b/poetry.lock index 4155a57ff..68eebf8de 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,10 @@ [[package]] name = "aio-pika" -version = "6.8.0" +version = "6.8.1" description = "Wrapper for the aiormq for asyncio and humans." category = "main" optional = false -python-versions = ">3.5.*, <4" +python-versions = ">=3.5, <4" [package.dependencies] aiormq = ">=3.2.3,<4" @@ -128,7 +128,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (> [[package]] name = "backports.entry-points-selectable" -version = "1.1.0" +version = "1.1.1" description = "Compatibility shim providing selectable entry points for older implementations" category = "dev" optional = false @@ -136,7 +136,7 @@ python-versions = ">=2.7" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=4.6)", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] +testing = ["pytest", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] [[package]] name = "beautifulsoup4" @@ -190,9 +190,9 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "charset-normalizer" -version = "2.0.7" +version = "2.0.9" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "dev" +category = "main" optional = false python-versions = ">=3.5.0" @@ -262,6 +262,20 @@ ordered-set = ">=3.1.1" [package.extras] murmur = ["mmh3"] +[[package]] +name = "deprecated" +version = "1.2.13" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] + [[package]] name = "discord.py" version = "2.0.0a0" @@ -281,10 +295,9 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] [package.source] type = "url" url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" - [[package]] name = "distlib" -version = "0.3.3" +version = "0.3.4" description = "Distribution utilities" category = "dev" optional = false @@ -322,7 +335,7 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "1.6.1" +version = "1.7.0" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false @@ -330,7 +343,7 @@ python-versions = ">=3.5" [package.dependencies] packaging = "*" -redis = "<3.6.0" +redis = "<4.1.0" six = ">=1.12" sortedcontainers = "*" @@ -351,9 +364,9 @@ sgmllib3k = "*" [[package]] name = "filelock" -version = "3.3.1" +version = "3.4.0" description = "A platform independent file lock." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -493,14 +506,14 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "identify" -version = "2.3.0" +version = "2.4.0" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.6.1" [package.extras] -license = ["editdistance-s"] +license = ["ukkonen"] [[package]] name = "idna" @@ -520,7 +533,7 @@ python-versions = "*" [[package]] name = "isort" -version = "5.9.3" +version = "5.10.1" description = "A Python utility / library to sort Python imports." category = "dev" optional = false @@ -534,7 +547,7 @@ plugins = ["setuptools"] [[package]] name = "lxml" -version = "4.6.5" +version = "4.7.1" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -568,7 +581,7 @@ python-versions = "*" [[package]] name = "more-itertools" -version = "8.10.0" +version = "8.12.0" description = "More routines for operating on iterables, beyond itertools" category = "main" optional = false @@ -608,14 +621,14 @@ python-versions = ">=3.5" [[package]] name = "packaging" -version = "21.0" +version = "21.3" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -pyparsing = ">=2.0.2" +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pamqp" @@ -680,7 +693,7 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.15.0" +version = "2.16.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -715,11 +728,11 @@ python-versions = "*" [[package]] name = "py" -version = "1.10.0" +version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pycares" @@ -745,7 +758,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pycparser" -version = "2.20" +version = "2.21" description = "C parser in Python" category = "main" optional = false @@ -775,11 +788,14 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyparsing" -version = "2.4.7" +version = "3.0.6" description = "Python parsing module" category = "main" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyreadline3" @@ -828,11 +844,11 @@ testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtuale [[package]] name = "pytest-forked" -version = "1.3.0" +version = "1.4.0" description = "run tests in isolated forked subprocesses" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] py = "*" @@ -903,7 +919,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [[package]] name = "rapidfuzz" -version = "1.8.0" +version = "1.9.1" description = "rapid fuzzy string matching" category = "main" optional = false @@ -914,14 +930,17 @@ full = ["numpy"] [[package]] name = "redis" -version = "3.5.3" -description = "Python client for Redis key-value store" +version = "4.0.2" +description = "Python client for Redis database and key-value store" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" + +[package.dependencies] +deprecated = "*" [package.extras] -hiredis = ["hiredis (>=0.1.3)"] +hiredis = ["hiredis (>=1.0.0)"] [[package]] name = "regex" @@ -935,7 +954,7 @@ python-versions = "*" name = "requests" version = "2.26.0" description = "Python HTTP for Humans." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" @@ -949,9 +968,21 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +[[package]] +name = "requests-file" +version = "1.5.1" +description = "File transport adapter for Requests" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +requests = ">=1.0.0" +six = "*" + [[package]] name = "sentry-sdk" -version = "1.4.3" +version = "1.5.1" description = "Python client for Sentry (https://sentry.io)" category = "main" optional = false @@ -996,7 +1027,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "snowballstemmer" -version = "2.1.0" +version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." category = "dev" optional = false @@ -1012,7 +1043,7 @@ python-versions = "*" [[package]] name = "soupsieve" -version = "2.2.1" +version = "2.3.1" description = "A modern CSS selector implementation for Beautiful Soup." category = "main" optional = false @@ -1052,6 +1083,20 @@ build = ["setuptools-git", "wheel", "twine"] docs = ["sphinx", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybil", "twisted", "mock", "django (<2)", "django"] +[[package]] +name = "tldextract" +version = "3.1.2" +description = "Accurately separate the TLD from the registered domain and subdomains of a URL, using the Public Suffix List. By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +filelock = ">=3.0.8" +idna = "*" +requests = ">=2.1.0" +requests-file = ">=1.4" + [[package]] name = "toml" version = "0.10.2" @@ -1062,11 +1107,11 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typing-extensions" -version = "3.10.0.2" -description = "Backported and Experimental Type Hints for Python 3.5+" +version = "4.0.1" +description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" [[package]] name = "urllib3" @@ -1083,7 +1128,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.8.1" +version = "20.10.0" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1092,17 +1137,25 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] "backports.entry-points-selectable" = ">=1.0.4" distlib = ">=0.3.1,<1" -filelock = ">=3.0.0,<4" +filelock = ">=3.2,<4" platformdirs = ">=2,<3" six = ">=1.9.0,<2" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +[[package]] +name = "wrapt" +version = "1.13.3" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + [[package]] name = "yarl" -version = "1.7.0" +version = "1.7.2" description = "Yet another URL library" category = "main" optional = false @@ -1115,12 +1168,12 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "e6fe15a64ae57232a639149df793d6580a93f613425cae85c9892cf959710430" +content-hash = "14ad70153b8c2f4a7e8492bf89f60bf7c468a939da36ce62871b677495f75302" [metadata.files] aio-pika = [ - {file = "aio-pika-6.8.0.tar.gz", hash = "sha256:1d4305a5f78af3857310b4fe48348cdcf6c097e0e275ea88c2cd08570531a369"}, - {file = "aio_pika-6.8.0-py3-none-any.whl", hash = "sha256:e69afef8695f47c5d107bbdba21bdb845d5c249acb3be53ef5c2d497b02657c0"}, + {file = "aio-pika-6.8.1.tar.gz", hash = "sha256:c2b2b46949a34252ff0e64c3bc208eef1893e5791b51aeefabf1676788d56b66"}, + {file = "aio_pika-6.8.1-py3-none-any.whl", hash = "sha256:059ab8ecc03d73997f64ed28df7269105984232174d0e6406389c4e8ed30941c"}, ] aiodns = [ {file = "aiodns-2.0.0-py2.py3-none-any.whl", hash = "sha256:aaa5ac584f40fe778013df0aa6544bf157799bd3f608364b451840ed2c8688de"}, @@ -1194,8 +1247,8 @@ attrs = [ {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] "backports.entry-points-selectable" = [ - {file = "backports.entry_points_selectable-1.1.0-py2.py3-none-any.whl", hash = "sha256:a6d9a871cde5e15b4c4a53e3d43ba890cc6861ec1332c9c2428c92f977192acc"}, - {file = "backports.entry_points_selectable-1.1.0.tar.gz", hash = "sha256:988468260ec1c196dab6ae1149260e2f5472c9110334e5d51adcb77867361f6a"}, + {file = "backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl", hash = "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b"}, + {file = "backports.entry_points_selectable-1.1.1.tar.gz", hash = "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386"}, ] beautifulsoup4 = [ {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, @@ -1266,8 +1319,8 @@ chardet = [ {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.7.tar.gz", hash = "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0"}, - {file = "charset_normalizer-2.0.7-py3-none-any.whl", hash = "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b"}, + {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, + {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -1339,10 +1392,14 @@ deepdiff = [ {file = "deepdiff-4.3.2-py3-none-any.whl", hash = "sha256:59fc1e3e7a28dd0147b0f2b00e3e27181f0f0ef4286b251d5f214a5bcd9a9bc4"}, {file = "deepdiff-4.3.2.tar.gz", hash = "sha256:91360be1d9d93b1d9c13ae9c5048fa83d9cff17a88eb30afaa0d7ff2d0fee17d"}, ] +deprecated = [ + {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, + {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, +] "discord.py" = [] distlib = [ - {file = "distlib-0.3.3-py2.py3-none-any.whl", hash = "sha256:c8b54e8454e5bf6237cc84c20e8264c3e991e824ef27e8f1e81049867d861e31"}, - {file = "distlib-0.3.3.zip", hash = "sha256:d982d0751ff6eaaab5e2ec8e691d949ee80eddf01a62eaa96ddb11531fe16b05"}, + {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, + {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, ] docopt = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, @@ -1355,16 +1412,16 @@ execnet = [ {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] fakeredis = [ - {file = "fakeredis-1.6.1-py3-none-any.whl", hash = "sha256:5eb1516f1fe1813e9da8f6c482178fc067af09f53de587ae03887ef5d9d13024"}, - {file = "fakeredis-1.6.1.tar.gz", hash = "sha256:0d06a9384fb79da9f2164ce96e34eb9d4e2ea46215070805ea6fd3c174590b47"}, + {file = "fakeredis-1.7.0-py3-none-any.whl", hash = "sha256:6f1e04f64557ad3b6835bdc6e5a8d022cbace4bdc24a47ad58f6a72e0fbff760"}, + {file = "fakeredis-1.7.0.tar.gz", hash = "sha256:c9bd12e430336cbd3e189fae0e91eb99997b93e76dbfdd6ed67fa352dc684c71"}, ] feedparser = [ {file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"}, {file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"}, ] filelock = [ - {file = "filelock-3.3.1-py3-none-any.whl", hash = "sha256:2b5eb3589e7fdda14599e7eb1a50e09b4cc14f34ed98b8ba56d33bfaafcbef2f"}, - {file = "filelock-3.3.1.tar.gz", hash = "sha256:34a9f35f95c441e7b38209775d6e0337f9a3759f3565f6c5798f19618527c76f"}, + {file = "filelock-3.4.0-py3-none-any.whl", hash = "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8"}, + {file = "filelock-3.4.0.tar.gz", hash = "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4"}, ] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, @@ -1449,8 +1506,8 @@ humanfriendly = [ {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, ] identify = [ - {file = "identify-2.3.0-py2.py3-none-any.whl", hash = "sha256:d1e82c83d063571bb88087676f81261a4eae913c492dafde184067c584bc7c05"}, - {file = "identify-2.3.0.tar.gz", hash = "sha256:fd08c97f23ceee72784081f1ce5125c8f53a02d3f2716dde79a6ab8f1039fea5"}, + {file = "identify-2.4.0-py2.py3-none-any.whl", hash = "sha256:eba31ca80258de6bb51453084bff4a923187cd2193b9c13710f2516ab30732cc"}, + {file = "identify-2.4.0.tar.gz", hash = "sha256:a33ae873287e81651c7800ca309dc1f84679b763c9c8b30680e16fbfa82f0107"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -1461,70 +1518,70 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.9.3-py3-none-any.whl", hash = "sha256:e17d6e2b81095c9db0a03a8025a957f334d6ea30b26f9ec70805411e5c7c81f2"}, - {file = "isort-5.9.3.tar.gz", hash = "sha256:9c2ea1e62d871267b78307fe511c0838ba0da28698c5732d54e2790bf3ba9899"}, + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] lxml = [ - {file = "lxml-4.6.5-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2"}, - {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b"}, - {file = "lxml-4.6.5-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808"}, - {file = "lxml-4.6.5-cp27-cp27m-win32.whl", hash = "sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c"}, - {file = "lxml-4.6.5-cp27-cp27m-win_amd64.whl", hash = "sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71"}, - {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93"}, - {file = "lxml-4.6.5-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352"}, - {file = "lxml-4.6.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210"}, - {file = "lxml-4.6.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1"}, - {file = "lxml-4.6.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5"}, - {file = "lxml-4.6.5-cp310-cp310-win32.whl", hash = "sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952"}, - {file = "lxml-4.6.5-cp310-cp310-win_amd64.whl", hash = "sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0"}, - {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203"}, - {file = "lxml-4.6.5-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4"}, - {file = "lxml-4.6.5-cp35-cp35m-win32.whl", hash = "sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b"}, - {file = "lxml-4.6.5-cp35-cp35m-win_amd64.whl", hash = "sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408"}, - {file = "lxml-4.6.5-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb"}, - {file = "lxml-4.6.5-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c"}, - {file = "lxml-4.6.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3"}, - {file = "lxml-4.6.5-cp36-cp36m-win32.whl", hash = "sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04"}, - {file = "lxml-4.6.5-cp36-cp36m-win_amd64.whl", hash = "sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120"}, - {file = "lxml-4.6.5-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd"}, - {file = "lxml-4.6.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2"}, - {file = "lxml-4.6.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9"}, - {file = "lxml-4.6.5-cp37-cp37m-win32.whl", hash = "sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090"}, - {file = "lxml-4.6.5-cp37-cp37m-win_amd64.whl", hash = "sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f"}, - {file = "lxml-4.6.5-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c"}, - {file = "lxml-4.6.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44"}, - {file = "lxml-4.6.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9"}, - {file = "lxml-4.6.5-cp38-cp38-win32.whl", hash = "sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d"}, - {file = "lxml-4.6.5-cp38-cp38-win_amd64.whl", hash = "sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41"}, - {file = "lxml-4.6.5-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9"}, - {file = "lxml-4.6.5-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace"}, - {file = "lxml-4.6.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542"}, - {file = "lxml-4.6.5-cp39-cp39-win32.whl", hash = "sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b"}, - {file = "lxml-4.6.5-cp39-cp39-win_amd64.whl", hash = "sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c"}, - {file = "lxml-4.6.5-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"}, - {file = "lxml-4.6.5-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e"}, - {file = "lxml-4.6.5.tar.gz", hash = "sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca"}, + {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, + {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, + {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17"}, + {file = "lxml-4.7.1-cp27-cp27m-win32.whl", hash = "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419"}, + {file = "lxml-4.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2"}, + {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5"}, + {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d"}, + {file = "lxml-4.7.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3"}, + {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175"}, + {file = "lxml-4.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6"}, + {file = "lxml-4.7.1-cp310-cp310-win32.whl", hash = "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6"}, + {file = "lxml-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e"}, + {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675"}, + {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7"}, + {file = "lxml-4.7.1-cp35-cp35m-win32.whl", hash = "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5"}, + {file = "lxml-4.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03"}, + {file = "lxml-4.7.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d"}, + {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6"}, + {file = "lxml-4.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d"}, + {file = "lxml-4.7.1-cp36-cp36m-win32.whl", hash = "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d"}, + {file = "lxml-4.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a"}, + {file = "lxml-4.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3"}, + {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944"}, + {file = "lxml-4.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b"}, + {file = "lxml-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4"}, + {file = "lxml-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1"}, + {file = "lxml-4.7.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e"}, + {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e"}, + {file = "lxml-4.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9"}, + {file = "lxml-4.7.1-cp38-cp38-win32.whl", hash = "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd"}, + {file = "lxml-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d"}, + {file = "lxml-4.7.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db"}, + {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851"}, + {file = "lxml-4.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4"}, + {file = "lxml-4.7.1-cp39-cp39-win32.whl", hash = "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0"}, + {file = "lxml-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f"}, + {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267"}, + {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, + {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, ] markdownify = [ {file = "markdownify-0.6.1-py3-none-any.whl", hash = "sha256:7489fd5c601536996a376c4afbcd1dd034db7690af807120681461e82fbc0acc"}, @@ -1535,8 +1592,8 @@ mccabe = [ {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] more-itertools = [ - {file = "more-itertools-8.10.0.tar.gz", hash = "sha256:1debcabeb1df793814859d64a81ad7cb10504c24349368ccf214c664c474f41f"}, - {file = "more_itertools-8.10.0-py3-none-any.whl", hash = "sha256:56ddac45541718ba332db05f464bebfb0768110111affd27f66e0051f276fa43"}, + {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, + {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, ] mslex = [ {file = "mslex-0.3.0-py2.py3-none-any.whl", hash = "sha256:380cb14abf8fabf40e56df5c8b21a6d533dc5cbdcfe42406bbf08dda8f42e42a"}, @@ -1624,8 +1681,8 @@ ordered-set = [ {file = "ordered-set-4.0.2.tar.gz", hash = "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95"}, ] packaging = [ - {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"}, - {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"}, + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pamqp = [ {file = "pamqp-2.3.0-py2.py3-none-any.whl", hash = "sha256:2f81b5c186f668a67f165193925b6bfd83db4363a6222f599517f29ecee60b02"}, @@ -1648,8 +1705,8 @@ pluggy = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pre-commit = [ - {file = "pre_commit-2.15.0-py2.py3-none-any.whl", hash = "sha256:a4ed01000afcb484d9eb8d504272e642c4c4099bbad3a6b27e519bd6a3e928a6"}, - {file = "pre_commit-2.15.0.tar.gz", hash = "sha256:3c25add78dbdfb6a28a651780d5c311ac40dd17f160eb3954a0c59da40a505a7"}, + {file = "pre_commit-2.16.0-py2.py3-none-any.whl", hash = "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e"}, + {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"}, ] psutil = [ {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, @@ -1685,8 +1742,8 @@ ptable = [ {file = "PTable-0.9.2.tar.gz", hash = "sha256:aa7fc151cb40f2dabcd2275ba6f7fd0ff8577a86be3365cd3fb297cbe09cc292"}, ] py = [ - {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, - {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pycares = [ {file = "pycares-4.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:71b99b9e041ae3356b859822c511f286f84c8889ec9ed1fbf6ac30fb4da13e4c"}, @@ -1726,8 +1783,8 @@ pycodestyle = [ {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] pycparser = [ - {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, - {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, @@ -1738,8 +1795,8 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, + {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, + {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] pyreadline3 = [ {file = "pyreadline3-3.3-py3-none-any.whl", hash = "sha256:0003fd0079d152ecbd8111202c5a7dfa6a5569ffd65b235e45f3c2ecbee337b4"}, @@ -1754,8 +1811,8 @@ pytest-cov = [ {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, ] pytest-forked = [ - {file = "pytest-forked-1.3.0.tar.gz", hash = "sha256:6aa9ac7e00ad1a539c41bec6d21011332de671e938c7637378ec9710204e37ca"}, - {file = "pytest_forked-1.3.0-py2.py3-none-any.whl", hash = "sha256:dc4147784048e70ef5d437951728825a131b81714b398d5d52f17c7c144d8815"}, + {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, + {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, ] pytest-xdist = [ {file = "pytest-xdist-2.3.0.tar.gz", hash = "sha256:e8ecde2f85d88fbcadb7d28cb33da0fa29bca5cf7d5967fa89fc0e97e5299ea5"}, @@ -1805,68 +1862,62 @@ pyyaml = [ {file = "PyYAML-5.4.1.tar.gz", hash = "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e"}, ] rapidfuzz = [ - {file = "rapidfuzz-1.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:91f094562c683802e6c972bce27a692dad70d6cd1114e626b29d990c3704c653"}, - {file = "rapidfuzz-1.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:4a20682121e245cf5ad2dbdd771360763ea11b77520632a1034c4bb9ad1e854c"}, - {file = "rapidfuzz-1.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8810e75d8f9c4453bbd6209c372bf97514359b0b5efff555caf85b15f8a9d862"}, - {file = "rapidfuzz-1.8.0-cp27-cp27m-win32.whl", hash = "sha256:00cf713d843735b5958d87294f08b05c653a593ced7c4120be34f5d26d7a320a"}, - {file = "rapidfuzz-1.8.0-cp27-cp27m-win_amd64.whl", hash = "sha256:2baca64e23a623e077f57e5470de21af2765af15aa1088676eb2d475e664eed0"}, - {file = "rapidfuzz-1.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:9bf7a6c61bacedd84023be356e057e1d209dd6997cfaa3c1cee77aa21d642f88"}, - {file = "rapidfuzz-1.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:61b6434e3341ca5158ecb371b1ceb4c1f6110563a72d28bdce4eb2a084493e47"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e425e690383f6cf308e8c2e8d630fa9596f67d233344efd8fae11e70a9f5635f"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93db5e693b76d616b09df27ca5c79e0dda169af7f1b8f5ab3262826d981e37e2"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a8c4f76ed1c8a65892d98dc2913027c9acdb219d18f3a441cfa427a32861af9"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71e217fd30901214cc96c0c15057278bafb7072aa9b2be4c97459c1fedf3e731"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d579dd447b8e851462e79054b68f94b66b09df8b3abb2aa5ca07fe00912ef5e8"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-win32.whl", hash = "sha256:5808064555273496dcd594d659bd28ee8d399149dd31575321034424455dc955"}, - {file = "rapidfuzz-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:798fef1671ca66c78b47802228e9583f7ab32b99bdfe3984ebb1f96e93e38b5f"}, - {file = "rapidfuzz-1.8.0-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:c9e0ed210831f5c73533bf11099ea7897db491e76c3443bef281d9c1c67d7f3a"}, - {file = "rapidfuzz-1.8.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:c819bb19eb615a31ddc9cb8248a285bf04f58158b53ce096451178631f99b652"}, - {file = "rapidfuzz-1.8.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:942ee45564f28ef70320d1229f02dc998bd93e3519c1f3a80f33ce144b51039c"}, - {file = "rapidfuzz-1.8.0-cp35-cp35m-win32.whl", hash = "sha256:7e6ae2e5a3bc9acc51e118f25d32b8efcd431c5d8deb408336dd2ed0f21d087c"}, - {file = "rapidfuzz-1.8.0-cp35-cp35m-win_amd64.whl", hash = "sha256:98901fba67c89ad2506f3946642cf6eb8f489592fb7eb307ebdf8bdb0c4e97f9"}, - {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e1686f406a0c77ef323cdb7369b7cf9e68f2abfcb83ff5f1e0a5b21f5a534"}, - {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:da0c5fe5fdbbd74206c1778af6b8c5ff8dfbe2dd04ae12bbe96642b358acefce"}, - {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:535253bc9224215131ae450aad6c9f7ef1b24f15c685045eab2b52511268bd06"}, - {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acdad83f07d886705fce164b0d1f4e3b56788a205602ed3a7fc8b10ceaf05fbf"}, - {file = "rapidfuzz-1.8.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35097f649831f8375d6c65a237deccac3aceb573aa7fae1e5d3fa942e89de1c8"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6f4db142e5b4b44314166a90e11603220db659bd2f9c23dd5db402c13eac8eb7"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:19a3f55f27411d68360540484874beda0b428b062596d5f0f141663ef0738bfd"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22b4c1a7f6fe29bd8dae49f7d5ab085dc42c3964f1a78b6dca22fdf83b5c9bfa"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8bfb2fbc147904b78d5c510ee75dc8704b606e956df23f33a9e89abc03f45c3"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6dc5111ebfed2c4f2e4d120a9b280ea13ea4fbb60b6915dd239817b4fc092ed"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db5ee2457d97cb967ffe08446a8c595c03fe747fdc2e145266713f9c516d1c4a"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:12c1b78cc15fc26f555a4bf66088d5afb6354b5a5aa149a123f01a15af6c411b"}, - {file = "rapidfuzz-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:693e9579048d8db4ff020715dd6f25aa315fd6445bc94e7400d7a94a227dad27"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b4fe19df3edcf7de359448b872aec08e6592b4ca2d3df4d8ee57b5812d68bebf"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f3670b9df0e1f479637cad1577afca7766a02775dc08c14837cf495c82861d7c"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:61d118f36eb942649b0db344f7b7a19ad7e9b5749d831788187eb03b57ce1bfa"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fce3a2c8a1d10da12aff4a0d367624e8ae9e15c1b84a5144843681d39be0c355"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1577ef26e3647ccc4cc9754c34ffaa731639779f4d7779e91a761c72adac093e"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fec9b7e60fde51990c3b48fc1aa9dba9ac3acaf78f623dbb645a6fe21a9654e"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b954469d93858bc8b48129bc63fd644382a4df5f3fb1b4b290f48eac1d00a2da"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:190ba709069a7e5a6b39b7c8bc413a08cfa7f1f4defec5d974c4128b510e0234"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-win32.whl", hash = "sha256:97b2d13d6323649b43d1b113681e4013ba230bd6e9827cc832dcebee447d7250"}, - {file = "rapidfuzz-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:81c3091209b75f6611efe2af18834180946d4ce28f41ca8d44fce816187840d2"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d610afa33e92aa0481a514ffda3ec51ca5df3c684c1c1c795307589c62025931"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d976f33ca6b5fabbb095c0a662f5b86baf706184fc24c7f125d4ddb54b8bf036"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f5ca7bca2af598d4ddcf5b93b64b50654a9ff684e6f18d865f6e13fee442b3e"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2aac5ea6b0306dcd28a6d1a89d35ed2c6ac426f2673ee1b92cf3f1d0fd5cd"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f145c9831c0454a696a3136a6380ea4e01434e9cc2f2bc10d032864c16d1d0e5"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ce53291575b56c9d45add73ea013f43bafcea55eee9d5139aa759918d7685f"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de5773a39c00a0f23cfc5da9e0e5fd0fb512b0ebe23dc7289a38e1f9a4b5cefc"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87a802e55792bfbe192e2d557f38867dbe3671b49b3d5ecd873859c7460746ba"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-win32.whl", hash = "sha256:9391abf1121df831316222f28cea37397a0f72bd7978f3be6e7da29a7821e4e5"}, - {file = "rapidfuzz-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:9eeca1b436042b5523dcf314f5822b1131597898c1d967f140d1917541a8a3d1"}, - {file = "rapidfuzz-1.8.0-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:a01f2495aca479b49d3b3a8863d6ba9bea2043447a1ced74ae5ec5270059cbc1"}, - {file = "rapidfuzz-1.8.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:b7d4b1a5d16817f8cdb34365c7b58ae22d5cf1b3207720bb2fa0b55968bdb034"}, - {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c738d0d7f1744646d48d19b4c775926082bcefebd2460f45ca383a0e882f5672"}, - {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0fb9c6078c17c12b52e66b7d0a2a1674f6bbbdc6a76e454c8479b95147018123"}, - {file = "rapidfuzz-1.8.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1482b385d83670eb069577c9667f72b41eec4f005aee32f1a4ff4e71e88afde2"}, - {file = "rapidfuzz-1.8.0.tar.gz", hash = "sha256:83fff37acf0367314879231264169dcbc5e7de969a94f4b82055d06a7fddab9a"}, + {file = "rapidfuzz-1.9.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:68227a8b25291d6a2140aef049271ea30a77be5ef672a58e582a55a5cc1fce93"}, + {file = "rapidfuzz-1.9.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c33541995b96ff40025c1456b8c74b7dd2ab9cbf91943fc35a7bb621f48940e2"}, + {file = "rapidfuzz-1.9.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:c2fafbbf97a4632822248f4201601b691e2eac5fdb30e5d7a96d07a6d058a7d4"}, + {file = "rapidfuzz-1.9.1-cp27-cp27m-win32.whl", hash = "sha256:364795f617a99e1dbb55ac3947ab8366588b72531cb2d6152666287d20610706"}, + {file = "rapidfuzz-1.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:f171d9e66144b0647f9b998ef10bdd919a640e4b1357250c8ef6259deb5ffe0d"}, + {file = "rapidfuzz-1.9.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c83801a7c5209663aa120b815a4f2c39e95fe8e0b774ec58a1e0affd6a2fcfc6"}, + {file = "rapidfuzz-1.9.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:67e61c2baa6bb1848c4a33752f1781124dcc90bf3f31b18b44db1ae4e4e26634"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8ab7eb003a18991347174910f11d38ff40399081185d9e3199ec277535f7828b"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5ad450badf06ddf98a246140b5059ba895ee8445e8102a5a289908327f551f81"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:402b2174bded62a793c5f7d9aec16bc32c661402360a934819ae72b54cfbce1e"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92066ccb054efc2e17afb4049c98b550969653cd58f71dd756cfcc8e6864630a"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8dc0bf1814accee08a9c9bace6672ef06eae6b0446fce88e3e97e23dfaf3ea10"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdbd387efb8478605951344f327dd03bf053c138d757369a43404305b99e55db"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-win32.whl", hash = "sha256:b1c54807e556dbcc6caf4ce0f24446c01b195f3cc46e2a6e74b82d3a21eaa45d"}, + {file = "rapidfuzz-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:ac3273364cd1619cab3bf0ba731efea5405833f9eba362da7dcd70bd42073d8e"}, + {file = "rapidfuzz-1.9.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:d9faf62606c08a0a6992dd480c72b6a068733ae02688dc35f2e36ba0d44673f4"}, + {file = "rapidfuzz-1.9.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:f6a56a48be047637b1b0b2459a11cf7cd5aa7bbe16a439bd4f73b4af39e620e4"}, + {file = "rapidfuzz-1.9.1-cp35-cp35m-win32.whl", hash = "sha256:aa91609979e9d2700f0ff100df99b36e7d700b70169ee385d43d5de9e471ae97"}, + {file = "rapidfuzz-1.9.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b4cfdd0915ab4cec86c2ff6bab9f01b03454f3de0963c37f9f219df2ddf42b95"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c6bfa4ad0158a093cd304f795ceefdc3861ae6942a61432b2a50858be6de88ca"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:eb0ea02295d9278bd2dcd2df4760b0f2887b6c3f2f374005ec5af320d8d3a37e"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d5187cd5cd6273e9fee07de493a42a2153134a4914df74cb1abb0744551c548a"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6e5b8af63f9c05b64454460759ed84a715d581d598ec4484f4ec512f398e8b1"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-win32.whl", hash = "sha256:36137f88f2b28115af506118e64e11c816611eab2434293af7fdacd1290ffb9d"}, + {file = "rapidfuzz-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:fcc420cad46be7c9887110edf04cdee545f26dbf22650a443d89790fc35f7b88"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b06de314f426aebff8a44319016bbe2b22f7848c84e44224f80b0690b7b08b18"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e5de44e719faea79e45322b037f0d4a141d750b80d2204fa68f43a42a24f0fbc"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f9439df09a782afd01b67005a3b110c70bbf9e1cf06d2ac9b293ce2d02d3c549"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e903d4702647465721e2d0431c95f04fd56a06577f06f41e2960c83fd63c1bad"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-win32.whl", hash = "sha256:a5298f4ac1975edcbb15583eab659a44b33aebaf3bccf172e185cfea68771c08"}, + {file = "rapidfuzz-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:103193a01921b54fcdad6b01cfda3a68e00aeafca236b7ecd5b1b2c2e7e96337"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1d98a3187040dca855e02179a35c137f72ef83ce243783d44ea59efa86b94b3a"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cb92bf7fc911b787055a88d9295ca3b4fe8576e3b59271f070f1b1b181eb087d"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3f014a0f5f8159a94c6ee884fedd1c30e07fb866a5d76ff2c18091bc6363b76f"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:31474074a99f72289ac325fbd77983e7d355d48860bfe7a4f6f6396fdb24410a"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec67d79af5a2d7b0cf67b570a5579710e461cadda4120478e813b63491f394dd"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ebc0d3d15ed32f98f0052cf6e3e9c9b8010fb93c04fb74d2022e3c51ec540e2"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-win32.whl", hash = "sha256:477ab1a3044bab89db45caabc562b158f68765ecaa638b73ba17e92f09dfa5ff"}, + {file = "rapidfuzz-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:8e872763dc0367d7544aa585d2e8b27af233323b8a7cd2f9b78cafa05bae5018"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8401c41e219ae36ca7a88762776a6270511650d4cc70d024ae61561e96d67e47"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea10bd8e0436801c3264f7084a5ea194f12ba9fe1ba898aa4a2107d276501292"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:433737914b46c1ffa0c678eceae1c260dc6b7fb5b6cad4c725d3e3607c764b32"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8c3b08e90e45acbc469d1f456681643256e952bf84ec7714f58979baba0c8a1c"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bbcd265b3c86176e5db4cbba7b4364d7333c214ee80e2d259c7085929934ca9d"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d69fabcd635783cd842e7d5ee4b77164314c5124b82df5a0c436ab3d698f8a9"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-win32.whl", hash = "sha256:01f16b6f3fa5d1a26c12f5da5de0032f1e12c919d876005b57492a8ec9a5c043"}, + {file = "rapidfuzz-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:0bcc5bbfdbe6068cc2cf0029ab6cde08dceac498d232fa3a61dd34fbfa0b3f36"}, + {file = "rapidfuzz-1.9.1-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:de869c8f4e8edb9b2f7b8232a04896645501defcbd9d85bc0202ff3ec6285f6b"}, + {file = "rapidfuzz-1.9.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:db5978e970fb0955974d51021da4b929e2e4890fef17792989ee32658e2b159c"}, + {file = "rapidfuzz-1.9.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:33479f75f36ac3a1d8421365d4fa906e013490790730a89caba31d06e6f71738"}, + {file = "rapidfuzz-1.9.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:af991cb333ec526d894923163050931b3a870b7694bf7687aaa6154d341a98f5"}, + {file = "rapidfuzz-1.9.1.tar.gz", hash = "sha256:bd7a4fe33ba49db3417f0f57a8af02462554f1296dedcf35b026cd3525efef74"}, ] redis = [ - {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, - {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, + {file = "redis-4.0.2-py3-none-any.whl", hash = "sha256:c8481cf414474e3497ec7971a1ba9b998c8efad0f0d289a009a5bbef040894f9"}, + {file = "redis-4.0.2.tar.gz", hash = "sha256:ccf692811f2c1fc7a92b466aa2599e4a6d2d73d5f736a2c70be600657c0da34a"}, ] regex = [ {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, @@ -1915,9 +1966,13 @@ requests = [ {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, ] +requests-file = [ + {file = "requests-file-1.5.1.tar.gz", hash = "sha256:07d74208d3389d01c38ab89ef403af0cfec63957d53a0081d8eca738d0247d8e"}, + {file = "requests_file-1.5.1-py2.py3-none-any.whl", hash = "sha256:dfe5dae75c12481f68ba353183c53a65e6044c923e64c24b2209f6c7570ca953"}, +] sentry-sdk = [ - {file = "sentry-sdk-1.4.3.tar.gz", hash = "sha256:b9844751e40710e84a457c5bc29b21c383ccb2b63d76eeaad72f7f1c808c8828"}, - {file = "sentry_sdk-1.4.3-py2.py3-none-any.whl", hash = "sha256:c091cc7115ff25fe3a0e410dbecd7a996f81a3f6137d2272daef32d6c3cfa6dc"}, + {file = "sentry-sdk-1.5.1.tar.gz", hash = "sha256:2a1757d6611e4bec7d672c2b7ef45afef79fed201d064f53994753303944f5a8"}, + {file = "sentry_sdk-1.5.1-py2.py3-none-any.whl", hash = "sha256:e4cb107e305b2c1b919414775fa73a9997f996447417d22b98e7610ded1e9eb5"}, ] sgmllib3k = [ {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, @@ -1927,16 +1982,16 @@ six = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] snowballstemmer = [ - {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, - {file = "snowballstemmer-2.1.0.tar.gz", hash = "sha256:e997baa4f2e9139951b6f4c631bad912dfd3c792467e2f03d7239464af90e914"}, + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] sortedcontainers = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] soupsieve = [ - {file = "soupsieve-2.2.1-py3-none-any.whl", hash = "sha256:c2c1c2d44f158cdbddab7824a9af8c4f83c76b1e23e049479aa432feb6c4c23b"}, - {file = "soupsieve-2.2.1.tar.gz", hash = "sha256:052774848f448cf19c7e959adf5566904d525f33a3f8b6ba6f6f8f26ec7de0cc"}, + {file = "soupsieve-2.3.1-py3-none-any.whl", hash = "sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb"}, + {file = "soupsieve-2.3.1.tar.gz", hash = "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9"}, ] statsd = [ {file = "statsd-3.3.0-py2.py3-none-any.whl", hash = "sha256:c610fb80347fca0ef62666d241bce64184bd7cc1efe582f9690e045c25535eaa"}, @@ -1950,94 +2005,150 @@ testfixtures = [ {file = "testfixtures-6.18.3-py2.py3-none-any.whl", hash = "sha256:6ddb7f56a123e1a9339f130a200359092bd0a6455e31838d6c477e8729bb7763"}, {file = "testfixtures-6.18.3.tar.gz", hash = "sha256:2600100ae96ffd082334b378e355550fef8b4a529a6fa4c34f47130905c7426d"}, ] +tldextract = [ + {file = "tldextract-3.1.2-py2.py3-none-any.whl", hash = "sha256:f55e05f6bf4cc952a87d13594386d32ad2dd265630a8bdfc3df03bd60425c6b0"}, + {file = "tldextract-3.1.2.tar.gz", hash = "sha256:d2034c3558651f7d8fdadea83fb681050b2d662dc67a00d950326dc902029444"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typing-extensions = [ - {file = "typing_extensions-3.10.0.2-py2-none-any.whl", hash = "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7"}, - {file = "typing_extensions-3.10.0.2-py3-none-any.whl", hash = "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"}, - {file = "typing_extensions-3.10.0.2.tar.gz", hash = "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e"}, + {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, + {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] urllib3 = [ {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, ] virtualenv = [ - {file = "virtualenv-20.8.1-py2.py3-none-any.whl", hash = "sha256:10062e34c204b5e4ec5f62e6ef2473f8ba76513a9a617e873f1f8fb4a519d300"}, - {file = "virtualenv-20.8.1.tar.gz", hash = "sha256:bcc17f0b3a29670dd777d6f0755a4c04f28815395bca279cdcb213b97199a6b8"}, + {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, + {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, +] +wrapt = [ + {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, + {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, + {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, + {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, + {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, + {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, + {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, + {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, + {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, + {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, + {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, + {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, + {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, + {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, + {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, + {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, + {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, + {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, + {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, + {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, + {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, + {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, + {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, + {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, + {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, + {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, + {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, + {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, + {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, + {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, + {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, + {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, ] yarl = [ - {file = "yarl-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e35d8230e4b08d86ea65c32450533b906a8267a87b873f2954adeaecede85169"}, - {file = "yarl-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb4b3f277880c314e47720b4b6bb2c85114ab3c04c5442c9bc7006b3787904d8"}, - {file = "yarl-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7015dcedb91d90a138eebdc7e432aec8966e0147ab2a55f2df27b1904fa7291"}, - {file = "yarl-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb3e478175e15e00d659fb0354a6a8db71a7811a2a5052aed98048bc972e5d2b"}, - {file = "yarl-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b8c409aa3a7966647e7c1c524846b362a6bcbbe120bf8a176431f940d2b9a2e"}, - {file = "yarl-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b22ea41c7e98170474a01e3eded1377d46b2dfaef45888a0005c683eaaa49285"}, - {file = "yarl-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a7dfc46add4cfe5578013dbc4127893edc69fe19132d2836ff2f6e49edc5ecd6"}, - {file = "yarl-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:82ff6f85f67500a4f74885d81659cd270eb24dfe692fe44e622b8a2fd57e7279"}, - {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f3cd2158b2ed0fb25c6811adfdcc47224efe075f2d68a750071dacc03a7a66e4"}, - {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:59c0f13f9592820c51280d1cf811294d753e4a18baf90f0139d1dc93d4b6fc5f"}, - {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7f7655ad83d1a8afa48435a449bf2f3009293da1604f5dd95b5ddcf5f673bd69"}, - {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:aa9f0d9b62d15182341b3e9816582f46182cab91c1a57b2d308b9a3c4e2c4f78"}, - {file = "yarl-1.7.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fdd1b90c225a653b1bd1c0cae8edf1957892b9a09c8bf7ee6321eeb8208eac0f"}, - {file = "yarl-1.7.0-cp310-cp310-win32.whl", hash = "sha256:7c8d0bb76eabc5299db203e952ec55f8f4c53f08e0df4285aac8c92bd9e12675"}, - {file = "yarl-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:622a36fa779efb4ff9eff5fe52730ff17521431379851a31e040958fc251670c"}, - {file = "yarl-1.7.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3d461b7a8e139b9e4b41f62eb417ffa0b98d1c46d4caf14c845e6a3b349c0bb1"}, - {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81cfacdd1e40bc931b5519499342efa388d24d262c30a3d31187bfa04f4a7001"}, - {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:821b978f2152be7695d4331ef0621d207aedf9bbd591ba23a63412a3efc29a01"}, - {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b64bd24c8c9a487f4a12260dc26732bf41028816dbf0c458f17864fbebdb3131"}, - {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:98c9ddb92b60a83c21be42c776d3d9d5ec632a762a094c41bda37b7dfbd2cd83"}, - {file = "yarl-1.7.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a532d75ca74431c053a88a802e161fb3d651b8bf5821a3440bc3616e38754583"}, - {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:053e09817eafb892e94e172d05406c1b3a22a93bc68f6eff5198363a3d764459"}, - {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:98c51f02d542945d306c8e934aa2c1e66ba5e9c1c86b5bf37f3a51c8a747067e"}, - {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:15ec41a5a5fdb7bace6d7b16701f9440007a82734f69127c0fbf6d87e10f4a1e"}, - {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:a7f08819dba1e1255d6991ed37448a1bf4b1352c004bcd899b9da0c47958513d"}, - {file = "yarl-1.7.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8e3ffab21db0542ffd1887f3b9575ddd58961f2cf61429cb6458afc00c4581e0"}, - {file = "yarl-1.7.0-cp36-cp36m-win32.whl", hash = "sha256:50127634f519b2956005891507e3aa4ac345f66a7ea7bbc2d7dcba7401f41898"}, - {file = "yarl-1.7.0-cp36-cp36m-win_amd64.whl", hash = "sha256:36ec44f15193f6d5288d42ebb8e751b967ebdfb72d6830983838d45ab18edb4f"}, - {file = "yarl-1.7.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ec1b5a25a25c880c976d0bb3d107def085bb08dbb3db7f4442e0a2b980359d24"}, - {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b36f5a63c891f813c6f04ef19675b382efc190fd5ce7e10ab19386d2548bca06"}, - {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38173b8c3a29945e7ecade9a3f6ff39581eee8201338ee6a2c8882db5df3e806"}, - {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ba402f32184f0b405fb281b93bd0d8ab7e3257735b57b62a6ed2e94cdf4fe50"}, - {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:be52bc5208d767cdd8308a9e93059b3b36d1e048fecbea0e0346d0d24a76adc0"}, - {file = "yarl-1.7.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:08c2044a956f4ef30405f2f433ce77f1f57c2c773bf81ae43201917831044d5a"}, - {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:484d61c047c45670ef5967653a1d0783e232c54bf9dd786a7737036828fa8d54"}, - {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b7de92a4af85cfcaf4081f8aa6165b1d63ee5de150af3ee85f954145f93105a7"}, - {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:376e41775aab79c5575534924a386c8e0f1a5d91db69fc6133fd27a489bcaf10"}, - {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:8a8b10d0e7bac154f959b709fcea593cda527b234119311eb950096653816a86"}, - {file = "yarl-1.7.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:f46cd4c43e6175030e2a56def8f1d83b64e6706eeb2bb9ab0ef4756f65eab23f"}, - {file = "yarl-1.7.0-cp37-cp37m-win32.whl", hash = "sha256:b28cfb46140efe1a6092b8c5c4994a1fe70dc83c38fbcea4992401e0c6fb9cce"}, - {file = "yarl-1.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9624154ec9c02a776802da1086eed7f5034bd1971977f5146233869c2ac80297"}, - {file = "yarl-1.7.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:69945d13e1bbf81784a9bc48824feb9cd66491e6a503d4e83f6cd7c7cc861361"}, - {file = "yarl-1.7.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:46a742ed9e363bd01be64160ce7520e92e11989bd4cb224403cfd31c101cc83d"}, - {file = "yarl-1.7.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb4ff1ac7cb4500f43581b3f4cbd627d702143aa6be1fdc1fa3ebffaf4dc1be5"}, - {file = "yarl-1.7.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ad51e17cd65ea3debb0e10f0120cf8dd987c741fe423ed2285087368090b33d"}, - {file = "yarl-1.7.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e37786ea89a5d3ffbbf318ea9790926f8dfda83858544f128553c347ad143c6"}, - {file = "yarl-1.7.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c63c1e208f800daad71715786bfeb1cecdc595d87e2e9b1cd234fd6e597fd71d"}, - {file = "yarl-1.7.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:91cbe24300c11835ef186436363352b3257db7af165e0a767f4f17aa25761388"}, - {file = "yarl-1.7.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e510dbec7c59d32eaa61ffa48173d5e3d7170a67f4a03e8f5e2e9e3971aca622"}, - {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3def6e681cc02397e5d8141ee97b41d02932b2bcf0fb34532ad62855eab7c60e"}, - {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:263c81b94e6431942b27f6f671fa62f430a0a5c14bb255f2ab69eeb9b2b66ff7"}, - {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e78c91faefe88d601ddd16e3882918dbde20577a2438e2320f8239c8b7507b8f"}, - {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:22b2430c49713bfb2f0a0dd4a8d7aab218b28476ba86fd1c78ad8899462cbcf2"}, - {file = "yarl-1.7.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2e7ad9db939082f5d0b9269cfd92c025cb8f2fbbb1f1b9dc5a393c639db5bd92"}, - {file = "yarl-1.7.0-cp38-cp38-win32.whl", hash = "sha256:3a31e4a8dcb1beaf167b7e7af61b88cb961b220db8d3ba1c839723630e57eef7"}, - {file = "yarl-1.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:d579957439933d752358c6a300c93110f84aae67b63dd0c19dde6ecbf4056f6b"}, - {file = "yarl-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:87721b549505a546eb003252185103b5ec8147de6d3ad3714d148a5a67b6fe53"}, - {file = "yarl-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1fa866fa24d9f4108f9e58ea8a2135655419885cdb443e36b39a346e1181532"}, - {file = "yarl-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d3b8449dfedfe94eaff2b77954258b09b24949f6818dfa444b05dbb05ae1b7e"}, - {file = "yarl-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db2372e350794ce8b9f810feb094c606b7e0e4aa6807141ac4fadfe5ddd75bb0"}, - {file = "yarl-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a06d9d0b9a97fa99b84fee71d9dd11e69e21ac8a27229089f07b5e5e50e8d63c"}, - {file = "yarl-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3455c2456d6307bcfa80bc1157b8603f7d93573291f5bdc7144489ca0df4628"}, - {file = "yarl-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d30d67e3486aea61bb2cbf7cf81385364c2e4f7ce7469a76ed72af76a5cdfe6b"}, - {file = "yarl-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c18a4b286e8d780c3a40c31d7b79836aa93b720f71d5743f20c08b7e049ca073"}, - {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d54c925396e7891666cabc0199366ca55b27d003393465acef63fd29b8b7aa92"}, - {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:64773840952de17851a1c7346ad7f71688c77e74248d1f0bc230e96680f84028"}, - {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:acbf1756d9dc7cd0ae943d883be72e84e04396f6c2ff93a6ddeca929d562039f"}, - {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:2e48f27936aa838939c798f466c851ba4ae79e347e8dfce43b009c64b930df12"}, - {file = "yarl-1.7.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1beef4734ca1ad40a9d8c6b20a76ab46e3a2ed09f38561f01e4aa2ea82cafcef"}, - {file = "yarl-1.7.0-cp39-cp39-win32.whl", hash = "sha256:8ee78c9a5f3c642219d4607680a4693b59239c27a3aa608b64ef79ddc9698039"}, - {file = "yarl-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:d750503682605088a14d29a4701548c15c510da4f13c8b17409c4097d5b04c52"}, - {file = "yarl-1.7.0.tar.gz", hash = "sha256:8e7ebaf62e19c2feb097ffb7c94deb0f0c9fab52590784c8cd679d30ab009162"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, + {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, + {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, + {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, + {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, + {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, + {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, + {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, + {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, + {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, + {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, + {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, + {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, + {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, + {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, + {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, + {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, + {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, + {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, + {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, + {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, + {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, + {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, + {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, + {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, + {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, + {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, ] diff --git a/pyproject.toml b/pyproject.toml index 44d09f89e..928435975 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ pyyaml = "~=5.1" regex = "==2021.4.4" sentry-sdk = "~=1.3" statsd = "~=3.3" +tldextract = "^3.1.2" [tool.poetry.dev-dependencies] coverage = "~=5.0" -- cgit v1.2.3 From 94f5c99c1ff5815341862431d02129e80ceb6850 Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Tue, 28 Dec 2021 18:11:52 +0000 Subject: Include message counts in all channels (#2016) Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/info/information.py | 11 +++------ bot/exts/moderation/voice_gate.py | 5 +--- tests/bot/exts/info/test_information.py | 43 +++++++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 73357211e..d0e1eae74 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -298,11 +298,11 @@ class Information(Cog): "Member information", membership ), + await self.user_messages(user), ] # Show more verbose output in moderation channels for infractions and nominations if is_mod_channel(ctx.channel): - fields.append(await self.user_messages(user)) fields.append(await self.expanded_user_infraction_counts(user)) fields.append(await self.user_nomination_counts(user)) else: @@ -420,13 +420,8 @@ class Information(Cog): if e.status == 404: activity_output = "No activity" else: - activity_output.append(user_activity["total_messages"] or "No messages") - - if (activity_blocks := user_activity.get("activity_blocks")) is not None: - # activity_blocks is not included in the response if the user has a lot of messages - activity_output.append(activity_blocks or "No activity") # Special case when activity_blocks is 0. - else: - activity_output.append("Too many to count!") + activity_output.append(f"{user_activity['total_messages']:,}" or "No messages") + activity_output.append(f"{user_activity['activity_blocks']:,}" or "No activity") activity_output = "\n".join( f"{name}: {metric}" for name, metric in zip(["Messages", "Activity blocks"], activity_output) diff --git a/bot/exts/moderation/voice_gate.py b/bot/exts/moderation/voice_gate.py index ae55a03a0..a382b13d1 100644 --- a/bot/exts/moderation/voice_gate.py +++ b/bot/exts/moderation/voice_gate.py @@ -171,11 +171,8 @@ class VoiceGate(Cog): ), "total_messages": data["total_messages"] < GateConf.minimum_messages, "voice_banned": data["voice_banned"], + "activity_blocks": data["activity_blocks"] < GateConf.minimum_activity_blocks, } - if activity_blocks := data.get("activity_blocks"): - # activity_blocks is not included in the response if the user has a lot of messages. - # Only check if the user has enough activity blocks if it is included. - checks["activity_blocks"] = activity_blocks < GateConf.minimum_activity_blocks failed = any(checks.values()) failed_reasons = [MESSAGE_FIELD_MAP[key] for key, value in checks.items() if value is True] diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index 632287322..724456b04 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -276,6 +276,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) ) + @unittest.mock.patch( + f"{COG_PATH}.user_messages", + new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count")) + ) async def test_create_user_embed_uses_string_representation_of_user_in_title_if_nick_is_not_available(self): """The embed should use the string representation of the user if they don't have a nick.""" ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1)) @@ -293,6 +297,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) ) + @unittest.mock.patch( + f"{COG_PATH}.user_messages", + new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count")) + ) async def test_create_user_embed_uses_nick_in_title_if_available(self): """The embed should use the nick if it's available.""" ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1)) @@ -310,6 +318,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) ) + @unittest.mock.patch( + f"{COG_PATH}.user_messages", + new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count")) + ) async def test_create_user_embed_ignores_everyone_role(self): """Created `!user` embeds should not contain mention of the @everyone-role.""" ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=1)) @@ -325,6 +337,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): @unittest.mock.patch(f"{COG_PATH}.expanded_user_infraction_counts", new_callable=unittest.mock.AsyncMock) @unittest.mock.patch(f"{COG_PATH}.user_nomination_counts", new_callable=unittest.mock.AsyncMock) + @unittest.mock.patch( + f"{COG_PATH}.user_messages", + new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count")) + ) async def test_create_user_embed_expanded_information_in_moderation_channels( self, nomination_counts, @@ -363,13 +379,19 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): ) @unittest.mock.patch(f"{COG_PATH}.basic_user_infraction_counts", new_callable=unittest.mock.AsyncMock) - async def test_create_user_embed_basic_information_outside_of_moderation_channels(self, infraction_counts): + @unittest.mock.patch(f"{COG_PATH}.user_messages", new_callable=unittest.mock.AsyncMock) + async def test_create_user_embed_basic_information_outside_of_moderation_channels( + self, + user_messages, + infraction_counts, + ): """The embed should contain only basic infraction data outside of mod channels.""" ctx = helpers.MockContext(channel=helpers.MockTextChannel(id=100)) moderators_role = helpers.MockRole(name='Moderators') infraction_counts.return_value = ("Infractions", "basic infractions info") + user_messages.return_value = ("Messages", "user message counts") user = helpers.MockMember(id=314, roles=[moderators_role], colour=100) embed = await self.cog.create_user_embed(ctx, user) @@ -394,14 +416,23 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): ) self.assertEqual( - "basic infractions info", + "user message counts", embed.fields[2].value ) + self.assertEqual( + "basic infractions info", + embed.fields[3].value + ) + @unittest.mock.patch( f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) ) + @unittest.mock.patch( + f"{COG_PATH}.user_messages", + new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count")) + ) async def test_create_user_embed_uses_top_role_colour_when_user_has_roles(self): """The embed should be created with the colour of the top role, if a top role is available.""" ctx = helpers.MockContext() @@ -417,6 +448,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) ) + @unittest.mock.patch( + f"{COG_PATH}.user_messages", + new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count")) + ) async def test_create_user_embed_uses_og_blurple_colour_when_user_has_no_roles(self): """The embed should be created with the og blurple colour if the user has no assigned roles.""" ctx = helpers.MockContext() @@ -430,6 +465,10 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): f"{COG_PATH}.basic_user_infraction_counts", new=unittest.mock.AsyncMock(return_value=("Infractions", "basic infractions")) ) + @unittest.mock.patch( + f"{COG_PATH}.user_messages", + new=unittest.mock.AsyncMock(return_value=("Messsages", "user message count")) + ) async def test_create_user_embed_uses_png_format_of_user_avatar_as_thumbnail(self): """The embed thumbnail should be set to the user's avatar in `png` format.""" ctx = helpers.MockContext() -- cgit v1.2.3 From 548766959abc77ffc9140ec4f5be52cfcdff3a6c Mon Sep 17 00:00:00 2001 From: Shom770 <82843611+Shom770@users.noreply.github.com> Date: Wed, 29 Dec 2021 13:37:56 -0500 Subject: Strip gotcha tag (PR #2000) * adding strip-gotcha tag --- bot/resources/tags/strip-gotcha.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 bot/resources/tags/strip-gotcha.md diff --git a/bot/resources/tags/strip-gotcha.md b/bot/resources/tags/strip-gotcha.md new file mode 100644 index 000000000..9ad495cd2 --- /dev/null +++ b/bot/resources/tags/strip-gotcha.md @@ -0,0 +1,17 @@ +When working with `strip`, `lstrip`, or `rstrip`, you might think that this would be the case: +```py +>>> "Monty Python".rstrip(" Python") +"Monty" +``` +While this seems intuitive, it would actually result in: +```py +"M" +``` +as Python interprets the argument to these functions as a set of characters rather than a substring. + +If you want to remove a prefix/suffix from a string, `str.removeprefix` and `str.removesuffix` are recommended and were added in 3.9. +```py +>>> "Monty Python".removesuffix(" Python") +"Monty" +``` +See the documentation of [str.removeprefix](https://docs.python.org/3.10/library/stdtypes.html#str.removeprefix) and [str.removesuffix](https://docs.python.org/3.10/library/stdtypes.html#str.removesuffix) for more information. -- cgit v1.2.3 From 681771b945ad9c3968323c083c3ed45a32ba37bf Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Wed, 29 Dec 2021 20:38:05 +0000 Subject: Add text indicating when user fetched by message (#2013) Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/info/information.py | 10 ++++++---- tests/bot/exts/info/test_information.py | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index d0e1eae74..1f95c460f 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -227,7 +227,7 @@ class Information(Cog): @command(name="user", aliases=["user_info", "member", "member_info", "u"]) async def user_info(self, ctx: Context, user_or_message: Union[MemberOrUser, Message] = None) -> None: """Returns info about a user.""" - if isinstance(user_or_message, Message): + if passed_as_message := isinstance(user_or_message, Message): user = user_or_message.author else: user = user_or_message @@ -242,10 +242,10 @@ class Information(Cog): # Will redirect to #bot-commands if it fails. if in_whitelist_check(ctx, roles=constants.STAFF_PARTNERS_COMMUNITY_ROLES): - embed = await self.create_user_embed(ctx, user) + embed = await self.create_user_embed(ctx, user, passed_as_message) await ctx.send(embed=embed) - async def create_user_embed(self, ctx: Context, user: MemberOrUser) -> Embed: + async def create_user_embed(self, ctx: Context, user: MemberOrUser, passed_as_message: bool) -> Embed: """Creates an embed containing information on the `user`.""" on_server = bool(await get_or_fetch_member(ctx.guild, user.id)) @@ -256,6 +256,9 @@ class Information(Cog): name = f"{user.nick} ({name})" name = escape_markdown(name) + if passed_as_message: + name += " - From Message" + if user.public_flags.verified_bot: name += f" {constants.Emojis.verified_bot}" elif user.bot: @@ -282,7 +285,6 @@ class Information(Cog): membership = textwrap.dedent("\n".join([f"{key}: {value}" for key, value in membership.items()])) else: - roles = None membership = "The user is not a member of the server" fields = [ diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index 724456b04..30e5258fb 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -289,7 +289,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock") user.colour = 0 - embed = await self.cog.create_user_embed(ctx, user) + embed = await self.cog.create_user_embed(ctx, user, False) self.assertEqual(embed.title, "Mr. Hemlock") @@ -310,7 +310,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock") user.colour = 0 - embed = await self.cog.create_user_embed(ctx, user) + embed = await self.cog.create_user_embed(ctx, user, False) self.assertEqual(embed.title, "Cat lover (Mr. Hemlock)") @@ -330,7 +330,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): # A `MockMember` has the @Everyone role by default; we add the Admins to that. user = helpers.MockMember(roles=[admins_role], colour=100) - embed = await self.cog.create_user_embed(ctx, user) + embed = await self.cog.create_user_embed(ctx, user, False) self.assertIn("&Admins", embed.fields[1].value) self.assertNotIn("&Everyone", embed.fields[1].value) @@ -355,7 +355,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): nomination_counts.return_value = ("Nominations", "nomination info") user = helpers.MockMember(id=314, roles=[moderators_role], colour=100) - embed = await self.cog.create_user_embed(ctx, user) + embed = await self.cog.create_user_embed(ctx, user, False) infraction_counts.assert_called_once_with(user) nomination_counts.assert_called_once_with(user) @@ -394,7 +394,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): user_messages.return_value = ("Messages", "user message counts") user = helpers.MockMember(id=314, roles=[moderators_role], colour=100) - embed = await self.cog.create_user_embed(ctx, user) + embed = await self.cog.create_user_embed(ctx, user, False) infraction_counts.assert_called_once_with(user) @@ -440,7 +440,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): moderators_role = helpers.MockRole(name='Moderators') user = helpers.MockMember(id=314, roles=[moderators_role], colour=100) - embed = await self.cog.create_user_embed(ctx, user) + embed = await self.cog.create_user_embed(ctx, user, False) self.assertEqual(embed.colour, discord.Colour(100)) @@ -457,7 +457,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): ctx = helpers.MockContext() user = helpers.MockMember(id=217, colour=discord.Colour.default()) - embed = await self.cog.create_user_embed(ctx, user) + embed = await self.cog.create_user_embed(ctx, user, False) self.assertEqual(embed.colour, discord.Colour.og_blurple()) @@ -475,7 +475,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): user = helpers.MockMember(id=217, colour=0) user.display_avatar.url = "avatar url" - embed = await self.cog.create_user_embed(ctx, user) + embed = await self.cog.create_user_embed(ctx, user, False) self.assertEqual(embed.thumbnail.url, "avatar url") @@ -528,7 +528,7 @@ class UserCommandTests(unittest.IsolatedAsyncioTestCase): await self.cog.user_info(self.cog, ctx) - create_embed.assert_called_once_with(ctx, self.author) + create_embed.assert_called_once_with(ctx, self.author, False) ctx.send.assert_called_once() @unittest.mock.patch("bot.exts.info.information.Information.create_user_embed") @@ -539,7 +539,7 @@ class UserCommandTests(unittest.IsolatedAsyncioTestCase): await self.cog.user_info(self.cog, ctx, self.author) - create_embed.assert_called_once_with(ctx, self.author) + create_embed.assert_called_once_with(ctx, self.author, False) ctx.send.assert_called_once() @unittest.mock.patch("bot.exts.info.information.Information.create_user_embed") @@ -550,7 +550,7 @@ class UserCommandTests(unittest.IsolatedAsyncioTestCase): await self.cog.user_info(self.cog, ctx) - create_embed.assert_called_once_with(ctx, self.moderator) + create_embed.assert_called_once_with(ctx, self.moderator, False) ctx.send.assert_called_once() @unittest.mock.patch("bot.exts.info.information.Information.create_user_embed") @@ -562,5 +562,5 @@ class UserCommandTests(unittest.IsolatedAsyncioTestCase): await self.cog.user_info(self.cog, ctx, self.target) - create_embed.assert_called_once_with(ctx, self.target) + create_embed.assert_called_once_with(ctx, self.target, False) ctx.send.assert_called_once() -- cgit v1.2.3 From d30776a17f058fb526d7fb93fbd3d754d35f2bd5 Mon Sep 17 00:00:00 2001 From: Izan Date: Sat, 1 Jan 2022 17:02:59 +0000 Subject: Infraction mod-log improvements - Add infraction id to infraction edit modlog - Add missing colon in "infraction applied" message - Utilise defined infraction id variable instead of indexing dict again --- bot/exts/moderation/infraction/_scheduler.py | 4 ++-- bot/exts/moderation/infraction/management.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 6f379a9a0..57aa2d9b6 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -223,7 +223,7 @@ class InfractionScheduler: failed = True if failed: - log.trace(f"Deleted infraction {infraction['id']} from database because applying infraction failed.") + log.trace(f"Trying to delete infraction {id_} from database because applying infraction failed.") try: await self.bot.api_client.delete(f"bot/infractions/{id_}") except ResponseCodeError as e: @@ -265,7 +265,7 @@ class InfractionScheduler: {additional_info} """), content=log_content, - footer=f"ID {infraction['id']}" + footer=f"ID: {id_}" ) log.info(f"Applied {purge}{infr_type} infraction #{id_} to {user}.") diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index b77c20434..9649ff852 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -176,7 +176,7 @@ class ModManagement(commands.Cog): if 'expires_at' in request_data: # A scheduled task should only exist if the old infraction wasn't permanent if infraction['expires_at']: - self.infractions_cog.scheduler.cancel(new_infraction['id']) + self.infractions_cog.scheduler.cancel(infraction_id) # If the infraction was not marked as permanent, schedule a new expiration task if request_data['expires_at']: @@ -210,7 +210,8 @@ class ModManagement(commands.Cog): Member: {user_text} Actor: <@{new_infraction['actor']}> Edited by: {ctx.message.author.mention}{log_text} - """) + """), + footer=f"ID: {infraction_id}" ) # endregion -- cgit v1.2.3 From 3824ddbddff8c3191632c0479f0f594985a55b32 Mon Sep 17 00:00:00 2001 From: Kronifer <44979306+Kronifer@users.noreply.github.com> Date: Tue, 4 Jan 2022 13:22:50 -0600 Subject: modlog: wait for guild init before using channel cache Not doing so could cause an error where get_channel would return none for the mod logs channel. --- bot/exts/moderation/modlog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 91709e5e5..fc9204998 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -96,6 +96,7 @@ class ModLog(Cog, name="ModLog"): footer: t.Optional[str] = None, ) -> Context: """Generate log embed and send to logging channel.""" + await self.bot.wait_until_guild_available() # Truncate string directly here to avoid removing newlines embed = discord.Embed( description=text[:4093] + "..." if len(text) > 4096 else text @@ -614,6 +615,7 @@ class ModLog(Cog, name="ModLog"): This is called when a message absent from the cache is deleted. Hence, the message contents aren't logged. """ + await self.bot.wait_until_guild_available() if self.is_channel_ignored(event.channel_id): return @@ -727,6 +729,7 @@ class ModLog(Cog, name="ModLog"): @Cog.listener() async def on_raw_message_edit(self, event: discord.RawMessageUpdateEvent) -> None: """Log raw message edit event to message change log.""" + await self.bot.wait_until_guild_available() try: channel = self.bot.get_channel(int(event.data["channel_id"])) message = await channel.fetch_message(event.message_id) -- cgit v1.2.3 From 3962e0e72e6bf6de4977092740ee79776159845e Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Sat, 8 Jan 2022 12:53:23 -0700 Subject: Restrict allowed mentions for !eval results --- bot/exts/utils/snekbox.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index fbfc58d0b..ef24cbd77 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -7,7 +7,7 @@ from functools import partial from signal import Signals from typing import Optional, Tuple -from discord import HTTPException, Message, NotFound, Reaction, User +from discord import AllowedMentions, HTTPException, Message, NotFound, Reaction, User from discord.ext.commands import Cog, Context, command, guild_only from bot.bot import Bot @@ -218,7 +218,8 @@ class Snekbox(Cog): if filter_triggered: response = await ctx.send("Attempt to circumvent filter detected. Moderator team has been alerted.") else: - response = await ctx.send(msg) + allowed_mentions = AllowedMentions(everyone=False, roles=False, users=[ctx.author]) + response = await ctx.send(msg, allowed_mentions=allowed_mentions) scheduling.create_task(wait_for_deletion(response, (ctx.author.id,)), event_loop=self.bot.loop) log.info(f"{ctx.author}'s job had a return code of {results['returncode']}") -- cgit v1.2.3 From f6b50c17b59f6ec02c9f7e8a7cf6f7ef1a426b7a Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Sat, 8 Jan 2022 14:39:15 -0700 Subject: Fix snekbox tests with new allowed_mentions --- tests/bot/exts/utils/test_snekbox.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/bot/exts/utils/test_snekbox.py b/tests/bot/exts/utils/test_snekbox.py index 321a92445..8bdeedd27 100644 --- a/tests/bot/exts/utils/test_snekbox.py +++ b/tests/bot/exts/utils/test_snekbox.py @@ -2,6 +2,7 @@ import asyncio import unittest from unittest.mock import AsyncMock, MagicMock, Mock, call, create_autospec, patch +from discord import AllowedMentions from discord.ext import commands from bot import constants @@ -201,7 +202,7 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): ctx = MockContext() ctx.message = MockMessage() ctx.send = AsyncMock() - ctx.author.mention = '@LemonLemonishBeard#0042' + ctx.author = MockUser(mention='@LemonLemonishBeard#0042') self.cog.post_eval = AsyncMock(return_value={'stdout': '', 'returncode': 0}) self.cog.get_results_message = MagicMock(return_value=('Return code 0', '')) @@ -213,9 +214,16 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.bot.get_cog.return_value = mocked_filter_cog await self.cog.send_eval(ctx, 'MyAwesomeCode') - ctx.send.assert_called_once_with( + + ctx.send.assert_called_once() + self.assertEqual( + ctx.send.call_args.args[0], '@LemonLemonishBeard#0042 :yay!: Return code 0.\n\n```\n[No output]\n```' ) + allowed_mentions = ctx.send.call_args.kwargs['allowed_mentions'] + expected_allowed_mentions = AllowedMentions(everyone=False, roles=False, users=[ctx.author]) + self.assertEqual(allowed_mentions.to_dict(), expected_allowed_mentions.to_dict()) + self.cog.post_eval.assert_called_once_with('MyAwesomeCode') self.cog.get_status_emoji.assert_called_once_with({'stdout': '', 'returncode': 0}) self.cog.get_results_message.assert_called_once_with({'stdout': '', 'returncode': 0}) @@ -238,10 +246,14 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.bot.get_cog.return_value = mocked_filter_cog await self.cog.send_eval(ctx, 'MyAwesomeCode') - ctx.send.assert_called_once_with( + + ctx.send.assert_called_once() + self.assertEqual( + ctx.send.call_args.args[0], '@LemonLemonishBeard#0042 :yay!: Return code 0.' '\n\n```\nWay too long beard\n```\nFull output: lookatmybeard.com' ) + self.cog.post_eval.assert_called_once_with('MyAwesomeCode') self.cog.get_status_emoji.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0}) self.cog.get_results_message.assert_called_once_with({'stdout': 'Way too long beard', 'returncode': 0}) @@ -263,9 +275,13 @@ class SnekboxTests(unittest.IsolatedAsyncioTestCase): self.bot.get_cog.return_value = mocked_filter_cog await self.cog.send_eval(ctx, 'MyAwesomeCode') - ctx.send.assert_called_once_with( + + ctx.send.assert_called_once() + self.assertEqual( + ctx.send.call_args.args[0], '@LemonLemonishBeard#0042 :nope!: Return code 127.\n\n```\nBeard got stuck in the eval\n```' ) + self.cog.post_eval.assert_called_once_with('MyAwesomeCode') self.cog.get_status_emoji.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}) self.cog.get_results_message.assert_called_once_with({'stdout': 'ERROR', 'returncode': 127}) -- cgit v1.2.3 -- cgit v1.2.3 From 751f3865c007a2bec99444da39107e34df3df618 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 9 Jan 2022 16:08:42 +0000 Subject: Add bot-core as a dependancy --- poetry.lock | 155 +++++++++++++++++++++++++++++---------------------------- pyproject.toml | 2 + 2 files changed, 81 insertions(+), 76 deletions(-) diff --git a/poetry.lock b/poetry.lock index 68eebf8de..9c9cc97ad 100644 --- a/poetry.lock +++ b/poetry.lock @@ -114,29 +114,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.2.0" +version = "21.4.0" description = "Classes Without Boilerplate" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] - -[[package]] -name = "backports.entry-points-selectable" -version = "1.1.1" -description = "Compatibility shim providing selectable entry points for older implementations" -category = "dev" -optional = false -python-versions = ">=2.7" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest", "pytest-flake8", "pytest-cov", "pytest-black (>=0.3.7)", "pytest-mypy", "pytest-checkdocs (>=2.4)", "pytest-enabler (>=1.0.1)"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "beautifulsoup4" @@ -153,6 +141,20 @@ soupsieve = ">1.2" html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "bot-core" +version = "1.2.0" +description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community." +category = "main" +optional = false +python-versions = "3.9.*" + +[package.dependencies] +"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip"} + +[package.source] +type = "url" +url = "https://github.com/python-discord/bot-core/archive/511bcba1b0196cd498c707a525ea56921bd971db.zip" [[package]] name = "certifi" version = "2021.10.8" @@ -190,7 +192,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "charset-normalizer" -version = "2.0.9" +version = "2.0.10" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -295,6 +297,7 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] [package.source] type = "url" url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" + [[package]] name = "distlib" version = "0.3.4" @@ -364,11 +367,11 @@ sgmllib3k = "*" [[package]] name = "filelock" -version = "3.4.0" +version = "3.4.2" description = "A platform independent file lock." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] @@ -506,7 +509,7 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "identify" -version = "2.4.0" +version = "2.4.2" description = "File identification library for Python" category = "dev" optional = false @@ -669,11 +672,11 @@ test = ["docutils", "pytest-cov", "pytest-pycodestyle", "pytest-runner"] [[package]] name = "platformdirs" -version = "2.4.0" +version = "2.4.1" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] @@ -709,7 +712,7 @@ virtualenv = ">=20.0.8" [[package]] name = "psutil" -version = "5.8.0" +version = "5.9.0" description = "Cross-platform lib for process and system monitoring in Python." category = "dev" optional = false @@ -952,7 +955,7 @@ python-versions = "*" [[package]] name = "requests" -version = "2.26.0" +version = "2.27.1" description = "Python HTTP for Humans." category = "main" optional = false @@ -1115,7 +1118,7 @@ python-versions = ">=3.6" [[package]] name = "urllib3" -version = "1.26.7" +version = "1.26.8" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -1128,14 +1131,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.10.0" +version = "20.13.0" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [package.dependencies] -"backports.entry-points-selectable" = ">=1.0.4" distlib = ">=0.3.1,<1" filelock = ">=3.2,<4" platformdirs = ">=2,<3" @@ -1168,7 +1170,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "14ad70153b8c2f4a7e8492bf89f60bf7c468a939da36ce62871b677495f75302" +content-hash = "d625aaae916c07c21080bf504831f5bf4d2bf4f0e3696e404448b43719eff201" [metadata.files] aio-pika = [ @@ -1243,17 +1245,14 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, - {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, -] -"backports.entry-points-selectable" = [ - {file = "backports.entry_points_selectable-1.1.1-py2.py3-none-any.whl", hash = "sha256:7fceed9532a7aa2bd888654a7314f864a3c16a4e710b34a58cfc0f08114c663b"}, - {file = "backports.entry_points_selectable-1.1.1.tar.gz", hash = "sha256:914b21a479fde881635f7af5adc7f6e38d6b274be32269070c53b698c60d5386"}, + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, ] beautifulsoup4 = [ {file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"}, {file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"}, ] +bot-core = [] certifi = [ {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, @@ -1319,8 +1318,8 @@ chardet = [ {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.9.tar.gz", hash = "sha256:b0b883e8e874edfdece9c28f314e3dd5badf067342e42fb162203335ae61aa2c"}, - {file = "charset_normalizer-2.0.9-py3-none-any.whl", hash = "sha256:1eecaa09422db5be9e29d7fc65664e6c33bd06f9ced7838578ba40d58bdf3721"}, + {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, + {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -1420,8 +1419,8 @@ feedparser = [ {file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"}, ] filelock = [ - {file = "filelock-3.4.0-py3-none-any.whl", hash = "sha256:2e139a228bcf56dd8b2274a65174d005c4a6b68540ee0bdbb92c76f43f29f7e8"}, - {file = "filelock-3.4.0.tar.gz", hash = "sha256:93d512b32a23baf4cac44ffd72ccf70732aeff7b8050fcaf6d3ec406d954baf4"}, + {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, + {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, ] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, @@ -1506,8 +1505,8 @@ humanfriendly = [ {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, ] identify = [ - {file = "identify-2.4.0-py2.py3-none-any.whl", hash = "sha256:eba31ca80258de6bb51453084bff4a923187cd2193b9c13710f2516ab30732cc"}, - {file = "identify-2.4.0.tar.gz", hash = "sha256:a33ae873287e81651c7800ca309dc1f84679b763c9c8b30680e16fbfa82f0107"}, + {file = "identify-2.4.2-py2.py3-none-any.whl", hash = "sha256:67c1e66225870dce721228176637a8ef965e8dd58450bcc7592249d0dfc4da6c"}, + {file = "identify-2.4.2.tar.gz", hash = "sha256:93e8ec965e888f2212aa5c24b2b662f4832c39acb1d7196a70ea45acb626a05e"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -1697,8 +1696,8 @@ pip-licenses = [ {file = "pip_licenses-3.5.3-py3-none-any.whl", hash = "sha256:59c148d6a03784bf945d232c0dc0e9de4272a3675acaa0361ad7712398ca86ba"}, ] platformdirs = [ - {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, - {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, + {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, + {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -1709,34 +1708,38 @@ pre-commit = [ {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"}, ] psutil = [ - {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, - {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"}, - {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"}, - {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"}, - {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"}, - {file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"}, - {file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"}, - {file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"}, - {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"}, - {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"}, - {file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"}, - {file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"}, - {file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"}, - {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"}, - {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"}, - {file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"}, - {file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"}, - {file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"}, - {file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"}, - {file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"}, - {file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"}, - {file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"}, - {file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"}, - {file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"}, - {file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"}, - {file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"}, - {file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"}, - {file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"}, + {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b"}, + {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:7336292a13a80eb93c21f36bde4328aa748a04b68c13d01dfddd67fc13fd0618"}, + {file = "psutil-5.9.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:cb8d10461c1ceee0c25a64f2dd54872b70b89c26419e147a05a10b753ad36ec2"}, + {file = "psutil-5.9.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:7641300de73e4909e5d148e90cc3142fb890079e1525a840cf0dfd39195239fd"}, + {file = "psutil-5.9.0-cp27-none-win32.whl", hash = "sha256:ea42d747c5f71b5ccaa6897b216a7dadb9f52c72a0fe2b872ef7d3e1eacf3ba3"}, + {file = "psutil-5.9.0-cp27-none-win_amd64.whl", hash = "sha256:ef216cc9feb60634bda2f341a9559ac594e2eeaadd0ba187a4c2eb5b5d40b91c"}, + {file = "psutil-5.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90a58b9fcae2dbfe4ba852b57bd4a1dded6b990a33d6428c7614b7d48eccb492"}, + {file = "psutil-5.9.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d41f8b3e9ebb6b6110057e40019a432e96aae2008951121ba4e56040b84f3"}, + {file = "psutil-5.9.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:742c34fff804f34f62659279ed5c5b723bb0195e9d7bd9907591de9f8f6558e2"}, + {file = "psutil-5.9.0-cp310-cp310-win32.whl", hash = "sha256:8293942e4ce0c5689821f65ce6522ce4786d02af57f13c0195b40e1edb1db61d"}, + {file = "psutil-5.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9b51917c1af3fa35a3f2dabd7ba96a2a4f19df3dec911da73875e1edaf22a40b"}, + {file = "psutil-5.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e9805fed4f2a81de98ae5fe38b75a74c6e6ad2df8a5c479594c7629a1fe35f56"}, + {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c51f1af02334e4b516ec221ee26b8fdf105032418ca5a5ab9737e8c87dafe203"}, + {file = "psutil-5.9.0-cp36-cp36m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32acf55cb9a8cbfb29167cd005951df81b567099295291bcfd1027365b36591d"}, + {file = "psutil-5.9.0-cp36-cp36m-win32.whl", hash = "sha256:e5c783d0b1ad6ca8a5d3e7b680468c9c926b804be83a3a8e95141b05c39c9f64"}, + {file = "psutil-5.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:d62a2796e08dd024b8179bd441cb714e0f81226c352c802fca0fd3f89eeacd94"}, + {file = "psutil-5.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d00a664e31921009a84367266b35ba0aac04a2a6cad09c550a89041034d19a0"}, + {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7779be4025c540d1d65a2de3f30caeacc49ae7a2152108adeaf42c7534a115ce"}, + {file = "psutil-5.9.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072664401ae6e7c1bfb878c65d7282d4b4391f1bc9a56d5e03b5a490403271b5"}, + {file = "psutil-5.9.0-cp37-cp37m-win32.whl", hash = "sha256:df2c8bd48fb83a8408c8390b143c6a6fa10cb1a674ca664954de193fdcab36a9"}, + {file = "psutil-5.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1d7b433519b9a38192dfda962dd8f44446668c009833e1429a52424624f408b4"}, + {file = "psutil-5.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3400cae15bdb449d518545cbd5b649117de54e3596ded84aacabfbb3297ead2"}, + {file = "psutil-5.9.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2237f35c4bbae932ee98902a08050a27821f8f6dfa880a47195e5993af4702d"}, + {file = "psutil-5.9.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1070a9b287846a21a5d572d6dddd369517510b68710fca56b0e9e02fd24bed9a"}, + {file = "psutil-5.9.0-cp38-cp38-win32.whl", hash = "sha256:76cebf84aac1d6da5b63df11fe0d377b46b7b500d892284068bacccf12f20666"}, + {file = "psutil-5.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:3151a58f0fbd8942ba94f7c31c7e6b310d2989f4da74fcbf28b934374e9bf841"}, + {file = "psutil-5.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:539e429da49c5d27d5a58e3563886057f8fc3868a5547b4f1876d9c0f007bccf"}, + {file = "psutil-5.9.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58c7d923dc209225600aec73aa2c4ae8ea33b1ab31bc11ef8a5933b027476f07"}, + {file = "psutil-5.9.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3611e87eea393f779a35b192b46a164b1d01167c9d323dda9b1e527ea69d697d"}, + {file = "psutil-5.9.0-cp39-cp39-win32.whl", hash = "sha256:4e2fb92e3aeae3ec3b7b66c528981fd327fb93fd906a77215200404444ec1845"}, + {file = "psutil-5.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:7d190ee2eaef7831163f254dc58f6d2e2a22e27382b936aab51c835fc080c3d3"}, + {file = "psutil-5.9.0.tar.gz", hash = "sha256:869842dbd66bb80c3217158e629d6fceaecc3a3166d3d1faee515b05dd26ca25"}, ] ptable = [ {file = "PTable-0.9.2.tar.gz", hash = "sha256:aa7fc151cb40f2dabcd2275ba6f7fd0ff8577a86be3365cd3fb297cbe09cc292"}, @@ -1963,8 +1966,8 @@ regex = [ {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, ] requests = [ - {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"}, - {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"}, + {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, + {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] requests-file = [ {file = "requests-file-1.5.1.tar.gz", hash = "sha256:07d74208d3389d01c38ab89ef403af0cfec63957d53a0081d8eca738d0247d8e"}, @@ -2018,12 +2021,12 @@ typing-extensions = [ {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] urllib3 = [ - {file = "urllib3-1.26.7-py2.py3-none-any.whl", hash = "sha256:c4fdf4019605b6e5423637e01bc9fe4daef873709a7973e195ceba0a62bbc844"}, - {file = "urllib3-1.26.7.tar.gz", hash = "sha256:4987c65554f7a2dbf30c18fd48778ef124af6fab771a377103da0585e2336ece"}, + {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, + {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] virtualenv = [ - {file = "virtualenv-20.10.0-py2.py3-none-any.whl", hash = "sha256:4b02e52a624336eece99c96e3ab7111f469c24ba226a53ec474e8e787b365814"}, - {file = "virtualenv-20.10.0.tar.gz", hash = "sha256:576d05b46eace16a9c348085f7d0dc8ef28713a2cabaa1cf0aea41e8f12c9218"}, + {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"}, + {file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"}, ] wrapt = [ {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, diff --git a/pyproject.toml b/pyproject.toml index 928435975..19e5f78a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,8 @@ license = "MIT" [tool.poetry.dependencies] python = "3.9.*" "discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip"} +# See https://bot-core.pythondiscord.com/ for docs. +bot-core = {url = "https://github.com/python-discord/bot-core/archive/511bcba1b0196cd498c707a525ea56921bd971db.zip"} aio-pika = "~=6.1" aiodns = "~=2.0" aiohttp = "~=3.7" -- cgit v1.2.3 From e9b72ccdf7fb74331176418d12ab298c2e1426ab Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 9 Jan 2022 16:09:23 +0000 Subject: use regex from bot-core for discord invites --- bot/converters.py | 4 ++-- bot/exts/filters/filtering.py | 4 ++-- bot/utils/regex.py | 12 ------------ 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/bot/converters.py b/bot/converters.py index 559e759e1..cd33f5ed0 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -9,6 +9,7 @@ import dateutil.parser import dateutil.tz import discord from aiohttp import ClientConnectorError +from botcore.regex import DISCORD_INVITE from dateutil.relativedelta import relativedelta from discord.ext.commands import BadArgument, Bot, Context, Converter, IDConverter, MemberConverter, UserConverter from discord.utils import escape_markdown, snowflake_time @@ -21,7 +22,6 @@ from bot.exts.info.doc import _inventory_parser from bot.exts.info.tags import TagIdentifier from bot.log import get_logger from bot.utils.extensions import EXTENSIONS, unqualify -from bot.utils.regex import INVITE_RE from bot.utils.time import parse_duration_string if t.TYPE_CHECKING: @@ -72,7 +72,7 @@ class ValidDiscordServerInvite(Converter): async def convert(self, ctx: Context, server_invite: str) -> dict: """Check whether the string is a valid Discord server invite.""" - invite_code = INVITE_RE.match(server_invite) + invite_code = DISCORD_INVITE.match(server_invite) if invite_code: response = await ctx.bot.http_session.get( f"{URLs.discord_invite_api}/{invite_code.group('invite')}" diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index ad904d147..1f83acf9b 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -10,6 +10,7 @@ import discord.errors import regex import tldextract from async_rediscache import RedisCache +from botcore.regex import DISCORD_INVITE from dateutil.relativedelta import relativedelta from discord import Colour, HTTPException, Member, Message, NotFound, TextChannel from discord.ext.commands import Cog @@ -23,7 +24,6 @@ from bot.exts.moderation.modlog import ModLog from bot.log import get_logger from bot.utils import scheduling from bot.utils.messages import format_user -from bot.utils.regex import INVITE_RE log = get_logger(__name__) @@ -566,7 +566,7 @@ class Filtering(Cog): # discord\.gg/gdudes-pony-farm text = text.replace("\\", "") - invites = [m.group("invite") for m in INVITE_RE.finditer(text)] + invites = [m.group("invite") for m in DISCORD_INVITE.finditer(text)] invite_data = dict() for invite in invites: if invite in invite_data: diff --git a/bot/utils/regex.py b/bot/utils/regex.py index 9dc1eba9d..205c3ae34 100644 --- a/bot/utils/regex.py +++ b/bot/utils/regex.py @@ -1,15 +1,3 @@ import re -INVITE_RE = re.compile( - r"(discord([\.,]|dot)gg|" # Could be discord.gg/ - r"discord([\.,]|dot)com(\/|slash)invite|" # or discord.com/invite/ - r"discordapp([\.,]|dot)com(\/|slash)invite|" # or discordapp.com/invite/ - r"discord([\.,]|dot)me|" # or discord.me - r"discord([\.,]|dot)li|" # or discord.li - r"discord([\.,]|dot)io|" # or discord.io. - r"((?[a-zA-Z0-9\-]+)", # the invite code itself - flags=re.IGNORECASE -) MESSAGE_ID_RE = re.compile(r'(?P[0-9]{15,20})$') -- cgit v1.2.3 From 3ff21f1afe37e28b6a185e817691564df7e25821 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 9 Jan 2022 16:10:56 +0000 Subject: Move single-use message ID regex to inside file that uses it This moves the regex closer to the place actually using the regex, and removes the need for a regex.py file entirely. --- bot/monkey_patches.py | 3 ++- bot/utils/regex.py | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 bot/utils/regex.py diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py index b5c0de8d9..4840fa454 100644 --- a/bot/monkey_patches.py +++ b/bot/monkey_patches.py @@ -1,3 +1,4 @@ +import re from datetime import timedelta import arrow @@ -5,9 +6,9 @@ from discord import Forbidden, http from discord.ext import commands from bot.log import get_logger -from bot.utils.regex import MESSAGE_ID_RE log = get_logger(__name__) +MESSAGE_ID_RE = re.compile(r'(?P[0-9]{15,20})$') class Command(commands.Command): diff --git a/bot/utils/regex.py b/bot/utils/regex.py deleted file mode 100644 index 205c3ae34..000000000 --- a/bot/utils/regex.py +++ /dev/null @@ -1,3 +0,0 @@ -import re - -MESSAGE_ID_RE = re.compile(r'(?P[0-9]{15,20})$') -- cgit v1.2.3 From 8845e5bdbe195f2612b3652775378129383c1d6b Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 9 Jan 2022 17:35:18 +0000 Subject: Use codeblock regex from bot-core in snekbox cog --- bot/exts/utils/snekbox.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index ef24cbd77..cc3a2e1d7 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -7,6 +7,7 @@ from functools import partial from signal import Signals from typing import Optional, Tuple +from botcore.regex import FORMATTED_CODE_REGEX, RAW_CODE_REGEX from discord import AllowedMentions, HTTPException, Message, NotFound, Reaction, User from discord.ext.commands import Cog, Context, command, guild_only @@ -20,21 +21,6 @@ from bot.utils.messages import wait_for_deletion log = get_logger(__name__) ESCAPE_REGEX = re.compile("[`\u202E\u200B]{3,}") -FORMATTED_CODE_REGEX = re.compile( - r"(?P(?P```)|``?)" # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block - r"(?(block)(?:(?P[a-z]+)\n)?)" # if we're in a block, match optional language (only letters plus newline) - r"(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code - r"(?P.*?)" # extract all code inside the markup - r"\s*" # any more whitespace before the end of the code markup - r"(?P=delim)", # match the exact same delimiter from the start again - re.DOTALL | re.IGNORECASE # "." also matches newlines, case insensitive -) -RAW_CODE_REGEX = re.compile( - r"^(?:[ \t]*\n)*" # any blank (empty or tabs/spaces only) lines before the code - r"(?P.*?)" # extract all the rest as code - r"\s*$", # any trailing whitespace until the end of the string - re.DOTALL # "." also matches newlines -) MAX_PASTE_LEN = 10000 -- cgit v1.2.3 From dfa5af2036801124a820891dfa69c0c8884aaa03 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 9 Jan 2022 17:12:49 -0800 Subject: Converters: use datetime.timezone instead of dateutil.tz They're equivalent for UTC. Get rid of the extra import. --- bot/converters.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bot/converters.py b/bot/converters.py index b68c4d623..1865c705c 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -6,7 +6,6 @@ from datetime import datetime, timezone from ssl import CertificateError import dateutil.parser -import dateutil.tz import discord from aiohttp import ClientConnectorError from dateutil.relativedelta import relativedelta @@ -454,9 +453,9 @@ class ISODateTime(Converter): raise BadArgument(f"`{datetime_string}` is not a valid ISO-8601 datetime string") if dt.tzinfo: - dt = dt.astimezone(dateutil.tz.UTC) + dt = dt.astimezone(timezone.utc) else: # Without a timezone, assume it represents UTC. - dt = dt.replace(tzinfo=dateutil.tz.UTC) + dt = dt.replace(tzinfo=timezone.utc) return dt -- cgit v1.2.3 From bdf43f4ced428ce092ac2e24cdcf7d47c9995ff0 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 9 Jan 2022 17:17:03 -0800 Subject: Scheduling: add Arrow to schedule_at's type annotations --- bot/utils/scheduling.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/utils/scheduling.py b/bot/utils/scheduling.py index 7b4c8e2de..23acacf74 100644 --- a/bot/utils/scheduling.py +++ b/bot/utils/scheduling.py @@ -5,6 +5,8 @@ import typing as t from datetime import datetime from functools import partial +from arrow import Arrow + from bot.log import get_logger @@ -58,7 +60,7 @@ class Scheduler: self._scheduled_tasks[task_id] = task self._log.debug(f"Scheduled task #{task_id} {id(task)}.") - def schedule_at(self, time: datetime, task_id: t.Hashable, coroutine: t.Coroutine) -> None: + def schedule_at(self, time: t.Union[datetime, Arrow], task_id: t.Hashable, coroutine: t.Coroutine) -> None: """ Schedule `coroutine` to be executed at the given `time`. -- cgit v1.2.3 From 5092205a62ffe97855f377e46e4cfc63836cff19 Mon Sep 17 00:00:00 2001 From: MarkKoz Date: Sun, 9 Jan 2022 17:24:54 -0800 Subject: Time: revise docstrings --- bot/utils/time.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/bot/utils/time.py b/bot/utils/time.py index dfe65369e..a0379c3ef 100644 --- a/bot/utils/time.py +++ b/bot/utils/time.py @@ -50,7 +50,7 @@ class TimestampFormats(Enum): def _stringify_time_unit(value: int, unit: str) -> str: """ - Returns a string to represent a value and time unit, ensuring that it uses the right plural form of the unit. + Return a string to represent a value and time unit, ensuring the unit's correct plural form is used. >>> _stringify_time_unit(1, "seconds") "1 second" @@ -213,7 +213,7 @@ def humanize_delta( ("seconds", delta.seconds), ) - # Add the time units that are >0, but stop at accuracy or max_units. + # Add the time units that are >0, but stop at precision or max_units. time_strings = [] unit_count = 0 for unit, value in units: @@ -224,7 +224,7 @@ def humanize_delta( if unit == precision or unit_count >= max_units: break - # Add the 'and' between the last two units, if necessary + # Add the 'and' between the last two units, if necessary. if len(time_strings) > 1: time_strings[-1] = f"{time_strings[-2]} and {time_strings[-1]}" del time_strings[-2] @@ -240,9 +240,10 @@ def humanize_delta( def parse_duration_string(duration: str) -> Optional[relativedelta]: """ - Converts a `duration` string to a relativedelta object. + Convert a `duration` string to a relativedelta object. + + The following symbols are supported for each unit of time: - The function supports the following symbols for each unit of time: - years: `Y`, `y`, `year`, `years` - months: `m`, `month`, `months` - weeks: `w`, `W`, `week`, `weeks` @@ -250,8 +251,9 @@ def parse_duration_string(duration: str) -> Optional[relativedelta]: - hours: `H`, `h`, `hour`, `hours` - minutes: `M`, `minute`, `minutes` - seconds: `S`, `s`, `second`, `seconds` + The units need to be provided in descending order of magnitude. - If the string does represent a durationdelta object, it will return None. + Return None if the `duration` string cannot be parsed according to the symbols above. """ match = _DURATION_REGEX.fullmatch(duration) if not match: @@ -264,7 +266,7 @@ def parse_duration_string(duration: str) -> Optional[relativedelta]: def relativedelta_to_timedelta(delta: relativedelta) -> datetime.timedelta: - """Converts a relativedelta object to a timedelta object.""" + """Convert a relativedelta object to a timedelta object.""" utcnow = arrow.utcnow() return utcnow + delta - utcnow -- cgit v1.2.3 From 7740f2ed3ce8678da1717d3e75d6a341555707f6 Mon Sep 17 00:00:00 2001 From: Steele Farnsworth Date: Sun, 16 Jan 2022 18:05:52 -0500 Subject: Shorten the `TXT_EMBED_DESCRIPTION` message (#2048) --- bot/exts/filters/antimalware.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/bot/exts/filters/antimalware.py b/bot/exts/filters/antimalware.py index d727f7940..674ca6c8b 100644 --- a/bot/exts/filters/antimalware.py +++ b/bot/exts/filters/antimalware.py @@ -18,14 +18,8 @@ PY_EMBED_DESCRIPTION = ( TXT_LIKE_FILES = {".txt", ".csv", ".json"} TXT_EMBED_DESCRIPTION = ( - "**Uh-oh!** It looks like your message got zapped by our spam filter. " - "We currently don't allow `{blocked_extension}` attachments, " - "so here are some tips to help you travel safely: \n\n" - "• If you attempted to send a message longer than 2000 characters, try shortening your message " - "to fit within the character limit or use a pasting service (see below) \n\n" - "• If you tried to show someone your code, you can use codeblocks \n(run `!code-blocks` in " - "{cmd_channel_mention} for more information) or use a pasting service like: " - f"\n\n{URLs.site_schema}{URLs.site_paste}" + "You either uploaded a `{blocked_extension}` file or entered a message that was too long. " + f"Please use our [{URLs.site_schema}{URLs.site_paste}](paste bin) instead." ) DISALLOWED_EMBED_DESCRIPTION = ( -- cgit v1.2.3 From 0a0d20658f3f0ba76e50e211d4d17c6bc5726f7c Mon Sep 17 00:00:00 2001 From: Nipa-Code Date: Mon, 17 Jan 2022 10:48:43 +0200 Subject: Fix pastebin hyperlink to use correct markdown syntax Fix format from `[link](text)` to `[text](link)` so that the link will be formatted as it should be. --- bot/exts/filters/antimalware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/filters/antimalware.py b/bot/exts/filters/antimalware.py index 674ca6c8b..6cccf3680 100644 --- a/bot/exts/filters/antimalware.py +++ b/bot/exts/filters/antimalware.py @@ -19,7 +19,7 @@ PY_EMBED_DESCRIPTION = ( TXT_LIKE_FILES = {".txt", ".csv", ".json"} TXT_EMBED_DESCRIPTION = ( "You either uploaded a `{blocked_extension}` file or entered a message that was too long. " - f"Please use our [{URLs.site_schema}{URLs.site_paste}](paste bin) instead." + f"Please use our [paste bin]({URLs.site_schema}{URLs.site_paste}) instead." ) DISALLOWED_EMBED_DESCRIPTION = ( -- cgit v1.2.3 From e7f6c4e59ba125d1b50d0892448dc4b4b3cb09df Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Mon, 24 Jan 2022 12:35:27 +0000 Subject: Remove dev-contrib and bot-commands from features list --- bot/exts/info/information.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 29a00ec5d..5b25fd0c3 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -180,8 +180,6 @@ class Information(Cog): if ctx.channel.id in ( *constants.MODERATION_CHANNELS, constants.Channels.dev_core, - constants.Channels.dev_contrib, - constants.Channels.bot_commands ): features = f"\nFeatures: {', '.join(ctx.guild.features)}" else: -- cgit v1.2.3 From 1faadcffddc3d95511acd3b0de309df60d279715 Mon Sep 17 00:00:00 2001 From: Andrew Hong <35881688+NovialRiptide@users.noreply.github.com> Date: Tue, 25 Jan 2022 02:54:46 -0500 Subject: Rename `contributing guidelines` to `contribution guide` --- bot/resources/tags/contribute.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/resources/tags/contribute.md b/bot/resources/tags/contribute.md index 070975646..50c5cd11f 100644 --- a/bot/resources/tags/contribute.md +++ b/bot/resources/tags/contribute.md @@ -7,6 +7,6 @@ Looking to contribute to Open Source Projects for the first time? Want to add a • [Site](https://github.com/python-discord/site) - resources, guides, and more **Where to start** -1. Read our [contributing guidelines](https://pythondiscord.com/pages/guides/pydis-guides/contributing/) +1. Read our [contribution guide](https://pythondiscord.com/pages/guides/pydis-guides/contributing/) 2. Chat with us in <#635950537262759947> if you're ready to jump in or have any questions 3. Open an issue or ask to be assigned to an issue to work on -- cgit v1.2.3 From 61d652a32ce23373e67bb0e1cf985dd4ffc99a18 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 21 Jan 2022 21:30:41 +0000 Subject: Rename voice_ban type to voice_mute This commit changes all of the back-end so that it is in line with the new site API (see this PR https://github.com/python-discord/site/pull/608). This comes with no changes to commands, or functions definitions. --- bot/exts/moderation/infraction/_utils.py | 2 +- bot/exts/moderation/infraction/infractions.py | 20 ++++++++++---------- bot/exts/moderation/voice_gate.py | 4 ++-- .../exts/moderation/infraction/test_infractions.py | 8 ++++---- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index e683c9db4..4df833ffb 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -21,7 +21,7 @@ INFRACTION_ICONS = { "note": (Icons.user_warn, None), "superstar": (Icons.superstarify, Icons.unsuperstarify), "warning": (Icons.user_warn, None), - "voice_ban": (Icons.voice_state_red, Icons.voice_state_green), + "voice_mute": (Icons.voice_state_red, Icons.voice_state_green), } RULES_URL = "https://pythondiscord.com/pages/rules" diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index e495a94b3..72e09cbf4 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -27,7 +27,7 @@ class Infractions(InfractionScheduler, commands.Cog): category_description = "Server moderation tools." def __init__(self, bot: Bot): - super().__init__(bot, supported_infractions={"ban", "kick", "mute", "note", "warning", "voice_ban"}) + super().__init__(bot, supported_infractions={"ban", "kick", "mute", "note", "warning", "voice_mute"}) self.category = "Moderation" self._muted_role = discord.Object(constants.Roles.muted) @@ -273,7 +273,7 @@ class Infractions(InfractionScheduler, commands.Cog): @command(aliases=("uvban",)) async def unvoiceban(self, ctx: Context, user: UnambiguousMemberOrUser) -> None: """Prematurely end the active voice ban infraction for the user.""" - await self.pardon_infraction(ctx, "voice_ban", user) + await self.pardon_infraction(ctx, "voice_mute", user) # endregion # region: Base apply functions @@ -397,10 +397,10 @@ class Infractions(InfractionScheduler, commands.Cog): @respect_role_hierarchy(member_arg=2) async def apply_voice_ban(self, ctx: Context, user: MemberOrUser, reason: t.Optional[str], **kwargs) -> None: """Apply a voice ban infraction with kwargs passed to `post_infraction`.""" - if await _utils.get_active_infraction(ctx, user, "voice_ban"): + if await _utils.get_active_infraction(ctx, user, "voice_mute"): return - infraction = await _utils.post_infraction(ctx, user, "voice_ban", reason, active=True, **kwargs) + infraction = await _utils.post_infraction(ctx, user, "voice_mute", reason, active=True, **kwargs) if infraction is None: return @@ -414,7 +414,7 @@ class Infractions(InfractionScheduler, commands.Cog): if not isinstance(user, Member): return - await user.move_to(None, reason="Disconnected from voice to apply voiceban.") + await user.move_to(None, reason="Disconnected from voice to apply voice mute.") await user.remove_roles(self._voice_verified_role, reason=reason) await self.apply_infraction(ctx, infraction, user, action()) @@ -487,9 +487,9 @@ class Infractions(InfractionScheduler, commands.Cog): # DM user about infraction expiration notified = await _utils.notify_pardon( user=user, - title="Voice ban ended", - content="You have been unbanned and can verify yourself again in the server.", - icon_url=_utils.INFRACTION_ICONS["voice_ban"][1] + title="Voice mute ended", + content="You have been unmuted and can verify yourself again in the server.", + icon_url=_utils.INFRACTION_ICONS["voice_mute"][1] ) log_text["DM"] = "Sent" if notified else "**Failed**" @@ -514,8 +514,8 @@ class Infractions(InfractionScheduler, commands.Cog): return await self.pardon_mute(user_id, guild, reason, notify=notify) elif infraction["type"] == "ban": return await self.pardon_ban(user_id, guild, reason) - elif infraction["type"] == "voice_ban": - return await self.pardon_voice_ban(user_id, guild, notify=notify) + elif infraction["type"] == "voice_mute": + return await self.pardon_voice_mute(user_id, guild, notify=notify) # endregion diff --git a/bot/exts/moderation/voice_gate.py b/bot/exts/moderation/voice_gate.py index a382b13d1..42505b8e7 100644 --- a/bot/exts/moderation/voice_gate.py +++ b/bot/exts/moderation/voice_gate.py @@ -30,7 +30,7 @@ FAILED_MESSAGE = ( MESSAGE_FIELD_MAP = { "joined_at": f"have been on the server for less than {GateConf.minimum_days_member} days", - "voice_banned": "have an active voice ban infraction", + "voice_muted": "have an active voice mute infraction", "total_messages": f"have sent less than {GateConf.minimum_messages} messages", "activity_blocks": f"have been active for fewer than {GateConf.minimum_activity_blocks} ten-minute blocks", } @@ -170,7 +170,7 @@ class VoiceGate(Cog): ctx.author.joined_at > arrow.utcnow() - timedelta(days=GateConf.minimum_days_member) ), "total_messages": data["total_messages"] < GateConf.minimum_messages, - "voice_banned": data["voice_banned"], + "voice_muted": data["voice_muted"], "activity_blocks": data["activity_blocks"] < GateConf.minimum_activity_blocks, } diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 4d01e18a5..a796fd049 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -89,7 +89,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): """Should call infraction pardoning function.""" self.cog.pardon_infraction = AsyncMock() self.assertIsNone(await self.cog.unvoiceban(self.cog, self.ctx, self.user)) - self.cog.pardon_infraction.assert_awaited_once_with(self.ctx, "voice_ban", self.user) + self.cog.pardon_infraction.assert_awaited_once_with(self.ctx, "voice_mute", self.user) @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction") @@ -97,7 +97,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): """Should return early when user already have Voice Ban infraction.""" get_active_infraction.return_value = {"foo": "bar"} self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar")) - get_active_infraction.assert_awaited_once_with(self.ctx, self.user, "voice_ban") + get_active_infraction.assert_awaited_once_with(self.ctx, self.user, "voice_mute") post_infraction_mock.assert_not_awaited() @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @@ -120,7 +120,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): post_infraction_mock.return_value = None self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar", my_kwarg=23)) post_infraction_mock.assert_awaited_once_with( - self.ctx, self.user, "voice_ban", "foobar", active=True, my_kwarg=23 + self.ctx, self.user, "voice_mute", "foobar", active=True, my_kwarg=23 ) @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @@ -187,7 +187,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): user = MockUser() await self.cog.voiceban(self.cog, self.ctx, user, reason=None) - post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_ban", None, active=True, expires_at=None) + post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_mute", None, active=True, expires_at=None) apply_infraction_mock.assert_called_once_with(self.cog, self.ctx, infraction, user, ANY) # Test action -- cgit v1.2.3 From 32d77fa9839eb9d373106700dcc4927851d94635 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 21 Jan 2022 21:34:41 +0000 Subject: Refactor voice_ban function definitions to voice_mute This changes all functions that reference voice_ban to voice_mute instead, which comes with breaking front-end changes. These front end changes are desirable, so that moderators get used to use voice_mute now, rather than voice_ban, in preparation for when we roll out real voice_bans. --- bot/exts/moderation/infraction/infractions.py | 42 ++++++------ .../exts/moderation/infraction/test_infractions.py | 78 +++++++++++----------- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 72e09cbf4..d6580bc14 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -107,8 +107,8 @@ class Infractions(InfractionScheduler, commands.Cog): """ await self.apply_ban(ctx, user, reason, 1, expires_at=duration) - @command(aliases=('vban',)) - async def voiceban( + @command(aliases=("vmute",)) + async def voicemute( self, ctx: Context, user: UnambiguousMemberOrUser, @@ -117,11 +117,11 @@ class Infractions(InfractionScheduler, commands.Cog): reason: t.Optional[str] ) -> None: """ - Permanently ban user from using voice channels. + Permanently mute user in voice channels. - If duration is specified, it temporarily voice bans that user for the given duration. + If duration is specified, it temporarily voice mutes that user for the given duration. """ - await self.apply_voice_ban(ctx, user, reason, expires_at=duration) + await self.apply_voice_mute(ctx, user, reason, expires_at=duration) # endregion # region: Temporary infractions @@ -185,17 +185,17 @@ class Infractions(InfractionScheduler, commands.Cog): """ await self.apply_ban(ctx, user, reason, expires_at=duration) - @command(aliases=("tempvban", "tvban")) - async def tempvoiceban( - self, - ctx: Context, - user: UnambiguousMemberOrUser, - duration: Expiry, - *, - reason: t.Optional[str] + @command(aliases=("tempvmute", "tvmute")) + async def tempvoicemute( + self, + ctx: Context, + user: UnambiguousMemberOrUser, + duration: Expiry, + *, + reason: t.Optional[str] ) -> None: """ - Temporarily voice ban a user for the given reason and duration. + Temporarily voice mute a user for the given reason and duration. A unit of time should be appended to the duration. Units (∗case-sensitive): @@ -209,7 +209,7 @@ class Infractions(InfractionScheduler, commands.Cog): Alternatively, an ISO 8601 timestamp can be provided for the duration. """ - await self.apply_voice_ban(ctx, user, reason, expires_at=duration) + await self.apply_voice_mute(ctx, user, reason, expires_at=duration) # endregion # region: Permanent shadow infractions @@ -270,9 +270,9 @@ class Infractions(InfractionScheduler, commands.Cog): """Prematurely end the active ban infraction for the user.""" await self.pardon_infraction(ctx, "ban", user) - @command(aliases=("uvban",)) - async def unvoiceban(self, ctx: Context, user: UnambiguousMemberOrUser) -> None: - """Prematurely end the active voice ban infraction for the user.""" + @command(aliases=("uvmute",)) + async def unvoicemute(self, ctx: Context, user: UnambiguousMemberOrUser) -> None: + """Prematurely end the active voice mute infraction for the user.""" await self.pardon_infraction(ctx, "voice_mute", user) # endregion @@ -395,8 +395,8 @@ class Infractions(InfractionScheduler, commands.Cog): await bb_cog.apply_unwatch(ctx, user, bb_reason, send_message=False) @respect_role_hierarchy(member_arg=2) - async def apply_voice_ban(self, ctx: Context, user: MemberOrUser, reason: t.Optional[str], **kwargs) -> None: - """Apply a voice ban infraction with kwargs passed to `post_infraction`.""" + async def apply_voice_mute(self, ctx: Context, user: MemberOrUser, reason: t.Optional[str], **kwargs) -> None: + """Apply a voice mute infraction with kwargs passed to `post_infraction`.""" if await _utils.get_active_infraction(ctx, user, "voice_mute"): return @@ -471,7 +471,7 @@ class Infractions(InfractionScheduler, commands.Cog): return log_text - async def pardon_voice_ban( + async def pardon_voice_mute( self, user_id: int, guild: discord.Guild, diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index a796fd049..f89465f84 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -62,8 +62,8 @@ class TruncationTests(unittest.IsolatedAsyncioTestCase): @patch("bot.exts.moderation.infraction.infractions.constants.Roles.voice_verified", new=123456) -class VoiceBanTests(unittest.IsolatedAsyncioTestCase): - """Tests for voice ban related functions and commands.""" +class VoiceMuteTests(unittest.IsolatedAsyncioTestCase): + """Tests for voice mute related functions and commands.""" def setUp(self): self.bot = MockBot() @@ -73,59 +73,59 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): self.ctx = MockContext(bot=self.bot, author=self.mod) self.cog = Infractions(self.bot) - async def test_permanent_voice_ban(self): - """Should call voice ban applying function without expiry.""" - self.cog.apply_voice_ban = AsyncMock() - self.assertIsNone(await self.cog.voiceban(self.cog, self.ctx, self.user, reason="foobar")) - self.cog.apply_voice_ban.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at=None) + async def test_permanent_voice_mute(self): + """Should call voice mute applying function without expiry.""" + self.cog.apply_voice_mute = AsyncMock() + self.assertIsNone(await self.cog.voicemute(self.cog, self.ctx, self.user, reason="foobar")) + self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at=None) - async def test_temporary_voice_ban(self): - """Should call voice ban applying function with expiry.""" - self.cog.apply_voice_ban = AsyncMock() - self.assertIsNone(await self.cog.tempvoiceban(self.cog, self.ctx, self.user, "baz", reason="foobar")) - self.cog.apply_voice_ban.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at="baz") + async def test_temporary_voice_mute(self): + """Should call voice mute applying function with expiry.""" + self.cog.apply_voice_mute = AsyncMock() + self.assertIsNone(await self.cog.tempvoicemute(self.cog, self.ctx, self.user, "baz", reason="foobar")) + self.cog.apply_voice_mute.assert_awaited_once_with(self.ctx, self.user, "foobar", expires_at="baz") - async def test_voice_unban(self): + async def test_voice_unmute(self): """Should call infraction pardoning function.""" self.cog.pardon_infraction = AsyncMock() - self.assertIsNone(await self.cog.unvoiceban(self.cog, self.ctx, self.user)) + self.assertIsNone(await self.cog.unvoicemute(self.cog, self.ctx, self.user)) self.cog.pardon_infraction.assert_awaited_once_with(self.ctx, "voice_mute", self.user) @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction") - async def test_voice_ban_user_have_active_infraction(self, get_active_infraction, post_infraction_mock): - """Should return early when user already have Voice Ban infraction.""" + async def test_voice_mute_user_have_active_infraction(self, get_active_infraction, post_infraction_mock): + """Should return early when user already have Voice Mute infraction.""" get_active_infraction.return_value = {"foo": "bar"} - self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar")) + self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, "foobar")) get_active_infraction.assert_awaited_once_with(self.ctx, self.user, "voice_mute") post_infraction_mock.assert_not_awaited() @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction") - async def test_voice_ban_infraction_post_failed(self, get_active_infraction, post_infraction_mock): + async def test_voice_mute_infraction_post_failed(self, get_active_infraction, post_infraction_mock): """Should return early when posting infraction fails.""" self.cog.mod_log.ignore = MagicMock() get_active_infraction.return_value = None post_infraction_mock.return_value = None - self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar")) + self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, "foobar")) post_infraction_mock.assert_awaited_once() self.cog.mod_log.ignore.assert_not_called() @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction") - async def test_voice_ban_infraction_post_add_kwargs(self, get_active_infraction, post_infraction_mock): - """Should pass all kwargs passed to apply_voice_ban to post_infraction.""" + async def test_voice_mute_infraction_post_add_kwargs(self, get_active_infraction, post_infraction_mock): + """Should pass all kwargs passed to apply_voice_mute to post_infraction.""" get_active_infraction.return_value = None # We don't want that this continue yet post_infraction_mock.return_value = None - self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar", my_kwarg=23)) + self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, "foobar", my_kwarg=23)) post_infraction_mock.assert_awaited_once_with( self.ctx, self.user, "voice_mute", "foobar", active=True, my_kwarg=23 ) @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction") - async def test_voice_ban_mod_log_ignore(self, get_active_infraction, post_infraction_mock): + async def test_voice_mute_mod_log_ignore(self, get_active_infraction, post_infraction_mock): """Should ignore Voice Verified role removing.""" self.cog.mod_log.ignore = MagicMock() self.cog.apply_infraction = AsyncMock() @@ -134,11 +134,11 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): get_active_infraction.return_value = None post_infraction_mock.return_value = {"foo": "bar"} - self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar")) + self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, "foobar")) self.cog.mod_log.ignore.assert_called_once_with(Event.member_update, self.user.id) async def action_tester(self, action, reason: str) -> None: - """Helper method to test voice ban action.""" + """Helper method to test voice mute action.""" self.assertTrue(inspect.iscoroutine(action)) await action @@ -147,7 +147,7 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction") - async def test_voice_ban_apply_infraction(self, get_active_infraction, post_infraction_mock): + async def test_voice_mute_apply_infraction(self, get_active_infraction, post_infraction_mock): """Should ignore Voice Verified role removing.""" self.cog.mod_log.ignore = MagicMock() self.cog.apply_infraction = AsyncMock() @@ -156,22 +156,22 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): post_infraction_mock.return_value = {"foo": "bar"} reason = "foobar" - self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, reason)) + self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, reason)) self.cog.apply_infraction.assert_awaited_once_with(self.ctx, {"foo": "bar"}, self.user, ANY) await self.action_tester(self.cog.apply_infraction.call_args[0][-1], reason) @patch("bot.exts.moderation.infraction.infractions._utils.post_infraction") @patch("bot.exts.moderation.infraction.infractions._utils.get_active_infraction") - async def test_voice_ban_truncate_reason(self, get_active_infraction, post_infraction_mock): - """Should truncate reason for voice ban.""" + async def test_voice_mute_truncate_reason(self, get_active_infraction, post_infraction_mock): + """Should truncate reason for voice mute.""" self.cog.mod_log.ignore = MagicMock() self.cog.apply_infraction = AsyncMock() get_active_infraction.return_value = None post_infraction_mock.return_value = {"foo": "bar"} - self.assertIsNone(await self.cog.apply_voice_ban(self.ctx, self.user, "foobar" * 3000)) + self.assertIsNone(await self.cog.apply_voice_mute(self.ctx, self.user, "foobar" * 3000)) self.cog.apply_infraction.assert_awaited_once_with(self.ctx, {"foo": "bar"}, self.user, ANY) # Test action @@ -180,13 +180,13 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): @autospec(_utils, "post_infraction", "get_active_infraction", return_value=None) @autospec(Infractions, "apply_infraction") - async def test_voice_ban_user_left_guild(self, apply_infraction_mock, post_infraction_mock, _): - """Should voice ban user that left the guild without throwing an error.""" + async def test_voice_mute_user_left_guild(self, apply_infraction_mock, post_infraction_mock, _): + """Should voice mute user that left the guild without throwing an error.""" infraction = {"foo": "bar"} post_infraction_mock.return_value = {"foo": "bar"} user = MockUser() - await self.cog.voiceban(self.cog, self.ctx, user, reason=None) + await self.cog.voicemute(self.cog, self.ctx, user, reason=None) post_infraction_mock.assert_called_once_with(self.ctx, user, "voice_mute", None, active=True, expires_at=None) apply_infraction_mock.assert_called_once_with(self.cog, self.ctx, infraction, user, ANY) @@ -195,22 +195,22 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): self.assertTrue(inspect.iscoroutine(action)) await action - async def test_voice_unban_user_not_found(self): + async def test_voice_unmute_user_not_found(self): """Should include info to return dict when user was not found from guild.""" self.guild.get_member.return_value = None self.guild.fetch_member.side_effect = NotFound(Mock(status=404), "Not found") - result = await self.cog.pardon_voice_ban(self.user.id, self.guild) + result = await self.cog.pardon_voice_mute(self.user.id, self.guild) self.assertEqual(result, {"Info": "User was not found in the guild."}) @patch("bot.exts.moderation.infraction.infractions._utils.notify_pardon") @patch("bot.exts.moderation.infraction.infractions.format_user") - async def test_voice_unban_user_found(self, format_user_mock, notify_pardon_mock): + async def test_voice_unmute_user_found(self, format_user_mock, notify_pardon_mock): """Should add role back with ignoring, notify user and return log dictionary..""" self.guild.get_member.return_value = self.user notify_pardon_mock.return_value = True format_user_mock.return_value = "my-user" - result = await self.cog.pardon_voice_ban(self.user.id, self.guild) + result = await self.cog.pardon_voice_mute(self.user.id, self.guild) self.assertEqual(result, { "Member": "my-user", "DM": "Sent" @@ -219,13 +219,13 @@ class VoiceBanTests(unittest.IsolatedAsyncioTestCase): @patch("bot.exts.moderation.infraction.infractions._utils.notify_pardon") @patch("bot.exts.moderation.infraction.infractions.format_user") - async def test_voice_unban_dm_fail(self, format_user_mock, notify_pardon_mock): + async def test_voice_unmute_dm_fail(self, format_user_mock, notify_pardon_mock): """Should add role back with ignoring, notify user and return log dictionary..""" self.guild.get_member.return_value = self.user notify_pardon_mock.return_value = False format_user_mock.return_value = "my-user" - result = await self.cog.pardon_voice_ban(self.user.id, self.guild) + result = await self.cog.pardon_voice_mute(self.user.id, self.guild) self.assertEqual(result, { "Member": "my-user", "DM": "**Failed**" -- cgit v1.2.3 From 07211bb6eaec2b18a5e13fdfc08ed0f4697a72b6 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 21 Jan 2022 21:35:49 +0000 Subject: Add voice_ban stub commands These stub commands are useful for moderators during the change over from voice_ban to voice_mute, to remind moderators that the command has been changed now. --- bot/exts/moderation/infraction/infractions.py | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index d6580bc14..7c0259b8e 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -107,6 +107,17 @@ class Infractions(InfractionScheduler, commands.Cog): """ await self.apply_ban(ctx, user, reason, 1, expires_at=duration) + @command(aliases=("vban",)) + async def voiceban(self, ctx: Context) -> None: + """ + NOT IMPLEMENTED. + + Permanently ban a user from joining voice channels. + + If duration is specified, it temporarily voice bans that user for the given duration. + """ + await ctx.send(":x: This command is not yet implemented. Maybe you meant to use `voicemute`?") + @command(aliases=("vmute",)) async def voicemute( self, @@ -185,6 +196,15 @@ class Infractions(InfractionScheduler, commands.Cog): """ await self.apply_ban(ctx, user, reason, expires_at=duration) + @command(aliases=("tempvban", "tvban")) + async def tempvoiceban(self, ctx: Context) -> None: + """ + NOT IMPLEMENTED. + + Temporarily voice bans that user for the given duration. + """ + await ctx.send(":x: This command is not yet implemented. Maybe you meant to use `tempvoicemute`?") + @command(aliases=("tempvmute", "tvmute")) async def tempvoicemute( self, @@ -270,6 +290,15 @@ class Infractions(InfractionScheduler, commands.Cog): """Prematurely end the active ban infraction for the user.""" await self.pardon_infraction(ctx, "ban", user) + @command(aliases=("uvban",)) + async def unvoiceban(self, ctx: Context) -> None: + """ + NOT IMPLEMENTED. + + Temporarily voice bans that user for the given duration. + """ + await ctx.send(":x: This command is not yet implemented. Maybe you meant to use `unvoicemute`?") + @command(aliases=("uvmute",)) async def unvoicemute(self, ctx: Context, user: UnambiguousMemberOrUser) -> None: """Prematurely end the active voice mute infraction for the user.""" -- cgit v1.2.3 From 10186afb0a376c82f71235b06f8f5af87f41bcb6 Mon Sep 17 00:00:00 2001 From: Izan Date: Sat, 22 Jan 2022 16:56:56 +0000 Subject: Add missing arguments to `notify_infraction` call Fixes an issue caused by #1951. --- bot/exts/moderation/infraction/superstarify.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index a037ca1be..3f1bffd76 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -57,7 +57,9 @@ class Superstarify(InfractionScheduler, Cog): return infraction = active_superstarifies[0] - forced_nick = self.get_nick(infraction["id"], before.id) + infr_id = infraction["id"] + + forced_nick = self.get_nick(infr_id, before.id) if after.display_name == forced_nick: return # Nick change was triggered by this event. Ignore. @@ -67,11 +69,13 @@ class Superstarify(InfractionScheduler, Cog): ) await after.edit( nick=forced_nick, - reason=f"Superstarified member tried to escape the prison: {infraction['id']}" + reason=f"Superstarified member tried to escape the prison: {infr_id}" ) notified = await _utils.notify_infraction( + bot=self.bot, user=after, + infr_id=infr_id, infr_type="Superstarify", expires_at=time.discord_timestamp(infraction["expires_at"]), reason=( -- cgit v1.2.3 From d7095c40e83800cc379115ef45bd987a5deb4649 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 25 Jan 2022 21:51:10 +0000 Subject: setuptools use stdlib distutils over embedded This is caused by an upstream issue with setuptools 60.* (via virtualenv) changeing the default to using the setuptools-embedded distutils rather than the stdlib distutils, which breaks within pip's isolated builds. This is explained quite well here https://github.com/pre-commit/pre-commit/issues/2178#issuecomment-1002163763 --- .github/workflows/lint-test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml index f2c9dfb6c..57cc544d9 100644 --- a/.github/workflows/lint-test.yml +++ b/.github/workflows/lint-test.yml @@ -46,6 +46,10 @@ jobs: PYTHONUSERBASE: ${{ github.workspace }}/.cache/py-user-base PRE_COMMIT_HOME: ${{ github.workspace }}/.cache/pre-commit-cache + # See https://github.com/pre-commit/pre-commit/issues/2178#issuecomment-1002163763 + # for why we set this. + SETUPTOOLS_USE_DISTUTILS: stdlib + steps: - name: Add custom PYTHONUSERBASE to PATH run: echo '${{ env.PYTHONUSERBASE }}/bin/' >> $GITHUB_PATH -- cgit v1.2.3 From 4383c139637025917645300cda8047f32926aa99 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Tue, 25 Jan 2022 21:51:56 +0000 Subject: Add missing restart-policy to metricity container --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index 869d9acb6..ce78f65aa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -38,6 +38,7 @@ services: metricity: << : *logging + << : *restart_policy restart: on-failure # USE_METRICITY=false will stop the container, so this ensures it only restarts on error depends_on: postgres: -- cgit v1.2.3 From 30fee62d259e71619017420a356ed0f55bf21d2b Mon Sep 17 00:00:00 2001 From: minalike Date: Thu, 27 Jan 2022 00:32:59 -0500 Subject: Add embed message mentioning help channel claimant Update docstring --- bot/exts/help_channels/_cog.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 60209ba6e..541c791e5 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -105,10 +105,18 @@ class HelpChannels(commands.Cog): """ Claim the channel in which the question `message` was sent. - Move the channel to the In Use category and pin the `message`. Add a cooldown to the - claimant to prevent them from asking another question. Lastly, make a new channel available. + Send an embed stating the claimant, move the channel to the In Use category, and pin the `message`. + Add a cooldown to the claimant to prevent them from asking another question. + Lastly, make a new channel available. """ log.info(f"Channel #{message.channel} was claimed by `{message.author.id}`.") + + embed = discord.Embed( + description=f"Channel claimed by {message.author.mention}.", + color=constants.Colours.bright_green, + ) + await message.channel.send(embed=embed) + await self.move_to_in_use(message.channel) # Handle odd edge case of `message.author` not being a `discord.Member` (see bot#1839) -- cgit v1.2.3 From 01df37482ffa39138173070f97b7c9ec6e92a055 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 27 Jan 2022 20:31:23 +0000 Subject: Use `voice_gate_blocked` field from API for voice_gate This new field is true when the user has any voice exception, which means the user is blocked from receiving the role. --- bot/exts/moderation/voice_gate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/voice_gate.py b/bot/exts/moderation/voice_gate.py index 42505b8e7..fa66b00dd 100644 --- a/bot/exts/moderation/voice_gate.py +++ b/bot/exts/moderation/voice_gate.py @@ -30,7 +30,7 @@ FAILED_MESSAGE = ( MESSAGE_FIELD_MAP = { "joined_at": f"have been on the server for less than {GateConf.minimum_days_member} days", - "voice_muted": "have an active voice mute infraction", + "voice_gate_blocked": "have an active voice infraction", "total_messages": f"have sent less than {GateConf.minimum_messages} messages", "activity_blocks": f"have been active for fewer than {GateConf.minimum_activity_blocks} ten-minute blocks", } @@ -170,7 +170,7 @@ class VoiceGate(Cog): ctx.author.joined_at > arrow.utcnow() - timedelta(days=GateConf.minimum_days_member) ), "total_messages": data["total_messages"] < GateConf.minimum_messages, - "voice_muted": data["voice_muted"], + "voice_gate_blocked": data["voice_gate_blocked"], "activity_blocks": data["activity_blocks"] < GateConf.minimum_activity_blocks, } -- cgit v1.2.3 From aff3e35a57cc231b73c7ffca568b20e8f83dd22b Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 22:29:22 +0000 Subject: ✏️`LATEST_MESSSAGE` -> `LATEST_MESSAGE` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_channel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index e43c1e789..940868245 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -22,7 +22,7 @@ class ClosingReason(Enum): """All possible closing reasons for help channels.""" COMMAND = "command" - LATEST_MESSSAGE = "auto.latest_message" + LATEST_MESSAGE = "auto.latest_message" CLAIMANT_TIMEOUT = "auto.claimant_timeout" OTHER_TIMEOUT = "auto.other_timeout" DELETED = "auto.deleted" @@ -75,7 +75,7 @@ async def get_closing_time(channel: discord.TextChannel, init_done: bool) -> t.T # Use the greatest offset to avoid the possibility of prematurely closing the channel. time = Arrow.fromdatetime(msg.created_at) + timedelta(minutes=idle_minutes_claimant) - reason = ClosingReason.DELETED if is_empty else ClosingReason.LATEST_MESSSAGE + reason = ClosingReason.DELETED if is_empty else ClosingReason.LATEST_MESSAGE return time, reason claimant_time = Arrow.utcfromtimestamp(claimant_time) -- cgit v1.2.3 From 73fe24f447c4626bbf8a2b7a235b8971f0d82f25 Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:05:31 +0000 Subject: 🔧 Add `notify_running_low` config values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config-default.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/config-default.yml b/config-default.yml index 583733fda..fcb1583a8 100644 --- a/config-default.yml +++ b/config-default.yml @@ -513,19 +513,16 @@ help_channels: # Prefix for help channel names name_prefix: 'help-' - # Notify if more available channels are needed but there are no more dormant ones - notify: true + notify_channel: *HELPERS # Channel in which to send notifications messages + notify_minutes: 15 # Minimum interval between helper notifications, used by both none_remaining and running_low - # Channel in which to send notifications - notify_channel: *HELPERS - - # Minimum interval between helper notifications - notify_minutes: 15 - - # Mention these roles in notifications - notify_roles: + notify_none_remaining: true # Pinging notification for the Helper role when no dormant channels remain + notify_none_remaining_roles: # Mention these roles in the non_remaining notification - *HELPERS_ROLE + notify_running_low: true # Non-pinging notification which is triggered when the channel count is equal or less than the threshold + notify_running_low_threshold: 4 # The amount of channels at which a running_low notification will be sent + redirect_output: delete_delay: 15 -- cgit v1.2.3 From 3f1089f701677233ee1f485979ed755c5285f5be Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:09:59 +0000 Subject: 🔧 Add `notify_running_low` config values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/constants.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 1b713a7e3..ecb1ed81b 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -619,10 +619,12 @@ class HelpChannels(metaclass=YAMLGetter): max_available: int max_total_channels: int name_prefix: str - notify: bool notify_channel: int notify_minutes: int - notify_roles: List[int] + notify_none_remaining: bool + notify_none_remaining_roles: List[int] + notify_running_low: bool + notify_running_low_threshold: int class RedirectOutput(metaclass=YAMLGetter): -- cgit v1.2.3 From d5606a7d49f420b82d1826f0f5d96fb88f3942ef Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:11:13 +0000 Subject: 🔧 Add `notify_running_low` config values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 241dd606c..cdc015a02 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -137,7 +137,7 @@ async def notify(channel: discord.TextChannel, last_notification: t.Optional[Arr * `HelpChannels.notify_minutes` - minimum interval between notifications * `HelpChannels.notify_roles` - roles mentioned in notifications """ - if not constants.HelpChannels.notify: + if not constants.HelpChannels.notify_none_remaining: return log.trace("Notifying about lack of channels.") @@ -156,8 +156,8 @@ async def notify(channel: discord.TextChannel, last_notification: t.Optional[Arr try: log.trace("Sending notification message.") - mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_roles) - allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_roles] + mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) + allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] message = await channel.send( f"{mentions} A new available help channel is needed but there " -- cgit v1.2.3 From e91ab75cc66976b5cee8041a43944034cbcf4335 Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:14:45 +0000 Subject: ♻️Rename `notify` -> `notify_none_remaining` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 2 +- bot/exts/help_channels/_message.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 541c791e5..46f09f29a 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -236,7 +236,7 @@ class HelpChannels(commands.Cog): if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") notify_channel = self.bot.get_channel(constants.HelpChannels.notify_channel) - last_notification = await _message.notify(notify_channel, self.last_notification) + last_notification = await _message.notify_none_remaining(notify_channel, self.last_notification) if last_notification: self.last_notification = last_notification self.bot.stats.incr("help.out_of_channel_alerts") diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index cdc015a02..70ae8b062 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -124,7 +124,7 @@ async def dm_on_open(message: discord.Message) -> None: ) -async def notify(channel: discord.TextChannel, last_notification: t.Optional[Arrow]) -> t.Optional[Arrow]: +async def notify_none_remaining(channel: discord.TextChannel, last_notification: t.Optional[Arrow]) -> t.Optional[Arrow]: """ Send a message in `channel` notifying about a lack of available help channels. -- cgit v1.2.3 From 289629cfcadf987efb28d8969516a22db52f4a77 Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:18:52 +0000 Subject: 📝 Update `notify_none_remaining` docstring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 70ae8b062..e21e9a450 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -126,16 +126,15 @@ async def dm_on_open(message: discord.Message) -> None: async def notify_none_remaining(channel: discord.TextChannel, last_notification: t.Optional[Arrow]) -> t.Optional[Arrow]: """ - Send a message in `channel` notifying about a lack of available help channels. + Send a pinging message in `channel` notifying about there being no dormant channels remaining. If a notification was sent, return the time at which the message was sent. Otherwise, return None. Configuration: - - * `HelpChannels.notify` - toggle notifications - * `HelpChannels.notify_minutes` - minimum interval between notifications - * `HelpChannels.notify_roles` - roles mentioned in notifications + * `HelpChannels.notify_minutes` - minimum interval between notifications + * `HelpChannels.notify_none_remaining` - toggle notifications + * `HelpChannels.notify_none_remaining_roles` - roles mentioned in notifications """ if not constants.HelpChannels.notify_none_remaining: return -- cgit v1.2.3 From 9361f32aa8c3b201dc62edf0153b71c053411712 Mon Sep 17 00:00:00 2001 From: GDWR Date: Mon, 31 Jan 2022 23:25:36 +0000 Subject: 💡 Update docstrings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index e21e9a450..b6b172e77 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -133,7 +133,7 @@ async def notify_none_remaining(channel: discord.TextChannel, last_notification: Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications - * `HelpChannels.notify_none_remaining` - toggle notifications + * `HelpChannels.notify_none_remaining` - toggle none_remaining notifications * `HelpChannels.notify_none_remaining_roles` - roles mentioned in notifications """ if not constants.HelpChannels.notify_none_remaining: @@ -171,6 +171,21 @@ async def notify_none_remaining(channel: discord.TextChannel, last_notification: log.exception("Failed to send notification about lack of dormant channels!") +async def notify_running_low(): + """ + Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. + + If a notification was sent, return the time at which the message was sent. + Otherwise, return None. + + Configuration: + * `HelpChannels.notify_minutes` - minimum interval between notifications + * `HelpChannels.notify_running_low` - toggle running_low notifications + * `HelpChannels.notify_running_low_threshold` - minimum amount of channels to trigger running_low notifications + """ + ... + + async def pin(message: discord.Message) -> None: """Pin an initial question `message` and store it in a cache.""" if await pin_wrapper(message.id, message.channel, pin=True): -- cgit v1.2.3 From 554e520aa0fbd928c02e90b3d263d64e3a4f6daf Mon Sep 17 00:00:00 2001 From: GDWR Date: Tue, 1 Feb 2022 00:24:12 +0000 Subject: ✨ Notify running low on channels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 19 ++++++++++++++----- bot/exts/help_channels/_message.py | 38 ++++++++++++++------------------------ config-default.yml | 2 +- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 46f09f29a..85799516c 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -78,7 +78,10 @@ class HelpChannels(commands.Cog): self.channel_queue: asyncio.Queue[discord.TextChannel] = None self.name_queue: t.Deque[str] = None - self.last_notification: t.Optional[arrow.Arrow] = None + # Notifications + self.notify_interval_seconds = (constants.HelpChannels.notify_minutes * 60) + self.last_none_remaining_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') + self.last_running_low_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') self.dynamic_message: t.Optional[int] = None self.available_help_channels: t.Set[discord.TextChannel] = set() @@ -229,16 +232,22 @@ class HelpChannels(commands.Cog): try: channel = self.channel_queue.get_nowait() + + within_interval = (arrow.utcnow() - self.last_running_low_notification).seconds >= self.notify_interval_seconds + if within_interval and self.channel_queue.qsize() <= constants.HelpChannels.notify_running_low_threshold: + await _message.notify_running_low(self.bot.get_channel(constants.HelpChannels.notify_channel), self.channel_queue.qsize()) + self.last_running_low_notification = arrow.utcnow() + except asyncio.QueueEmpty: log.info("No candidate channels in the queue; creating a new channel.") channel = await self.create_dormant() if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") - notify_channel = self.bot.get_channel(constants.HelpChannels.notify_channel) - last_notification = await _message.notify_none_remaining(notify_channel, self.last_notification) - if last_notification: - self.last_notification = last_notification + + if (arrow.utcnow() - self.last_none_remaining_notification).seconds >= self.notify_interval_seconds: + await _message.notify_none_remaining(self.bot.get_channel(constants.HelpChannels.notify_channel)) + self.last_none_remaining_notification = arrow.utcnow() self.bot.stats.incr("help.out_of_channel_alerts") channel = await self.wait_for_dormant_channel() diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index b6b172e77..d53a03b77 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -124,13 +124,10 @@ async def dm_on_open(message: discord.Message) -> None: ) -async def notify_none_remaining(channel: discord.TextChannel, last_notification: t.Optional[Arrow]) -> t.Optional[Arrow]: +async def notify_none_remaining(channel: discord.TextChannel) -> None: """ Send a pinging message in `channel` notifying about there being no dormant channels remaining. - If a notification was sent, return the time at which the message was sent. - Otherwise, return None. - Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications * `HelpChannels.notify_none_remaining` - toggle none_remaining notifications @@ -141,49 +138,42 @@ async def notify_none_remaining(channel: discord.TextChannel, last_notification: log.trace("Notifying about lack of channels.") - if last_notification: - elapsed = (arrow.utcnow() - last_notification).seconds - minimum_interval = constants.HelpChannels.notify_minutes * 60 - should_send = elapsed >= minimum_interval - else: - should_send = True - - if not should_send: - log.trace("Notification not sent because it's too recent since the previous one.") - return - try: log.trace("Sending notification message.") mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] - message = await channel.send( + await channel.send( f"{mentions} A new available help channel is needed but there " - f"are no more dormant ones. Consider freeing up some in-use channels manually by " + "are no more dormant ones. Consider freeing up some in-use channels manually by " f"using the `{constants.Bot.prefix}dormant` command within the channels.", allowed_mentions=discord.AllowedMentions(everyone=False, roles=allowed_roles) ) - - return Arrow.fromdatetime(message.created_at) except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about lack of dormant channels!") -async def notify_running_low(): +async def notify_running_low(channel: discord.TextChannel, number_of_channels_left: int) -> None: """ Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. - - If a notification was sent, return the time at which the message was sent. - Otherwise, return None. + Including the amount of channels left in dormant. Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications * `HelpChannels.notify_running_low` - toggle running_low notifications * `HelpChannels.notify_running_low_threshold` - minimum amount of channels to trigger running_low notifications """ - ... + if not constants.HelpChannels.notify_running_low: + return + + log.trace("Notifying about getting close to no dormant channels.") + + await channel.send( + f"There are only {number_of_channels_left} dormant channels left. " + "Consider participating in some help channels so that we don't run out." + ) async def pin(message: discord.Message) -> None: diff --git a/config-default.yml b/config-default.yml index fcb1583a8..6ad471cbd 100644 --- a/config-default.yml +++ b/config-default.yml @@ -514,7 +514,7 @@ help_channels: name_prefix: 'help-' notify_channel: *HELPERS # Channel in which to send notifications messages - notify_minutes: 15 # Minimum interval between helper notifications, used by both none_remaining and running_low + notify_minutes: 15 # Minimum interval between none_remaining or running_low notifications notify_none_remaining: true # Pinging notification for the Helper role when no dormant channels remain notify_none_remaining_roles: # Mention these roles in the non_remaining notification -- cgit v1.2.3 From 9fa586a85f3e9707e13fa19919d1c8519227f087 Mon Sep 17 00:00:00 2001 From: GDWR Date: Tue, 1 Feb 2022 00:30:05 +0000 Subject: 🚨 Linting fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 8 ++++++-- bot/exts/help_channels/_message.py | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 85799516c..5dfb09b64 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -233,9 +233,13 @@ class HelpChannels(commands.Cog): try: channel = self.channel_queue.get_nowait() - within_interval = (arrow.utcnow() - self.last_running_low_notification).seconds >= self.notify_interval_seconds + time_since_last_notify_seconds = (arrow.utcnow() - self.last_running_low_notification).seconds + within_interval = time_since_last_notify_seconds >= self.notify_interval_seconds if within_interval and self.channel_queue.qsize() <= constants.HelpChannels.notify_running_low_threshold: - await _message.notify_running_low(self.bot.get_channel(constants.HelpChannels.notify_channel), self.channel_queue.qsize()) + await _message.notify_running_low( + self.bot.get_channel(constants.HelpChannels.notify_channel), + self.channel_queue.qsize() + ) self.last_running_low_notification = arrow.utcnow() except asyncio.QueueEmpty: diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index d53a03b77..7d70c9f00 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -1,7 +1,6 @@ import textwrap import typing as t -import arrow import discord from arrow import Arrow @@ -158,7 +157,8 @@ async def notify_none_remaining(channel: discord.TextChannel) -> None: async def notify_running_low(channel: discord.TextChannel, number_of_channels_left: int) -> None: """ Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. - Including the amount of channels left in dormant. + + This will include the number of dormant channels left `number_of_channels_left` Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications -- cgit v1.2.3 From 3bfd75bda26b73dde5a838d4f7bb979170db9304 Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 18:41:16 +0000 Subject: 👌 Remove redundant parenthesis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 5dfb09b64..41d5bbe72 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -79,7 +79,7 @@ class HelpChannels(commands.Cog): self.name_queue: t.Deque[str] = None # Notifications - self.notify_interval_seconds = (constants.HelpChannels.notify_minutes * 60) + self.notify_interval_seconds = constants.HelpChannels.notify_minutes * 60 self.last_none_remaining_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') self.last_running_low_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') -- cgit v1.2.3 From e05f3c9124046a304ad402b54a786b7b9f3cdfb1 Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 18:45:29 +0000 Subject: 💡 Comment usage of arbitrarily old date MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 41d5bbe72..8c93b084d 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -80,6 +80,7 @@ class HelpChannels(commands.Cog): # Notifications self.notify_interval_seconds = constants.HelpChannels.notify_minutes * 60 + # Using a very old date so that we don't have to use Optional typing. self.last_none_remaining_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') self.last_running_low_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') -- cgit v1.2.3 From 1a53b6b7ea9203e348f4b1b1678c30b140e16544 Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 19:51:52 +0000 Subject: ♻️Move notifications into `_message.py` with predicate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 33 ++++++++++++------------ bot/exts/help_channels/_message.py | 52 ++++++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 8c93b084d..aefa0718e 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -79,7 +79,6 @@ class HelpChannels(commands.Cog): self.name_queue: t.Deque[str] = None # Notifications - self.notify_interval_seconds = constants.HelpChannels.notify_minutes * 60 # Using a very old date so that we don't have to use Optional typing. self.last_none_remaining_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') self.last_running_low_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') @@ -233,29 +232,31 @@ class HelpChannels(commands.Cog): try: channel = self.channel_queue.get_nowait() - - time_since_last_notify_seconds = (arrow.utcnow() - self.last_running_low_notification).seconds - within_interval = time_since_last_notify_seconds >= self.notify_interval_seconds - if within_interval and self.channel_queue.qsize() <= constants.HelpChannels.notify_running_low_threshold: - await _message.notify_running_low( - self.bot.get_channel(constants.HelpChannels.notify_channel), - self.channel_queue.qsize() - ) - self.last_running_low_notification = arrow.utcnow() - except asyncio.QueueEmpty: log.info("No candidate channels in the queue; creating a new channel.") channel = await self.create_dormant() if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") + last_notification = await _message.notify_none_remaining( + self.bot.get_channel(constants.HelpChannels.notify_channel), + self.last_none_remaining_notification + ) + + if last_notification: + self.last_none_remaining_notification = last_notification + + channel = await self.wait_for_dormant_channel() # Blocks until a new channel is available - if (arrow.utcnow() - self.last_none_remaining_notification).seconds >= self.notify_interval_seconds: - await _message.notify_none_remaining(self.bot.get_channel(constants.HelpChannels.notify_channel)) - self.last_none_remaining_notification = arrow.utcnow() - self.bot.stats.incr("help.out_of_channel_alerts") + else: + last_notification = await _message.notify_running_low( + self.bot.get_channel(constants.HelpChannels.notify_channel), + self.channel_queue.qsize(), + self.last_running_low_notification + ) - channel = await self.wait_for_dormant_channel() + if last_notification: + self.last_running_low_notification = last_notification return channel diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 7d70c9f00..097e648e0 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -1,6 +1,7 @@ import textwrap import typing as t +import arrow import discord from arrow import Arrow @@ -123,7 +124,7 @@ async def dm_on_open(message: discord.Message) -> None: ) -async def notify_none_remaining(channel: discord.TextChannel) -> None: +async def notify_none_remaining(channel: discord.TextChannel, last_notification: Arrow) -> t.Optional[Arrow]: """ Send a pinging message in `channel` notifying about there being no dormant channels remaining. @@ -133,16 +134,18 @@ async def notify_none_remaining(channel: discord.TextChannel) -> None: * `HelpChannels.notify_none_remaining_roles` - roles mentioned in notifications """ if not constants.HelpChannels.notify_none_remaining: - return + return None - log.trace("Notifying about lack of channels.") + if (arrow.utcnow() - last_notification).seconds < (constants.HelpChannels.notify_minutes * 60): + log.trace("Did not send none_remaining notification as it hasn't been enough time since the last one.") + return None - try: - log.trace("Sending notification message.") + log.trace("Notifying about lack of channels.") - mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) - allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] + mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) + allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] + try: await channel.send( f"{mentions} A new available help channel is needed but there " "are no more dormant ones. Consider freeing up some in-use channels manually by " @@ -152,9 +155,16 @@ async def notify_none_remaining(channel: discord.TextChannel) -> None: except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about lack of dormant channels!") + finally: + bot.instance.stats.incr("help.out_of_channel_alerts") + return arrow.utcnow() -async def notify_running_low(channel: discord.TextChannel, number_of_channels_left: int) -> None: +async def notify_running_low( + channel: discord.TextChannel, + number_of_channels_left: int, + last_notification: Arrow +) -> t.Optional[Arrow]: """ Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. @@ -166,14 +176,28 @@ async def notify_running_low(channel: discord.TextChannel, number_of_channels_le * `HelpChannels.notify_running_low_threshold` - minimum amount of channels to trigger running_low notifications """ if not constants.HelpChannels.notify_running_low: - return + return None - log.trace("Notifying about getting close to no dormant channels.") + if number_of_channels_left > constants.HelpChannels.notify_running_low_threshold: + log.trace("Did not send notify_running_low notification as the threshold was not met.") + return None - await channel.send( - f"There are only {number_of_channels_left} dormant channels left. " - "Consider participating in some help channels so that we don't run out." - ) + if (arrow.utcnow() - last_notification).seconds < (constants.HelpChannels.notify_minutes * 60): + log.trace("Did not send notify_running_low notification as it hasn't been enough time since the last one.") + return None + + log.trace("Notifying about getting close to no dormant channels.") + try: + await channel.send( + f"There are only {number_of_channels_left} dormant channels left. " + "Consider participating in some help channels so that we don't run out." + ) + except Exception: + # Handle it here cause this feature isn't critical for the functionality of the system. + log.exception("Failed to send notification about running low of dormant channels!") + finally: + bot.instance.stats.incr("help.running_low_alerts") + return arrow.utcnow() async def pin(message: discord.Message) -> None: -- cgit v1.2.3 From e02aee662043888ffcb982af694cea9da07403f2 Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 19:57:12 +0000 Subject: 👌 Remove the need to pass in channel via arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 11 ++--------- bot/exts/help_channels/_message.py | 17 +++++++++++------ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index aefa0718e..78b01aa03 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -238,10 +238,7 @@ class HelpChannels(commands.Cog): if not channel: log.info("Couldn't create a candidate channel; waiting to get one from the queue.") - last_notification = await _message.notify_none_remaining( - self.bot.get_channel(constants.HelpChannels.notify_channel), - self.last_none_remaining_notification - ) + last_notification = await _message.notify_none_remaining(self.last_none_remaining_notification) if last_notification: self.last_none_remaining_notification = last_notification @@ -249,11 +246,7 @@ class HelpChannels(commands.Cog): channel = await self.wait_for_dormant_channel() # Blocks until a new channel is available else: - last_notification = await _message.notify_running_low( - self.bot.get_channel(constants.HelpChannels.notify_channel), - self.channel_queue.qsize(), - self.last_running_low_notification - ) + last_notification = await _message.notify_running_low(self.channel_queue.qsize(), self.last_running_low_notification) if last_notification: self.last_running_low_notification = last_notification diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 097e648e0..554aac7b4 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -124,7 +124,7 @@ async def dm_on_open(message: discord.Message) -> None: ) -async def notify_none_remaining(channel: discord.TextChannel, last_notification: Arrow) -> t.Optional[Arrow]: +async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: """ Send a pinging message in `channel` notifying about there being no dormant channels remaining. @@ -145,6 +145,10 @@ async def notify_none_remaining(channel: discord.TextChannel, last_notification: mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] + channel = bot.instance.get_channel(constants.HelpChannels.notify_channel) + if channel is None: + log.trace("Did not send none_remaining notification as the notification channel couldn't be gathered.") + try: await channel.send( f"{mentions} A new available help channel is needed but there " @@ -160,11 +164,7 @@ async def notify_none_remaining(channel: discord.TextChannel, last_notification: return arrow.utcnow() -async def notify_running_low( - channel: discord.TextChannel, - number_of_channels_left: int, - last_notification: Arrow -) -> t.Optional[Arrow]: +async def notify_running_low(number_of_channels_left: int, last_notification: Arrow) -> t.Optional[Arrow]: """ Send a non-pinging message in `channel` notifying about there being a low amount of dormant channels. @@ -187,6 +187,11 @@ async def notify_running_low( return None log.trace("Notifying about getting close to no dormant channels.") + + channel = bot.instance.get_channel(constants.HelpChannels.notify_channel) + if channel is None: + log.trace("Did not send notify_running notification as the notification channel couldn't be gathered.") + try: await channel.send( f"There are only {number_of_channels_left} dormant channels left. " -- cgit v1.2.3 From e566a206b67af48c42159a5c0969fbddee76da1d Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 20:11:05 +0000 Subject: 🚨 Linting fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_cog.py | 5 ++++- bot/exts/help_channels/_message.py | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 78b01aa03..0fd631a6e 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -246,7 +246,10 @@ class HelpChannels(commands.Cog): channel = await self.wait_for_dormant_channel() # Blocks until a new channel is available else: - last_notification = await _message.notify_running_low(self.channel_queue.qsize(), self.last_running_low_notification) + last_notification = await _message.notify_running_low( + self.channel_queue.qsize(), + self.last_running_low_notification + ) if last_notification: self.last_running_low_notification = last_notification diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 554aac7b4..d867dd93d 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -161,7 +161,8 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: log.exception("Failed to send notification about lack of dormant channels!") finally: bot.instance.stats.incr("help.out_of_channel_alerts") - return arrow.utcnow() + + return arrow.utcnow() async def notify_running_low(number_of_channels_left: int, last_notification: Arrow) -> t.Optional[Arrow]: @@ -202,7 +203,8 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar log.exception("Failed to send notification about running low of dormant channels!") finally: bot.instance.stats.incr("help.running_low_alerts") - return arrow.utcnow() + + return arrow.utcnow() async def pin(message: discord.Message) -> None: -- cgit v1.2.3 From dab2673d5e6ba387340efc5b9e4ddebb85795903 Mon Sep 17 00:00:00 2001 From: GDWR Date: Thu, 3 Feb 2022 20:18:41 +0000 Subject: 💡 Update docstrings to reflect changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index d867dd93d..f8f10f774 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -128,6 +128,9 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: """ Send a pinging message in `channel` notifying about there being no dormant channels remaining. + If a notification was sent, return the time at which the message was sent. + Otherwise, return None. + Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications * `HelpChannels.notify_none_remaining` - toggle none_remaining notifications @@ -171,6 +174,9 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar This will include the number of dormant channels left `number_of_channels_left` + If a notification was sent, return the time at which the message was sent. + Otherwise, return None. + Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications * `HelpChannels.notify_running_low` - toggle running_low notifications -- cgit v1.2.3 From 9f2d0839e4fb9d31d500db2c5bc5e22d59ed2cbb Mon Sep 17 00:00:00 2001 From: minalike Date: Sat, 5 Feb 2022 11:06:01 -0500 Subject: Add reported message author's username and profile picture in embed --- bot/exts/moderation/incidents.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py index 77dfad255..b579416a6 100644 --- a/bot/exts/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -229,6 +229,7 @@ async def make_message_link_embed(ctx: Context, message_link: str) -> Optional[d ), timestamp=message.created_at ) + embed.set_author(name=message.author, icon_url=message.author.display_avatar.url) embed.add_field( name="Content", value=shorten_text(message.content) if message.content else "[No Message Content]" -- cgit v1.2.3 From d8db2e5cfe9285350b286427c05a1781303e8443 Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Sun, 6 Feb 2022 17:34:26 -0700 Subject: Remove Coveralls dev dependency --- poetry.lock | 34 ++-------------------------------- pyproject.toml | 1 - 2 files changed, 2 insertions(+), 33 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9c9cc97ad..6d3bd44bb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -155,6 +155,7 @@ python-versions = "3.9.*" [package.source] type = "url" url = "https://github.com/python-discord/bot-core/archive/511bcba1b0196cd498c707a525ea56921bd971db.zip" + [[package]] name = "certifi" version = "2021.10.8" @@ -234,22 +235,6 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] toml = ["toml"] -[[package]] -name = "coveralls" -version = "2.2.0" -description = "Show coverage stats online via coveralls.io" -category = "dev" -optional = false -python-versions = ">= 3.5" - -[package.dependencies] -coverage = ">=4.1,<6.0" -docopt = ">=0.6.1" -requests = ">=1.0.0" - -[package.extras] -yaml = ["PyYAML (>=3.10)"] - [[package]] name = "deepdiff" version = "4.3.2" @@ -306,14 +291,6 @@ category = "dev" optional = false python-versions = "*" -[[package]] -name = "docopt" -version = "0.6.2" -description = "Pythonic argument parser, that will make you smile" -category = "dev" -optional = false -python-versions = "*" - [[package]] name = "emoji" version = "0.6.0" @@ -1170,7 +1147,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "d625aaae916c07c21080bf504831f5bf4d2bf4f0e3696e404448b43719eff201" +content-hash = "0248fc7488c79af0cdb3a6db9528f4c3129db50b3a8d1dd3ba57dbc31b381c31" [metadata.files] aio-pika = [ @@ -1383,10 +1360,6 @@ coverage = [ {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, ] -coveralls = [ - {file = "coveralls-2.2.0-py2.py3-none-any.whl", hash = "sha256:2301a19500b06649d2ec4f2858f9c69638d7699a4c63027c5d53daba666147cc"}, - {file = "coveralls-2.2.0.tar.gz", hash = "sha256:b990ba1f7bc4288e63340be0433698c1efe8217f78c689d254c2540af3d38617"}, -] deepdiff = [ {file = "deepdiff-4.3.2-py3-none-any.whl", hash = "sha256:59fc1e3e7a28dd0147b0f2b00e3e27181f0f0ef4286b251d5f214a5bcd9a9bc4"}, {file = "deepdiff-4.3.2.tar.gz", hash = "sha256:91360be1d9d93b1d9c13ae9c5048fa83d9cff17a88eb30afaa0d7ff2d0fee17d"}, @@ -1400,9 +1373,6 @@ distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, ] -docopt = [ - {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, -] emoji = [ {file = "emoji-0.6.0.tar.gz", hash = "sha256:e42da4f8d648f8ef10691bc246f682a1ec6b18373abfd9be10ec0b398823bd11"}, ] diff --git a/pyproject.toml b/pyproject.toml index 19e5f78a7..c764910c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,6 @@ tldextract = "^3.1.2" [tool.poetry.dev-dependencies] coverage = "~=5.0" -coveralls = "~=2.1" flake8 = "~=3.8" flake8-annotations = "~=2.0" flake8-bugbear = "~=20.1" -- cgit v1.2.3 From dd40e70db9c41c1a61faae5f2991cb841f2c6d10 Mon Sep 17 00:00:00 2001 From: Ben Soyka Date: Sun, 6 Feb 2022 17:41:43 -0700 Subject: Remove Coveralls badge from readme --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 9df905dc8..06df4fd9a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ [![Lint & Test][1]][2] [![Build][3]][4] [![Deploy][5]][6] -[![Coverage Status](https://coveralls.io/repos/github/python-discord/bot/badge.svg)](https://coveralls.io/github/python-discord/bot) [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) This project is a Discord bot specifically for use with the Python Discord server. It provides numerous utilities -- cgit v1.2.3 From d7872d49f6355c811422ad39d299343c7fd7ef63 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Mon, 7 Feb 2022 05:45:34 +0200 Subject: Don't validate reminder author Validation relies on the cache which might not be properly filled. This can cause reminders to be sent for users who are no longer in the server, which seems negligible. --- bot/exts/utils/reminders.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index 289d00356..ad82d49c9 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -66,20 +66,19 @@ class Reminders(Cog): else: self.schedule_reminder(reminder) - def ensure_valid_reminder(self, reminder: dict) -> t.Tuple[bool, discord.User, discord.TextChannel]: - """Ensure reminder author and channel can be fetched otherwise delete the reminder.""" - user = self.bot.get_user(reminder['author']) + def ensure_valid_reminder(self, reminder: dict) -> t.Tuple[bool, discord.TextChannel]: + """Ensure reminder channel can be fetched otherwise delete the reminder.""" channel = self.bot.get_channel(reminder['channel_id']) is_valid = True - if not user or not channel: + if not channel: is_valid = False log.info( f"Reminder {reminder['id']} invalid: " - f"User {reminder['author']}={user}, Channel {reminder['channel_id']}={channel}." + f"Channel {reminder['channel_id']}={channel}." ) scheduling.create_task(self.bot.api_client.delete(f"bot/reminders/{reminder['id']}")) - return is_valid, user, channel + return is_valid, channel @staticmethod async def _send_confirmation( @@ -170,7 +169,7 @@ class Reminders(Cog): @lock_arg(LOCK_NAMESPACE, "reminder", itemgetter("id"), raise_error=True) async def send_reminder(self, reminder: dict, expected_time: t.Optional[time.Timestamp] = None) -> None: """Send the reminder.""" - is_valid, user, channel = self.ensure_valid_reminder(reminder) + is_valid, channel = self.ensure_valid_reminder(reminder) if not is_valid: # No need to cancel the task too; it'll simply be done once this coroutine returns. return @@ -206,7 +205,7 @@ class Reminders(Cog): f"There was an error when trying to reply to a reminder invocation message, {e}, " "fall back to using jump_url" ) - await channel.send(content=f"{user.mention} {additional_mentions}", embed=embed) + await channel.send(content=f"<@{reminder['author']}> {additional_mentions}", embed=embed) log.debug(f"Deleting reminder #{reminder['id']} (the user has been reminded).") await self.bot.api_client.delete(f"bot/reminders/{reminder['id']}") -- cgit v1.2.3 From f037942d3b2ba77a568a12fc4fa703fee3274d90 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Wed, 9 Feb 2022 07:58:04 +0200 Subject: Disable Reminders Cog (#2074) --- bot/exts/utils/reminders.py | 492 -------------------------------------------- 1 file changed, 492 deletions(-) delete mode 100644 bot/exts/utils/reminders.py diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py deleted file mode 100644 index 289d00356..000000000 --- a/bot/exts/utils/reminders.py +++ /dev/null @@ -1,492 +0,0 @@ -import random -import textwrap -import typing as t -from datetime import datetime, timezone -from operator import itemgetter - -import discord -from dateutil.parser import isoparse -from discord.ext.commands import Cog, Context, Greedy, group - -from bot.bot import Bot -from bot.constants import Guild, Icons, MODERATION_ROLES, POSITIVE_REPLIES, Roles, STAFF_PARTNERS_COMMUNITY_ROLES -from bot.converters import Duration, UnambiguousUser -from bot.log import get_logger -from bot.pagination import LinePaginator -from bot.utils import scheduling, time -from bot.utils.checks import has_any_role_check, has_no_roles_check -from bot.utils.lock import lock_arg -from bot.utils.members import get_or_fetch_member -from bot.utils.messages import send_denial -from bot.utils.scheduling import Scheduler - -log = get_logger(__name__) - -LOCK_NAMESPACE = "reminder" -WHITELISTED_CHANNELS = Guild.reminder_whitelist -MAXIMUM_REMINDERS = 5 - -Mentionable = t.Union[discord.Member, discord.Role] -ReminderMention = t.Union[UnambiguousUser, discord.Role] - - -class Reminders(Cog): - """Provide in-channel reminder functionality.""" - - def __init__(self, bot: Bot): - self.bot = bot - self.scheduler = Scheduler(self.__class__.__name__) - - scheduling.create_task(self.reschedule_reminders(), event_loop=self.bot.loop) - - def cog_unload(self) -> None: - """Cancel scheduled tasks.""" - self.scheduler.cancel_all() - - async def reschedule_reminders(self) -> None: - """Get all current reminders from the API and reschedule them.""" - await self.bot.wait_until_guild_available() - response = await self.bot.api_client.get( - 'bot/reminders', - params={'active': 'true'} - ) - - now = datetime.now(timezone.utc) - - for reminder in response: - is_valid, *_ = self.ensure_valid_reminder(reminder) - if not is_valid: - continue - - remind_at = isoparse(reminder['expiration']) - - # If the reminder is already overdue ... - if remind_at < now: - await self.send_reminder(reminder, remind_at) - else: - self.schedule_reminder(reminder) - - def ensure_valid_reminder(self, reminder: dict) -> t.Tuple[bool, discord.User, discord.TextChannel]: - """Ensure reminder author and channel can be fetched otherwise delete the reminder.""" - user = self.bot.get_user(reminder['author']) - channel = self.bot.get_channel(reminder['channel_id']) - is_valid = True - if not user or not channel: - is_valid = False - log.info( - f"Reminder {reminder['id']} invalid: " - f"User {reminder['author']}={user}, Channel {reminder['channel_id']}={channel}." - ) - scheduling.create_task(self.bot.api_client.delete(f"bot/reminders/{reminder['id']}")) - - return is_valid, user, channel - - @staticmethod - async def _send_confirmation( - ctx: Context, - on_success: str, - reminder_id: t.Union[str, int] - ) -> None: - """Send an embed confirming the reminder change was made successfully.""" - embed = discord.Embed( - description=on_success, - colour=discord.Colour.green(), - title=random.choice(POSITIVE_REPLIES) - ) - - footer_str = f"ID: {reminder_id}" - - embed.set_footer(text=footer_str) - - await ctx.send(embed=embed) - - @staticmethod - async def _check_mentions(ctx: Context, mentions: t.Iterable[Mentionable]) -> t.Tuple[bool, str]: - """ - Returns whether or not the list of mentions is allowed. - - Conditions: - - Role reminders are Mods+ - - Reminders for other users are Helpers+ - - If mentions aren't allowed, also return the type of mention(s) disallowed. - """ - if await has_no_roles_check(ctx, *STAFF_PARTNERS_COMMUNITY_ROLES): - return False, "members/roles" - elif await has_no_roles_check(ctx, *MODERATION_ROLES): - return all(isinstance(mention, (discord.User, discord.Member)) for mention in mentions), "roles" - else: - return True, "" - - @staticmethod - async def validate_mentions(ctx: Context, mentions: t.Iterable[Mentionable]) -> bool: - """ - Filter mentions to see if the user can mention, and sends a denial if not allowed. - - Returns whether or not the validation is successful. - """ - mentions_allowed, disallowed_mentions = await Reminders._check_mentions(ctx, mentions) - - if not mentions or mentions_allowed: - return True - else: - await send_denial(ctx, f"You can't mention other {disallowed_mentions} in your reminder!") - return False - - async def get_mentionables(self, mention_ids: t.List[int]) -> t.Iterator[Mentionable]: - """Converts Role and Member ids to their corresponding objects if possible.""" - guild = self.bot.get_guild(Guild.id) - for mention_id in mention_ids: - member = await get_or_fetch_member(guild, mention_id) - if mentionable := (member or guild.get_role(mention_id)): - yield mentionable - - def schedule_reminder(self, reminder: dict) -> None: - """A coroutine which sends the reminder once the time is reached, and cancels the running task.""" - reminder_datetime = isoparse(reminder['expiration']) - self.scheduler.schedule_at(reminder_datetime, reminder["id"], self.send_reminder(reminder)) - - async def _edit_reminder(self, reminder_id: int, payload: dict) -> dict: - """ - Edits a reminder in the database given the ID and payload. - - Returns the edited reminder. - """ - # Send the request to update the reminder in the database - reminder = await self.bot.api_client.patch( - 'bot/reminders/' + str(reminder_id), - json=payload - ) - return reminder - - async def _reschedule_reminder(self, reminder: dict) -> None: - """Reschedule a reminder object.""" - log.trace(f"Cancelling old task #{reminder['id']}") - self.scheduler.cancel(reminder["id"]) - - log.trace(f"Scheduling new task #{reminder['id']}") - self.schedule_reminder(reminder) - - @lock_arg(LOCK_NAMESPACE, "reminder", itemgetter("id"), raise_error=True) - async def send_reminder(self, reminder: dict, expected_time: t.Optional[time.Timestamp] = None) -> None: - """Send the reminder.""" - is_valid, user, channel = self.ensure_valid_reminder(reminder) - if not is_valid: - # No need to cancel the task too; it'll simply be done once this coroutine returns. - return - embed = discord.Embed() - if expected_time: - embed.colour = discord.Colour.red() - embed.set_author( - icon_url=Icons.remind_red, - name="Sorry, your reminder should have arrived earlier!" - ) - else: - embed.colour = discord.Colour.og_blurple() - embed.set_author( - icon_url=Icons.remind_blurple, - name="It has arrived!" - ) - - # Let's not use a codeblock to keep emojis and mentions working. Embeds are safe anyway. - embed.description = f"Here's your reminder: {reminder['content']}" - - # Here the jump URL is in the format of base_url/guild_id/channel_id/message_id - additional_mentions = ' '.join([ - mentionable.mention async for mentionable in self.get_mentionables(reminder["mentions"]) - ]) - - jump_url = reminder.get("jump_url") - embed.description += f"\n[Jump back to when you created the reminder]({jump_url})" - partial_message = channel.get_partial_message(int(jump_url.split("/")[-1])) - try: - await partial_message.reply(content=f"{additional_mentions}", embed=embed) - except discord.HTTPException as e: - log.info( - f"There was an error when trying to reply to a reminder invocation message, {e}, " - "fall back to using jump_url" - ) - await channel.send(content=f"{user.mention} {additional_mentions}", embed=embed) - - log.debug(f"Deleting reminder #{reminder['id']} (the user has been reminded).") - await self.bot.api_client.delete(f"bot/reminders/{reminder['id']}") - - @group(name="remind", aliases=("reminder", "reminders", "remindme"), invoke_without_command=True) - async def remind_group( - self, ctx: Context, mentions: Greedy[ReminderMention], expiration: Duration, *, content: t.Optional[str] = None - ) -> None: - """ - Commands for managing your reminders. - - The `expiration` duration of `!remind new` supports the following symbols for each unit of time: - - years: `Y`, `y`, `year`, `years` - - months: `m`, `month`, `months` - - weeks: `w`, `W`, `week`, `weeks` - - days: `d`, `D`, `day`, `days` - - hours: `H`, `h`, `hour`, `hours` - - minutes: `M`, `minute`, `minutes` - - seconds: `S`, `s`, `second`, `seconds` - - For example, to set a reminder that expires in 3 days and 1 minute, you can do `!remind new 3d1M Do something`. - """ - await self.new_reminder(ctx, mentions=mentions, expiration=expiration, content=content) - - @remind_group.command(name="new", aliases=("add", "create")) - async def new_reminder( - self, ctx: Context, mentions: Greedy[ReminderMention], expiration: Duration, *, content: t.Optional[str] = None - ) -> None: - """ - Set yourself a simple reminder. - - The `expiration` duration supports the following symbols for each unit of time: - - years: `Y`, `y`, `year`, `years` - - months: `m`, `month`, `months` - - weeks: `w`, `W`, `week`, `weeks` - - days: `d`, `D`, `day`, `days` - - hours: `H`, `h`, `hour`, `hours` - - minutes: `M`, `minute`, `minutes` - - seconds: `S`, `s`, `second`, `seconds` - - For example, to set a reminder that expires in 3 days and 1 minute, you can do `!remind new 3d1M Do something`. - """ - # If the user is not staff, partner or part of the python community, - # we need to verify whether or not to make a reminder at all. - if await has_no_roles_check(ctx, *STAFF_PARTNERS_COMMUNITY_ROLES): - - # If they don't have permission to set a reminder in this channel - if ctx.channel.id not in WHITELISTED_CHANNELS: - await send_denial(ctx, "Sorry, you can't do that here!") - return - - # Get their current active reminders - active_reminders = await self.bot.api_client.get( - 'bot/reminders', - params={ - 'author__id': str(ctx.author.id) - } - ) - - # Let's limit this, so we don't get 10 000 - # reminders from kip or something like that :P - if len(active_reminders) > MAXIMUM_REMINDERS: - await send_denial(ctx, "You have too many active reminders!") - return - - # Remove duplicate mentions - mentions = set(mentions) - mentions.discard(ctx.author) - - # Filter mentions to see if the user can mention members/roles - if not await self.validate_mentions(ctx, mentions): - return - - mention_ids = [mention.id for mention in mentions] - - # If `content` isn't provided then we try to get message content of a replied message - if not content: - if reference := ctx.message.reference: - if isinstance((resolved_message := reference.resolved), discord.Message): - content = resolved_message.content - # If we weren't able to get the content of a replied message - if content is None: - await send_denial(ctx, "Your reminder must have a content and/or reply to a message.") - return - - # If the replied message has no content (e.g. only attachments/embeds) - if content == "": - content = "See referenced message." - - # Now we can attempt to actually set the reminder. - reminder = await self.bot.api_client.post( - 'bot/reminders', - json={ - 'author': ctx.author.id, - 'channel_id': ctx.message.channel.id, - 'jump_url': ctx.message.jump_url, - 'content': content, - 'expiration': expiration.isoformat(), - 'mentions': mention_ids, - } - ) - - formatted_time = time.discord_timestamp(expiration, time.TimestampFormats.DAY_TIME) - mention_string = f"Your reminder will arrive on {formatted_time}" - - if mentions: - mention_string += f" and will mention {len(mentions)} other(s)" - mention_string += "!" - - # Confirm to the user that it worked. - await self._send_confirmation( - ctx, - on_success=mention_string, - reminder_id=reminder["id"] - ) - - self.schedule_reminder(reminder) - - @remind_group.command(name="list") - async def list_reminders(self, ctx: Context) -> None: - """View a paginated embed of all reminders for your user.""" - # Get all the user's reminders from the database. - data = await self.bot.api_client.get( - 'bot/reminders', - params={'author__id': str(ctx.author.id)} - ) - - # Make a list of tuples so it can be sorted by time. - reminders = sorted( - ( - (rem['content'], rem['expiration'], rem['id'], rem['mentions']) - for rem in data - ), - key=itemgetter(1) - ) - - lines = [] - - for content, remind_at, id_, mentions in reminders: - # Parse and humanize the time, make it pretty :D - expiry = time.format_relative(remind_at) - - mentions = ", ".join([ - # Both Role and User objects have the `name` attribute - mention.name async for mention in self.get_mentionables(mentions) - ]) - mention_string = f"\n**Mentions:** {mentions}" if mentions else "" - - text = textwrap.dedent(f""" - **Reminder #{id_}:** *expires {expiry}* (ID: {id_}){mention_string} - {content} - """).strip() - - lines.append(text) - - embed = discord.Embed() - embed.colour = discord.Colour.og_blurple() - embed.title = f"Reminders for {ctx.author}" - - # Remind the user that they have no reminders :^) - if not lines: - embed.description = "No active reminders could be found." - await ctx.send(embed=embed) - return - - # Construct the embed and paginate it. - embed.colour = discord.Colour.og_blurple() - - await LinePaginator.paginate( - lines, - ctx, embed, - max_lines=3, - empty=True - ) - - @remind_group.group(name="edit", aliases=("change", "modify"), invoke_without_command=True) - async def edit_reminder_group(self, ctx: Context) -> None: - """ - Commands for modifying your current reminders. - - The `expiration` duration supports the following symbols for each unit of time: - - years: `Y`, `y`, `year`, `years` - - months: `m`, `month`, `months` - - weeks: `w`, `W`, `week`, `weeks` - - days: `d`, `D`, `day`, `days` - - hours: `H`, `h`, `hour`, `hours` - - minutes: `M`, `minute`, `minutes` - - seconds: `S`, `s`, `second`, `seconds` - - For example, to edit a reminder to expire in 3 days and 1 minute, you can do `!remind edit duration 1234 3d1M`. - """ - await ctx.send_help(ctx.command) - - @edit_reminder_group.command(name="duration", aliases=("time",)) - async def edit_reminder_duration(self, ctx: Context, id_: int, expiration: Duration) -> None: - """ - Edit one of your reminder's expiration. - - The `expiration` duration supports the following symbols for each unit of time: - - years: `Y`, `y`, `year`, `years` - - months: `m`, `month`, `months` - - weeks: `w`, `W`, `week`, `weeks` - - days: `d`, `D`, `day`, `days` - - hours: `H`, `h`, `hour`, `hours` - - minutes: `M`, `minute`, `minutes` - - seconds: `S`, `s`, `second`, `seconds` - - For example, to edit a reminder to expire in 3 days and 1 minute, you can do `!remind edit duration 1234 3d1M`. - """ - await self.edit_reminder(ctx, id_, {'expiration': expiration.isoformat()}) - - @edit_reminder_group.command(name="content", aliases=("reason",)) - async def edit_reminder_content(self, ctx: Context, id_: int, *, content: str) -> None: - """Edit one of your reminder's content.""" - await self.edit_reminder(ctx, id_, {"content": content}) - - @edit_reminder_group.command(name="mentions", aliases=("pings",)) - async def edit_reminder_mentions(self, ctx: Context, id_: int, mentions: Greedy[ReminderMention]) -> None: - """Edit one of your reminder's mentions.""" - # Remove duplicate mentions - mentions = set(mentions) - mentions.discard(ctx.author) - - # Filter mentions to see if the user can mention members/roles - if not await self.validate_mentions(ctx, mentions): - return - - mention_ids = [mention.id for mention in mentions] - await self.edit_reminder(ctx, id_, {"mentions": mention_ids}) - - @lock_arg(LOCK_NAMESPACE, "id_", raise_error=True) - async def edit_reminder(self, ctx: Context, id_: int, payload: dict) -> None: - """Edits a reminder with the given payload, then sends a confirmation message.""" - if not await self._can_modify(ctx, id_): - return - reminder = await self._edit_reminder(id_, payload) - - # Send a confirmation message to the channel - await self._send_confirmation( - ctx, - on_success="That reminder has been edited successfully!", - reminder_id=id_, - ) - await self._reschedule_reminder(reminder) - - @remind_group.command("delete", aliases=("remove", "cancel")) - @lock_arg(LOCK_NAMESPACE, "id_", raise_error=True) - async def delete_reminder(self, ctx: Context, id_: int) -> None: - """Delete one of your active reminders.""" - if not await self._can_modify(ctx, id_): - return - - await self.bot.api_client.delete(f"bot/reminders/{id_}") - self.scheduler.cancel(id_) - - await self._send_confirmation( - ctx, - on_success="That reminder has been deleted successfully!", - reminder_id=id_ - ) - - async def _can_modify(self, ctx: Context, reminder_id: t.Union[str, int]) -> bool: - """ - Check whether the reminder can be modified by the ctx author. - - The check passes when the user is an admin, or if they created the reminder. - """ - if await has_any_role_check(ctx, Roles.admins): - return True - - api_response = await self.bot.api_client.get(f"bot/reminders/{reminder_id}") - if not api_response["author"] == ctx.author.id: - log.debug(f"{ctx.author} is not the reminder author and does not pass the check.") - await send_denial(ctx, "You can't modify reminders of other users!") - return False - - log.debug(f"{ctx.author} is the reminder author and passes the check.") - return True - - -def setup(bot: Bot) -> None: - """Load the Reminders cog.""" - bot.add_cog(Reminders(bot)) -- cgit v1.2.3 From 1296914615c72d597be1b0c4d00bfa84011c960c Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Sun, 13 Feb 2022 15:07:11 +0000 Subject: Add unsubscribe alias to subscribe command In quite a few places, such as #roles, we tell users to run the unsubscribe command to remove roles from them. However, this command no longer exists due to the rework of the subscribe command. Since the subscribe commands allows tyou to remove as well as add roles, I have added this as an alias. --- bot/exts/info/subscribe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index 1299d5d59..eff0c13b8 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -171,7 +171,7 @@ class Subscribe(commands.Cog): self.assignable_roles.sort(key=operator.methodcaller("is_currently_available"), reverse=True) @commands.cooldown(1, 10, commands.BucketType.member) - @commands.command(name="subscribe") + @commands.command(name="subscribe", aliases=("unsubscribe",)) @redirect_output( destination_channel=constants.Channels.bot_commands, bypass_roles=constants.STAFF_PARTNERS_COMMUNITY_ROLES, -- cgit v1.2.3 From 036fa718a64009ee37c6c8d0693ab9490ded3475 Mon Sep 17 00:00:00 2001 From: Steele Farnsworth Date: Sun, 13 Feb 2022 14:53:30 -0500 Subject: Traceback tag: Emphasize reason for sharing traceback (#2072) --- bot/resources/tags/traceback.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/bot/resources/tags/traceback.md b/bot/resources/tags/traceback.md index 321737aac..a4fa8aba9 100644 --- a/bot/resources/tags/traceback.md +++ b/bot/resources/tags/traceback.md @@ -1,18 +1,16 @@ Please provide the full traceback for your exception in order to help us identify your issue. +While the last line of the error message tells us what kind of error you got, +the full traceback will tell us which line, and other critical information to solve your problem. +Please avoid screenshots so we can copy and paste parts of the message. A full traceback could look like: ```py Traceback (most recent call last): - File "tiny", line 3, in - do_something() - File "tiny", line 2, in do_something - a = 6 / b -ZeroDivisionError: division by zero + File "my_file.py", line 5, in + add_three("6") + File "my_file.py", line 2, in add_three + a = num + 3 +TypeError: can only concatenate str (not "int") to str ``` -The best way to read your traceback is bottom to top. -• Identify the exception raised (in this case `ZeroDivisionError`) -• Make note of the line number (in this case `2`), and navigate there in your program. -• Try to understand why the error occurred (in this case because `b` is `0`). - -To read more about exceptions and errors, please refer to the [PyDis Wiki](https://pythondiscord.com/pages/guides/pydis-guides/asking-good-questions/#examining-tracebacks) or the [official Python tutorial](https://docs.python.org/3.7/tutorial/errors.html). +If the traceback is long, use [our pastebin](https://paste.pythondiscord.com/). -- cgit v1.2.3 From 6cdbd56005f3795bcc9bc993c96f640b4e678a1d Mon Sep 17 00:00:00 2001 From: Xithrius <15021300+Xithrius@users.noreply.github.com> Date: Sun, 13 Feb 2022 12:42:07 -0800 Subject: Removed extra newline in the traceback tag. (#2083) --- bot/resources/tags/traceback.md | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/resources/tags/traceback.md b/bot/resources/tags/traceback.md index a4fa8aba9..e21fa6c6e 100644 --- a/bot/resources/tags/traceback.md +++ b/bot/resources/tags/traceback.md @@ -12,5 +12,4 @@ Traceback (most recent call last): a = num + 3 TypeError: can only concatenate str (not "int") to str ``` - If the traceback is long, use [our pastebin](https://paste.pythondiscord.com/). -- cgit v1.2.3 From b651e3498fa49de527a0c8d4d5a441a30f85c88b Mon Sep 17 00:00:00 2001 From: Izan Date: Mon, 14 Feb 2022 10:27:34 +0000 Subject: Fix DM handling for code snippets. --- bot/exts/info/code_snippets.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 07b1b8a2d..95a0fb014 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -255,16 +255,23 @@ class CodeSnippets(Cog): except discord.NotFound: # Don't send snippets if the original message was deleted. return - - if len(message_to_send) > 1000 and message.channel.id != Channels.bot_commands: - # Redirects to #bot-commands if the snippet contents are too long - await self.bot.wait_until_guild_available() - destination = self.bot.get_channel(Channels.bot_commands) - - await message.channel.send( - 'The snippet you tried to send was too long. ' - f'Please see {destination.mention} for the full snippet.' - ) + except discord.Forbidden as e: + # We still want to send snippets when in DMs, but if we're in guild then + # reraise error since that means there's a permissions issue with the bot. + if message.guild: + raise e + + # If we're in a guild, then check if we need to redirect to #bot-commands + if destination.guild: + if len(message_to_send) > 1000 and message.channel.id != Channels.bot_commands: + # Redirects to #bot-commands if the snippet contents are too long + await self.bot.wait_until_guild_available() + destination = self.bot.get_channel(Channels.bot_commands) + + await message.channel.send( + 'The snippet you tried to send was too long. ' + f'Please see {destination.mention} for the full snippet.' + ) await wait_for_deletion( await destination.send(message_to_send), -- cgit v1.2.3 From d10b7d2c56d3530b69bf96c02e55ed0248a3f026 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Mon, 14 Feb 2022 16:17:03 +0000 Subject: Fix ignoring of raw DM edits (#2085) --- bot/exts/moderation/modlog.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 2c01a4a21..54a08738c 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -729,6 +729,9 @@ class ModLog(Cog, name="ModLog"): @Cog.listener() async def on_raw_message_edit(self, event: discord.RawMessageUpdateEvent) -> None: """Log raw message edit event to message change log.""" + if event.guild_id is None: + return # ignore DM edits + await self.bot.wait_until_guild_available() try: channel = self.bot.get_channel(int(event.data["channel_id"])) -- cgit v1.2.3 From da58a4a3338f78c9263947a3d4433c9c56d37a02 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Wed, 16 Feb 2022 22:06:54 +0000 Subject: Fix: `!raw` can now be used in threads (#2090) --- bot/exts/info/information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index 5b25fd0c3..e616b9208 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -470,7 +470,7 @@ class Information(Cog): If `json` is True, send the information in a copy-pasteable Python format. """ - if ctx.author not in message.channel.members: + if not message.channel.permissions_for(ctx.author).read_messages: await ctx.send(":x: You do not have permissions to see the channel this message is in.") return -- cgit v1.2.3 From 465c2f5f73e743d11892ebd8dd4421d35e599dd4 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 18:12:05 +0000 Subject: Reply with log url after cleaning messages If done outside a mod channel, it instead tags the invoker in #mods. --- bot/constants.py | 1 + bot/exts/moderation/clean.py | 36 ++++++++++++++++++++++++------------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/bot/constants.py b/bot/constants.py index 1b713a7e3..77c01bfa3 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -445,6 +445,7 @@ class Channels(metaclass=YAMLGetter): incidents_archive: int mod_alerts: int mod_meta: int + mods: int nominations: int nomination_voting: int organisation: int diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index e61ef7880..f8ba230b3 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -331,12 +331,17 @@ class Clean(Cog): return deleted - async def _modlog_cleaned_messages(self, messages: list[Message], channels: CleanChannels, ctx: Context) -> bool: - """Log the deleted messages to the modlog. Return True if logging was successful.""" + async def _modlog_cleaned_messages( + self, + messages: list[Message], + channels: CleanChannels, + ctx: Context + ) -> Optional[str]: + """Log the deleted messages to the modlog, returning the log url if logging was successful.""" if not messages: # Can't build an embed, nothing to clean! await self._send_expiring_message(ctx, ":x: No matching messages could be found.") - return False + return None # Reverse the list to have reverse chronological order log_messages = reversed(messages) @@ -362,7 +367,7 @@ class Clean(Cog): channel_id=Channels.mod_log, ) - return True + return log_url # endregion @@ -375,8 +380,8 @@ class Clean(Cog): regex: Optional[re.Pattern] = None, first_limit: Optional[CleanLimit] = None, second_limit: Optional[CleanLimit] = None, - ) -> None: - """A helper function that does the actual message cleaning.""" + ) -> Optional[str]: + """A helper function that does the actual message cleaning, returns the log url if logging was successful.""" self._validate_input(channels, bots_only, users, first_limit, second_limit) # Are we already performing a clean? @@ -384,7 +389,7 @@ class Clean(Cog): await self._send_expiring_message( ctx, ":x: Please wait for the currently ongoing clean operation to complete." ) - return + return None self.cleaning = True deletion_channels = self._channels_set(channels, ctx, first_limit, second_limit) @@ -418,7 +423,7 @@ class Clean(Cog): if not self.cleaning: # Means that the cleaning was canceled - return + return None # Now let's delete the actual messages with purge. self.mod_log.ignore(Event.message_delete, *message_ids) @@ -427,11 +432,18 @@ class Clean(Cog): if not channels: channels = deletion_channels - logged = await self._modlog_cleaned_messages(deleted_messages, channels, ctx) + log_url = await self._modlog_cleaned_messages(deleted_messages, channels, ctx) - if logged and is_mod_channel(ctx.channel): - with suppress(NotFound): # Can happen if the invoker deleted their own messages. - await ctx.message.add_reaction(Emojis.check_mark) + success_message = ( + f"{Emojis.ok_hand} Deleted {len(deleted_messages)} messages. " + f"A log of the deleted messages can be found here {log_url}." + ) + if log_url and is_mod_channel(ctx.channel): + await ctx.reply(success_message) + elif log_url: + if mods := self.bot.get_channel(Channels.mods): + await mods.send(f"{ctx.author.mention} {success_message}") + return log_url # region: Commands -- cgit v1.2.3 From caaf0fa6d73ff6c8cfe54c7f8b952caf0aec97e2 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 18:13:09 +0000 Subject: Support not deleting invoking message of a clean task --- bot/exts/moderation/clean.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index f8ba230b3..cb6836258 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -380,6 +380,7 @@ class Clean(Cog): regex: Optional[re.Pattern] = None, first_limit: Optional[CleanLimit] = None, second_limit: Optional[CleanLimit] = None, + attempt_delete_invocation: bool = True, ) -> Optional[str]: """A helper function that does the actual message cleaning, returns the log url if logging was successful.""" self._validate_input(channels, bots_only, users, first_limit, second_limit) @@ -404,8 +405,9 @@ class Clean(Cog): # Needs to be called after standardizing the input. predicate = self._build_predicate(first_limit, second_limit, bots_only, users, regex) - # Delete the invocation first - await self._delete_invocation(ctx) + if attempt_delete_invocation: + # Delete the invocation first + await self._delete_invocation(ctx) if self._use_cache(first_limit): log.trace(f"Messages for cleaning by {ctx.author.id} will be searched in the cache.") -- cgit v1.2.3 From 3a7871b839a1ff13fc95be562eb275676ed41b7f Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 18:14:44 +0000 Subject: Update respect_role_hierarchy decorator to pass through return values --- bot/decorators.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/bot/decorators.py b/bot/decorators.py index 048a2a09a..f4331264f 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -188,7 +188,7 @@ def respect_role_hierarchy(member_arg: function.Argument) -> t.Callable: """ def decorator(func: types.FunctionType) -> types.FunctionType: @command_wraps(func) - async def wrapper(*args, **kwargs) -> None: + async def wrapper(*args, **kwargs) -> t.Any: log.trace(f"{func.__name__}: respect role hierarchy decorator called") bound_args = function.get_bound_args(func, args, kwargs) @@ -196,8 +196,7 @@ def respect_role_hierarchy(member_arg: function.Argument) -> t.Callable: if not isinstance(target, Member): log.trace("The target is not a discord.Member; skipping role hierarchy check.") - await func(*args, **kwargs) - return + return await func(*args, **kwargs) ctx = function.get_arg_value(1, bound_args) cmd = ctx.command.name @@ -214,7 +213,7 @@ def respect_role_hierarchy(member_arg: function.Argument) -> t.Callable: ) else: log.trace(f"{func.__name__}: {target.top_role=} < {actor.top_role=}; calling func") - await func(*args, **kwargs) + return await func(*args, **kwargs) return wrapper return decorator -- cgit v1.2.3 From 15fb882b49a2fb66b55b31aeb377ac03421de73d Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 18:19:08 +0000 Subject: Change purgeban to use custom clean logic This migrates the purgeban command away from Discord's native purgeban to our custom logic. Discord's native purgeban does not leave us with any evidence or context of what messages were deleted. So when mods reference the infraction at a later date they are lacking information. Instead, we use our custom clean cog to delete all messages from the user in question for the last hour, and automatically append the link to the clean log to the infraction reason. . --- bot/exts/moderation/infraction/infractions.py | 70 ++++++++++++++++++++------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 7c0259b8e..20fcf28f9 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -9,7 +9,7 @@ from discord.ext.commands import Context, command from bot import constants from bot.bot import Bot from bot.constants import Event -from bot.converters import Duration, Expiry, MemberOrUser, UnambiguousMemberOrUser +from bot.converters import Age, Duration, Expiry, Infraction, MemberOrUser, UnambiguousMemberOrUser from bot.decorators import respect_role_hierarchy from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler @@ -19,6 +19,11 @@ from bot.utils.messages import format_user log = get_logger(__name__) +if t.TYPE_CHECKING: + from bot.exts.moderation.clean import Clean + from bot.exts.moderation.infraction.management import ModManagement + from bot.exts.moderation.watchchannels.bigbrother import BigBrother + class Infractions(InfractionScheduler, commands.Cog): """Apply and pardon infractions on users for moderation purposes.""" @@ -101,11 +106,44 @@ class Infractions(InfractionScheduler, commands.Cog): reason: t.Optional[str] = None ) -> None: """ - Same as ban but removes all their messages of the last 24 hours. + Same as ban, but also cleans all their messages from the last hour. If duration is specified, it temporarily bans that user for the given duration. """ - await self.apply_ban(ctx, user, reason, 1, expires_at=duration) + clean_cog: t.Optional[Clean] = self.bot.get_cog("Clean") + if clean_cog is None: + # If we can't get the clean cog, fall back to native purgeban. + await self.apply_ban(ctx, user, reason, 1, expires_at=duration) + return + + infraction = await self.apply_ban(ctx, user, reason, expires_at=duration) + if not infraction or not infraction.get("id"): + # Ban was unsuccessful, quit early. + return + + # Calling commands directly skips Discord.py's convertors, so we need to convert args manually. + clean_time = await Age().convert(ctx, "1h") + infraction = await Infraction().convert(ctx, infraction["id"]) + + log_url = await clean_cog._clean_messages( + ctx, + users=[user], + channels="*", + first_limit=clean_time, + attempt_delete_invocation=False, + ) + + infr_manage_cog: t.Optional[ModManagement] = self.bot.get_cog("ModManagement") + if infr_manage_cog is None: + # If we can't get the mod management cog, don't bother appending the log. + return + + # Overwrite the context's send function so infraction append + # doesn't output the update infraction confirmation message. + async def send(*args, **kwargs) -> None: + pass + ctx.send = send + await infr_manage_cog.infraction_append(ctx, infraction, None, reason=f"[Clean log]({log_url})") @command(aliases=("vban",)) async def voiceban(self, ctx: Context) -> None: @@ -368,7 +406,7 @@ class Infractions(InfractionScheduler, commands.Cog): reason: t.Optional[str], purge_days: t.Optional[int] = 0, **kwargs - ) -> None: + ) -> t.Optional[dict]: """ Apply a ban infraction with kwargs passed to `post_infraction`. @@ -376,7 +414,7 @@ class Infractions(InfractionScheduler, commands.Cog): """ if isinstance(user, Member) and user.top_role >= ctx.me.top_role: await ctx.send(":x: I can't ban users above or equal to me in the role hierarchy.") - return + return None # In the case of a permanent ban, we don't need get_active_infractions to tell us if one is active is_temporary = kwargs.get("expires_at") is not None @@ -385,19 +423,19 @@ class Infractions(InfractionScheduler, commands.Cog): if active_infraction: if is_temporary: log.trace("Tempban ignored as it cannot overwrite an active ban.") - return + return None if active_infraction.get('expires_at') is None: log.trace("Permaban already exists, notify.") await ctx.send(f":x: User is already permanently banned (#{active_infraction['id']}).") - return + return None log.trace("Old tempban is being replaced by new permaban.") await self.pardon_infraction(ctx, "ban", user, send_msg=is_temporary) infraction = await _utils.post_infraction(ctx, user, "ban", reason, active=True, **kwargs) if infraction is None: - return + return None infraction["purge"] = "purge " if purge_days else "" @@ -409,19 +447,17 @@ class Infractions(InfractionScheduler, commands.Cog): action = ctx.guild.ban(user, reason=reason, delete_message_days=purge_days) await self.apply_infraction(ctx, infraction, user, action) + bb_cog: t.Optional[BigBrother] = self.bot.get_cog("Big Brother") if infraction.get('expires_at') is not None: log.trace(f"Ban isn't permanent; user {user} won't be unwatched by Big Brother.") - return - - bb_cog = self.bot.get_cog("Big Brother") - if not bb_cog: + elif not bb_cog: log.error(f"Big Brother cog not loaded; perma-banned user {user} won't be unwatched.") - return - - log.trace(f"Big Brother cog loaded; attempting to unwatch perma-banned user {user}.") + else: + log.trace(f"Big Brother cog loaded; attempting to unwatch perma-banned user {user}.") + bb_reason = "User has been permanently banned from the server. Automatically removed." + await bb_cog.apply_unwatch(ctx, user, bb_reason, send_message=False) - bb_reason = "User has been permanently banned from the server. Automatically removed." - await bb_cog.apply_unwatch(ctx, user, bb_reason, send_message=False) + return infraction @respect_role_hierarchy(member_arg=2) async def apply_voice_mute(self, ctx: Context, user: MemberOrUser, reason: t.Optional[str], **kwargs) -> None: -- cgit v1.2.3 From 954c1a963dc3917e02e0fb0ff4560131db8f20b4 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 18:56:28 +0000 Subject: Add more aliases to purgeban --- bot/exts/moderation/infraction/infractions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 20fcf28f9..e2c4c9ee4 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -96,8 +96,8 @@ class Infractions(InfractionScheduler, commands.Cog): """ await self.apply_ban(ctx, user, reason, expires_at=duration) - @command(aliases=('pban',)) - async def purgeban( + @command(aliases=("cban", "purgeban", "pban")) + async def cleanban( self, ctx: Context, user: UnambiguousMemberOrUser, -- cgit v1.2.3 From fdcdaac97f055285cee82354a9a1352dca002194 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 31 Dec 2021 19:04:00 +0000 Subject: Don't append clean log if no clean was done from purge ban --- bot/exts/moderation/infraction/infractions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index e2c4c9ee4..32ff376cf 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -132,6 +132,9 @@ class Infractions(InfractionScheduler, commands.Cog): first_limit=clean_time, attempt_delete_invocation=False, ) + if not log_url: + # Cleaning failed, or there were no messages to clean, exit early. + return infr_manage_cog: t.Optional[ModManagement] = self.bot.get_cog("ModManagement") if infr_manage_cog is None: -- cgit v1.2.3 From 993529aa945a1f9ec8d769c770399dbe2cd8bd25 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 2 Jan 2022 20:13:53 +0000 Subject: Add tests for new CleanBan and Clean functionality --- .../exts/moderation/infraction/test_infractions.py | 90 +++++++++++++++++- tests/bot/exts/moderation/test_clean.py | 104 +++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 tests/bot/exts/moderation/test_clean.py diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index f89465f84..57235ec6d 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -1,13 +1,15 @@ import inspect import textwrap import unittest -from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch +from unittest.mock import ANY, AsyncMock, DEFAULT, MagicMock, Mock, patch from discord.errors import NotFound from bot.constants import Event +from bot.exts.moderation.clean import Clean from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction.infractions import Infractions +from bot.exts.moderation.infraction.management import ModManagement from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockRole, MockUser, autospec @@ -231,3 +233,89 @@ class VoiceMuteTests(unittest.IsolatedAsyncioTestCase): "DM": "**Failed**" }) notify_pardon_mock.assert_awaited_once() + + +class CleanBanTests(unittest.IsolatedAsyncioTestCase): + """Tests for cleanban functionality.""" + + def setUp(self): + self.bot = MockBot() + self.mod = MockMember(roles=[MockRole(id=7890123, position=10)]) + self.user = MockMember(roles=[MockRole(id=123456, position=1)]) + self.guild = MockGuild() + self.ctx = MockContext(bot=self.bot, author=self.mod) + self.cog = Infractions(self.bot) + self.clean_cog = Clean(self.bot) + self.management_cog = ModManagement(self.bot) + + self.cog.apply_ban = AsyncMock(return_value={"id": 42}) + self.log_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + self.clean_cog._clean_messages = AsyncMock(return_value=self.log_url) + + def mock_get_cog(self, enable_clean, enable_manage): + def inner(name): + if name == "ModManagement": + return self.management_cog if enable_manage else None + elif name == "Clean": + return self.clean_cog if enable_clean else None + else: + return DEFAULT + return inner + + async def test_cleanban_falls_back_to_native_purge_without_clean_cog(self): + """Should fallback to native purge if the Clean cog is not available.""" + self.bot.get_cog.side_effect = self.mock_get_cog(False, False) + + self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar")) + self.cog.apply_ban.assert_awaited_once_with( + self.ctx, + self.user, + "FooBar", + 1, + expires_at=None, + ) + + async def test_cleanban_doesnt_purge_messages_if_clean_cog_available(self): + """Cleanban command should use the native purge messages if the clean cog is available.""" + self.bot.get_cog.side_effect = self.mock_get_cog(True, False) + + self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar")) + self.cog.apply_ban.assert_awaited_once_with( + self.ctx, + self.user, + "FooBar", + expires_at=None, + ) + + @patch("bot.exts.moderation.infraction.infractions.Age") + async def test_cleanban_uses_clean_cog_when_available(self, mocked_age_converter): + """Test cleanban uses the clean cog to clean messages if it's available.""" + self.bot.api_client.patch = AsyncMock() + self.bot.get_cog.side_effect = self.mock_get_cog(True, False) + + mocked_age_converter.return_value.convert = AsyncMock(return_value="81M") + self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar")) + + self.clean_cog._clean_messages.assert_awaited_once_with( + self.ctx, + users=[self.user], + channels="*", + first_limit="81M", + attempt_delete_invocation=False, + ) + + @patch("bot.exts.moderation.infraction.infractions.Infraction") + async def test_cleanban_edits_infraction_reason(self, mocked_infraction_converter): + """Ensure cleanban edits the ban reason with a link to the clean log.""" + self.bot.get_cog.side_effect = self.mock_get_cog(True, True) + + self.management_cog.infraction_append = AsyncMock() + mocked_infraction_converter.return_value.convert = AsyncMock(return_value=42) + self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar")) + + self.management_cog.infraction_append.assert_awaited_once_with( + self.ctx, + 42, + None, + reason=f"[Clean log]({self.log_url})" + ) diff --git a/tests/bot/exts/moderation/test_clean.py b/tests/bot/exts/moderation/test_clean.py new file mode 100644 index 000000000..83489ea00 --- /dev/null +++ b/tests/bot/exts/moderation/test_clean.py @@ -0,0 +1,104 @@ +import unittest +from unittest.mock import AsyncMock, MagicMock, patch + +from bot.exts.moderation.clean import Clean +from tests.helpers import MockBot, MockContext, MockGuild, MockMember, MockMessage, MockRole, MockTextChannel + + +class CleanTests(unittest.IsolatedAsyncioTestCase): + """Tests for clean cog functionality.""" + + def setUp(self): + self.bot = MockBot() + self.mod = MockMember(roles=[MockRole(id=7890123, position=10)]) + self.user = MockMember(roles=[MockRole(id=123456, position=1)]) + self.guild = MockGuild() + self.ctx = MockContext(bot=self.bot, author=self.mod) + self.cog = Clean(self.bot) + + self.log_url = "https://www.youtube.com/watch?v=dQw4w9WgXcQ" + self.cog._modlog_cleaned_messages = AsyncMock(return_value=self.log_url) + + self.cog._use_cache = MagicMock(return_value=True) + self.cog._delete_found = AsyncMock(return_value=[42, 84]) + + @patch("bot.exts.moderation.clean.is_mod_channel") + async def test_clean_deletes_invocation_in_non_mod_channel(self, mod_channel_check): + """Clean command should delete the invocation message if ran in a non mod channel.""" + mod_channel_check.return_value = False + self.ctx.message.delete = AsyncMock() + + self.assertIsNone(await self.cog._delete_invocation(self.ctx)) + + self.ctx.message.delete.assert_awaited_once() + + @patch("bot.exts.moderation.clean.is_mod_channel") + async def test_clean_doesnt_delete_invocation_in_mod_channel(self, mod_channel_check): + """Clean command should not delete the invocation message if ran in a mod channel.""" + mod_channel_check.return_value = True + self.ctx.message.delete = AsyncMock() + + self.assertIsNone(await self.cog._delete_invocation(self.ctx)) + + self.ctx.message.delete.assert_not_awaited() + + async def test_clean_doesnt_attempt_deletion_when_attempt_delete_invocation_is_false(self): + """Clean command should not attempt to delete the invocation message if attempt_delete_invocation is false.""" + self.cog._delete_invocation = AsyncMock() + self.bot.get_channel = MagicMock(return_value=False) + + self.assertEqual( + await self.cog._clean_messages( + self.ctx, + None, + first_limit=MockMessage(), + attempt_delete_invocation=False, + ), + self.log_url, + ) + + self.cog._delete_invocation.assert_not_awaited() + + @patch("bot.exts.moderation.clean.is_mod_channel") + async def test_clean_replies_with_success_message_when_ran_in_mod_channel(self, mod_channel_check): + """Clean command should reply to the message with a confirmation message if invoked in a mod channel.""" + mod_channel_check.return_value = True + self.ctx.reply = AsyncMock() + + self.assertEqual( + await self.cog._clean_messages( + self.ctx, + None, + first_limit=MockMessage(), + attempt_delete_invocation=False, + ), + self.log_url, + ) + + self.ctx.reply.assert_awaited_once() + sent_message = self.ctx.reply.await_args[0][0] + self.assertIn(self.log_url, sent_message) + self.assertIn("2 messages", sent_message) + + @patch("bot.exts.moderation.clean.is_mod_channel") + async def test_clean_send_success_message__to_mods_when_ran_in_non_mod_channel(self, mod_channel_check): + """Clean command should send a confirmation message to #mods if invoked in a non-mod channel.""" + mod_channel_check.return_value = False + mocked_mods = MockTextChannel(id=1234567) + mocked_mods.send = AsyncMock() + self.bot.get_channel = MagicMock(return_value=mocked_mods) + + self.assertEqual( + await self.cog._clean_messages( + self.ctx, + None, + first_limit=MockMessage(), + attempt_delete_invocation=False, + ), + self.log_url, + ) + + mocked_mods.send.assert_awaited_once() + sent_message = mocked_mods.send.await_args[0][0] + self.assertIn(self.log_url, sent_message) + self.assertIn("2 messages", sent_message) -- cgit v1.2.3 From 6c139905cca53f7810a100435955ec0c5fbc30e1 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 14 Feb 2022 01:51:23 +0000 Subject: Send error when cleanban fails to ban Co-authored-by: GDWR --- bot/exts/moderation/infraction/infractions.py | 4 +++- tests/bot/exts/moderation/infraction/test_infractions.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 32ff376cf..09ee1a7b4 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -113,12 +113,14 @@ class Infractions(InfractionScheduler, commands.Cog): clean_cog: t.Optional[Clean] = self.bot.get_cog("Clean") if clean_cog is None: # If we can't get the clean cog, fall back to native purgeban. - await self.apply_ban(ctx, user, reason, 1, expires_at=duration) + await self.apply_ban(ctx, user, reason, purge_days=1, expires_at=duration) return infraction = await self.apply_ban(ctx, user, reason, expires_at=duration) if not infraction or not infraction.get("id"): # Ban was unsuccessful, quit early. + await ctx.send(":x: Failed to apply ban.") + log.error("Failed to apply ban to user %d", user.id) return # Calling commands directly skips Discord.py's convertors, so we need to convert args manually. diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 57235ec6d..8845fb382 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -271,7 +271,7 @@ class CleanBanTests(unittest.IsolatedAsyncioTestCase): self.ctx, self.user, "FooBar", - 1, + purge_days=1, expires_at=None, ) -- cgit v1.2.3 From 762b107056145d44b5219a929302455c9e6ed1d0 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 14 Feb 2022 01:53:33 +0000 Subject: Typo and docstrings in clean ban tests Co-authored-by: GDWR --- tests/bot/exts/moderation/infraction/test_infractions.py | 1 + tests/bot/exts/moderation/test_clean.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 8845fb382..8bed1e386 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -253,6 +253,7 @@ class CleanBanTests(unittest.IsolatedAsyncioTestCase): self.clean_cog._clean_messages = AsyncMock(return_value=self.log_url) def mock_get_cog(self, enable_clean, enable_manage): + """Mock get cog factory that allows the user to specify whether clean and manage cogs are enabled.""" def inner(name): if name == "ModManagement": return self.management_cog if enable_manage else None diff --git a/tests/bot/exts/moderation/test_clean.py b/tests/bot/exts/moderation/test_clean.py index 83489ea00..d7647fa48 100644 --- a/tests/bot/exts/moderation/test_clean.py +++ b/tests/bot/exts/moderation/test_clean.py @@ -81,7 +81,7 @@ class CleanTests(unittest.IsolatedAsyncioTestCase): self.assertIn("2 messages", sent_message) @patch("bot.exts.moderation.clean.is_mod_channel") - async def test_clean_send_success_message__to_mods_when_ran_in_non_mod_channel(self, mod_channel_check): + async def test_clean_send_success_message_to_mods_when_ran_in_non_mod_channel(self, mod_channel_check): """Clean command should send a confirmation message to #mods if invoked in a non-mod channel.""" mod_channel_check.return_value = False mocked_mods = MockTextChannel(id=1234567) -- cgit v1.2.3 From 7e8e95f07e343f1d7d9a8069b6cdb1a9fcbb00d7 Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Wed, 16 Feb 2022 22:48:09 +0000 Subject: Remove unnecessary Infraction conversion in clean ban (#2092) --- bot/exts/moderation/infraction/infractions.py | 3 +-- tests/bot/exts/moderation/infraction/test_infractions.py | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index 09ee1a7b4..af42ab1b8 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -9,7 +9,7 @@ from discord.ext.commands import Context, command from bot import constants from bot.bot import Bot from bot.constants import Event -from bot.converters import Age, Duration, Expiry, Infraction, MemberOrUser, UnambiguousMemberOrUser +from bot.converters import Age, Duration, Expiry, MemberOrUser, UnambiguousMemberOrUser from bot.decorators import respect_role_hierarchy from bot.exts.moderation.infraction import _utils from bot.exts.moderation.infraction._scheduler import InfractionScheduler @@ -125,7 +125,6 @@ class Infractions(InfractionScheduler, commands.Cog): # Calling commands directly skips Discord.py's convertors, so we need to convert args manually. clean_time = await Age().convert(ctx, "1h") - infraction = await Infraction().convert(ctx, infraction["id"]) log_url = await clean_cog._clean_messages( ctx, diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 8bed1e386..052048053 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -305,18 +305,16 @@ class CleanBanTests(unittest.IsolatedAsyncioTestCase): attempt_delete_invocation=False, ) - @patch("bot.exts.moderation.infraction.infractions.Infraction") - async def test_cleanban_edits_infraction_reason(self, mocked_infraction_converter): + async def test_cleanban_edits_infraction_reason(self): """Ensure cleanban edits the ban reason with a link to the clean log.""" self.bot.get_cog.side_effect = self.mock_get_cog(True, True) self.management_cog.infraction_append = AsyncMock() - mocked_infraction_converter.return_value.convert = AsyncMock(return_value=42) self.assertIsNone(await self.cog.cleanban(self.cog, self.ctx, self.user, None, reason="FooBar")) self.management_cog.infraction_append.assert_awaited_once_with( self.ctx, - 42, + {"id": 42}, None, reason=f"[Clean log]({self.log_url})" ) -- cgit v1.2.3 From 4daa320a8482bc263b8cac3e912f69aa390056bf Mon Sep 17 00:00:00 2001 From: Izan Date: Thu, 17 Feb 2022 09:15:15 +0000 Subject: Fix two errors - Changed `destination.guild` to `message.guild` since `DMChannel` doesn't have a "guild" attribute - Only call `wait_for_deletion` when inside a guild. --- bot/exts/info/code_snippets.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index 95a0fb014..ebc7ce1c6 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -262,7 +262,7 @@ class CodeSnippets(Cog): raise e # If we're in a guild, then check if we need to redirect to #bot-commands - if destination.guild: + if message.guild: if len(message_to_send) > 1000 and message.channel.id != Channels.bot_commands: # Redirects to #bot-commands if the snippet contents are too long await self.bot.wait_until_guild_available() @@ -273,10 +273,12 @@ class CodeSnippets(Cog): f'Please see {destination.mention} for the full snippet.' ) - await wait_for_deletion( - await destination.send(message_to_send), - (message.author.id,) - ) + await wait_for_deletion( + await destination.send(message_to_send), + (message.author.id,) + ) + else: + await destination.send(message_to_send) def setup(bot: Bot) -> None: -- cgit v1.2.3 From abfb0ed9f0b3f1c676dc75707e51b33ded86cdf5 Mon Sep 17 00:00:00 2001 From: Izan Date: Thu, 17 Feb 2022 11:01:53 +0000 Subject: Validate regex when adding to the filter_token filter --- bot/exts/filters/filter_lists.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bot/exts/filters/filter_lists.py b/bot/exts/filters/filter_lists.py index ee5bd89f3..d3e6393d3 100644 --- a/bot/exts/filters/filter_lists.py +++ b/bot/exts/filters/filter_lists.py @@ -1,3 +1,4 @@ +import re from typing import Optional from discord import Colour, Embed @@ -72,6 +73,18 @@ class FilterLists(Cog): elif list_type == "FILE_FORMAT" and not content.startswith("."): content = f".{content}" + # If it's a filter token, validate the passed regex + elif list_type == "FILTER_TOKEN": + try: + _ = re.compile(content) + except re.error: + await ctx.message.add_reaction("❌") + await ctx.send( + f"{ctx.author.mention} that's not a valid regex! " + "You may have forgotten to escape part of the regex." + ) + return + # Try to add the item to the database log.trace(f"Trying to add the {content} item to the {list_type} {allow_type}") payload = { -- cgit v1.2.3 From b0e21f0a5f342cd8c9e82ddc357b51e19c3fc9ad Mon Sep 17 00:00:00 2001 From: Izan Date: Thu, 17 Feb 2022 11:47:29 +0000 Subject: Include regex error in failure message f --- bot/exts/filters/filter_lists.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/filters/filter_lists.py b/bot/exts/filters/filter_lists.py index d3e6393d3..9d3a52942 100644 --- a/bot/exts/filters/filter_lists.py +++ b/bot/exts/filters/filter_lists.py @@ -77,11 +77,11 @@ class FilterLists(Cog): elif list_type == "FILTER_TOKEN": try: _ = re.compile(content) - except re.error: + except re.error as e: await ctx.message.add_reaction("❌") await ctx.send( f"{ctx.author.mention} that's not a valid regex! " - "You may have forgotten to escape part of the regex." + f"Regex error message: {e.msg}." ) return -- cgit v1.2.3 From 97ddd3a709e1a6b7e821a438b61cd7b64a76ba62 Mon Sep 17 00:00:00 2001 From: minalike Date: Thu, 17 Feb 2022 21:40:16 -0500 Subject: Add user ID to message content for all mod alerts This is a temporary quality of life improvement until filters rewrite. Largely benefits mobile moderators who cannot copy from embeds. --- bot/exts/filters/filtering.py | 1 + bot/exts/moderation/modlog.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 1f83acf9b..9d491baa5 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -439,6 +439,7 @@ class Filtering(Cog): # Send pretty mod log embed to mod-alerts await self.mod_log.send_log_message( + content=str(msg.author.id), # quality-of-life improvement for mobile moderators to copy & paste icon_url=Icons.filtering, colour=Colour(Colours.soft_red), title=f"{_filter['type'].title()} triggered!", diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 54a08738c..32ea0dc6a 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -116,7 +116,7 @@ class ModLog(Cog, name="ModLog"): if ping_everyone: if content: - content = f"<@&{Roles.moderators}>\n{content}" + content = f"<@&{Roles.moderators}> {content}" else: content = f"<@&{Roles.moderators}>" -- cgit v1.2.3 From 7c8458c8a1aa6443c31bcd319c439b86c7e1c384 Mon Sep 17 00:00:00 2001 From: Izan Date: Fri, 18 Feb 2022 10:37:18 +0000 Subject: Remove auto joining of new threads. --- bot/exts/utils/bot.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/bot/exts/utils/bot.py b/bot/exts/utils/bot.py index 788692777..8f0094bc9 100644 --- a/bot/exts/utils/bot.py +++ b/bot/exts/utils/bot.py @@ -1,7 +1,6 @@ -from contextlib import suppress from typing import Optional -from discord import Embed, Forbidden, TextChannel, Thread +from discord import Embed, TextChannel from discord.ext.commands import Cog, Context, command, group, has_any_role from bot.bot import Bot @@ -17,20 +16,6 @@ class BotCog(Cog, name="Bot"): def __init__(self, bot: Bot): self.bot = bot - @Cog.listener() - async def on_thread_join(self, thread: Thread) -> None: - """ - Try to join newly created threads. - - Despite the event name being misleading, this is dispatched when new threads are created. - """ - if thread.me: - # We have already joined this thread - return - - with suppress(Forbidden): - await thread.join() - @group(invoke_without_command=True, name="bot", hidden=True) async def botinfo_group(self, ctx: Context) -> None: """Bot informational commands.""" -- cgit v1.2.3 From f500249dfe8c8a0e1a957640c85c5e23d2af335e Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Fri, 18 Feb 2022 13:10:10 +0000 Subject: Remove unnecessary assignment Co-authored-by: ChrisJL --- bot/exts/filters/filter_lists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/filters/filter_lists.py b/bot/exts/filters/filter_lists.py index 9d3a52942..a883ddf54 100644 --- a/bot/exts/filters/filter_lists.py +++ b/bot/exts/filters/filter_lists.py @@ -76,7 +76,7 @@ class FilterLists(Cog): # If it's a filter token, validate the passed regex elif list_type == "FILTER_TOKEN": try: - _ = re.compile(content) + re.compile(content) except re.error as e: await ctx.message.add_reaction("❌") await ctx.send( -- cgit v1.2.3 From 8283043d1b60be3f7ad9094983c2bf1b959fb70b Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 13 Feb 2022 21:26:47 +0000 Subject: Add a cog to bump threads Quite often we want threads such as event discussions, or moderation discussions to live beyond their maximum of 1 week of auto-archival. This cog allows staff to add a thread to a list that will get 'bumped' back open by the bot when they are auto-archived --- bot/exts/utils/thread_bumper.py | 114 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 bot/exts/utils/thread_bumper.py diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py new file mode 100644 index 000000000..a10d151aa --- /dev/null +++ b/bot/exts/utils/thread_bumper.py @@ -0,0 +1,114 @@ +import typing as t + +import discord +from async_rediscache import RedisCache +from discord.ext import commands + +from bot import constants +from bot.bot import Bot +from bot.log import get_logger +from bot.pagination import LinePaginator +from bot.utils import scheduling + +log = get_logger(__name__) + + +class ThreadBumper(commands.Cog): + """Cog that allow users to add the current thread to a list that get reopened on archive.""" + + # RedisCache[discord.Thread.id, "sentinel"] + threads_to_bump = RedisCache() + + def __init__(self, bot: Bot): + self.bot = bot + self.init_task = scheduling.create_task(self.ensure_bumped_threads_are_active(), event_loop=self.bot.loop) + + async def ensure_bumped_threads_are_active(self) -> None: + """Ensure bumped threads are active, since threads could have been archived while the bot was down.""" + await self.bot.wait_until_guild_available() + + for thread_id, _ in await self.threads_to_bump.items(): + if thread := self.bot.get_channel(thread_id): + if not thread.archived: + continue + + try: + thread = await self.bot.fetch_channel(thread_id) + except discord.NotFound: + log.info(f"Thread {thread_id} has been deleted, removing from bumped threads.") + await self.threads_to_bump.delete(thread_id) + if thread.archived: + await thread.edit(archived=False) + + @commands.group(name="bump") + async def thread_bump_group(self, ctx: commands.Context) -> None: + """A group of commands to manage the bumping of threads.""" + if not ctx.invoked_subcommand: + await ctx.send_help(ctx.command) + + @thread_bump_group.command(name="add") + async def add_thread_to_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None: + """Add a thread to the bump list.""" + await self.init_task + + if not thread: + if isinstance(ctx.channel, discord.Thread): + thread = ctx.channel + else: + raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + + await self.threads_to_bump.set(thread.id, "sentinel") + await ctx.send(f":ok_hand:{thread.mention} has been added to the bump list.") + + @thread_bump_group.command(name="remove", aliases=("r", "rem", "d", "del", "delete")) + async def remove_thread_from_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None: + """Remove a thread from the bump list.""" + await self.init_task + + if not thread: + if isinstance(ctx.channel, discord.Thread): + thread = ctx.channel + else: + raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + + await self.threads_to_bump.delete(thread.id) + await ctx.send(f":ok_hand: {thread.mention} has been removed from the bump list.") + + @thread_bump_group.command(name="list", aliases=("get",)) + async def list_all_threads_in_bump_list(self, ctx: commands.Context) -> None: + """List all the threads in the bump list.""" + await self.init_task + + lines = [f"<#{k}>" for k, _ in await self.threads_to_bump.items()] + embed = discord.Embed( + title="Threads in the bump list", + colour=constants.Colours.blue + ) + await LinePaginator.paginate(lines, ctx, embed) + + @commands.Cog.listener() + async def on_thread_update(self, _: discord.Thread, after: discord.Thread) -> None: + """ + Listen for thread updates and check if the thread has been archived. + + If the thread has been archived, and is in the bump list, un-archive it. + """ + await self.init_task + + if not after.archived: + return + + bumped_threads = [k for k, _ in await self.threads_to_bump.items()] + if after.id in bumped_threads: + await after.edit(archived=False) + + async def cog_check(self, ctx: commands.Context) -> bool: + """Only allow staff & partner roles to invoke the commands in this cog.""" + return await commands.has_any_role( + *constants.STAFF_PARTNERS_COMMUNITY_ROLES + ).predicate(ctx) + + +def setup(bot: Bot) -> None: + """Load the ThreadBumper cog.""" + bot.add_cog(ThreadBumper(bot)) -- cgit v1.2.3 From abdfd0db7caa2961428e6cf6b601df4aaccd9151 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Thu, 17 Feb 2022 01:23:56 +0000 Subject: Add logic so that manually archived threads bypass the thread bump list --- bot/exts/utils/thread_bumper.py | 46 +++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py index a10d151aa..8c6f3518e 100644 --- a/bot/exts/utils/thread_bumper.py +++ b/bot/exts/utils/thread_bumper.py @@ -8,7 +8,7 @@ from bot import constants from bot.bot import Bot from bot.log import get_logger from bot.pagination import LinePaginator -from bot.utils import scheduling +from bot.utils import channel, scheduling log = get_logger(__name__) @@ -23,22 +23,50 @@ class ThreadBumper(commands.Cog): self.bot = bot self.init_task = scheduling.create_task(self.ensure_bumped_threads_are_active(), event_loop=self.bot.loop) + async def unarchive_threads_not_manually_archived(self, threads: list[discord.Thread]) -> None: + """ + Iterate through and unarchive any threads that weren't manually archived recently. + + This is done by extracting the manually archived threads from the audit log. + + Only the last 200 thread_update logs are checked, + as this is assumed to be more than enough to cover bot downtime. + """ + guild = self.bot.get_guild(constants.Guild.id) + + recent_manually_archived_thread_ids = [] + async for thread_update in guild.audit_logs(limit=200, action=discord.AuditLogAction.thread_update): + if getattr(thread_update.after, "archived", False): + recent_manually_archived_thread_ids.append(thread_update.target.id) + + for thread in threads: + if thread.id in recent_manually_archived_thread_ids: + log.info( + "#%s (%d) was manually archived. Leaving archived, and removing from bumped threads.", + thread.name, + thread.id + ) + await self.threads_to_bump.delete(thread.id) + else: + await thread.edit(archived=False) + async def ensure_bumped_threads_are_active(self) -> None: """Ensure bumped threads are active, since threads could have been archived while the bot was down.""" await self.bot.wait_until_guild_available() + threads_to_maybe_bump = [] for thread_id, _ in await self.threads_to_bump.items(): - if thread := self.bot.get_channel(thread_id): - if not thread.archived: - continue - try: - thread = await self.bot.fetch_channel(thread_id) + thread = await channel.get_or_fetch_channel(thread_id) except discord.NotFound: - log.info(f"Thread {thread_id} has been deleted, removing from bumped threads.") + log.info("Thread %d has been deleted, removing from bumped threads.", thread_id) await self.threads_to_bump.delete(thread_id) + continue + if thread.archived: - await thread.edit(archived=False) + threads_to_maybe_bump.append(thread) + + await self.unarchive_threads_not_manually_archived(threads_to_maybe_bump) @commands.group(name="bump") async def thread_bump_group(self, ctx: commands.Context) -> None: @@ -100,7 +128,7 @@ class ThreadBumper(commands.Cog): bumped_threads = [k for k, _ in await self.threads_to_bump.items()] if after.id in bumped_threads: - await after.edit(archived=False) + await self.unarchive_threads_not_manually_archived([after]) async def cog_check(self, ctx: commands.Context) -> bool: """Only allow staff & partner roles to invoke the commands in this cog.""" -- cgit v1.2.3 From 3346d71416fdb3223d0c4998f92e420886445fac Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Fri, 18 Feb 2022 17:13:31 +0000 Subject: fixup: implemeent code review comments --- bot/exts/utils/thread_bumper.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py index 8c6f3518e..35057f1fe 100644 --- a/bot/exts/utils/thread_bumper.py +++ b/bot/exts/utils/thread_bumper.py @@ -74,7 +74,7 @@ class ThreadBumper(commands.Cog): if not ctx.invoked_subcommand: await ctx.send_help(ctx.command) - @thread_bump_group.command(name="add") + @thread_bump_group.command(name="add", aliases=("a",)) async def add_thread_to_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None: """Add a thread to the bump list.""" await self.init_task @@ -85,6 +85,9 @@ class ThreadBumper(commands.Cog): else: raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + if await self.threads_to_bump.contains(thread.id): + raise commands.BadArgument("This thread is already in the bump list.") + await self.threads_to_bump.set(thread.id, "sentinel") await ctx.send(f":ok_hand:{thread.mention} has been added to the bump list.") @@ -99,6 +102,9 @@ class ThreadBumper(commands.Cog): else: raise commands.BadArgument("You must provide a thread, or run this command within a thread.") + if not await self.threads_to_bump.contains(thread.id): + raise commands.BadArgument("This thread is not in the bump list.") + await self.threads_to_bump.delete(thread.id) await ctx.send(f":ok_hand: {thread.mention} has been removed from the bump list.") @@ -126,8 +132,7 @@ class ThreadBumper(commands.Cog): if not after.archived: return - bumped_threads = [k for k, _ in await self.threads_to_bump.items()] - if after.id in bumped_threads: + if await self.threads_to_bump.contains(after.id): await self.unarchive_threads_not_manually_archived([after]) async def cog_check(self, ctx: commands.Context) -> bool: -- cgit v1.2.3 From 7eea60288866322836956a7b3689e49a3e9c5c41 Mon Sep 17 00:00:00 2001 From: mina Date: Fri, 18 Feb 2022 17:22:16 -0500 Subject: Add user ID in message content for mod-alerts, but not for autobans --- bot/exts/filters/antispam.py | 1 + bot/exts/filters/filtering.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py index ddfd11231..bcd845a43 100644 --- a/bot/exts/filters/antispam.py +++ b/bot/exts/filters/antispam.py @@ -103,6 +103,7 @@ class DeletionContext: mod_alert_message += content await modlog.send_log_message( + content=", ".join(str(m.id) for m in self.members), # quality-of-life improvement for mobile moderators icon_url=Icons.filtering, colour=Colour(Colours.soft_red), title="Spam detected!", diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index 9d491baa5..f44b28125 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -256,6 +256,7 @@ class Filtering(Cog): ) await self.mod_log.send_log_message( + content=str(member.id), # quality-of-life improvement for mobile moderators icon_url=Icons.token_removed, colour=Colours.soft_red, title="Username filtering alert", @@ -423,9 +424,12 @@ class Filtering(Cog): # Allow specific filters to override ping_everyone ping_everyone = Filter.ping_everyone and _filter.get("ping_everyone", True) - # If we are going to autoban, we don't want to ping + content = str(msg.author.id) # quality-of-life improvement for mobile moderators + + # If we are going to autoban, we don't want to ping and don't need the user ID if reason and "[autoban]" in reason: ping_everyone = False + content = None eval_msg = "using !eval " if is_eval else "" footer = f"Reason: {reason}" if reason else None @@ -439,7 +443,7 @@ class Filtering(Cog): # Send pretty mod log embed to mod-alerts await self.mod_log.send_log_message( - content=str(msg.author.id), # quality-of-life improvement for mobile moderators to copy & paste + content=content, icon_url=Icons.filtering, colour=Colour(Colours.soft_red), title=f"{_filter['type'].title()} triggered!", -- cgit v1.2.3 From d258203483e1c7d4d6044dbcc3b1628c8bc3a319 Mon Sep 17 00:00:00 2001 From: TizzySaurus <47674925+TizzySaurus@users.noreply.github.com> Date: Sun, 20 Feb 2022 19:55:41 +0000 Subject: Remove discord formatted timestamp from log message (#2100) --- bot/exts/moderation/stream.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index 4dccc8a7e..985cc6eb1 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -133,8 +133,12 @@ class Stream(commands.Cog): await ctx.send(f"{Emojis.check_mark} {member.mention} can now stream until {time.discord_timestamp(duration)}.") # Convert here for nicer logging - revoke_time = time.format_with_duration(duration) - log.debug(f"Successfully gave {member} ({member.id}) permission to stream until {revoke_time}.") + humanized_duration = time.humanize_delta(duration, arrow.utcnow(), max_units=2) + end_time = duration.strftime("%Y-%m-%d %H:%M:%S") + log.debug( + f"Successfully gave {member} ({member.id}) permission " + f"to stream for {humanized_duration} (until {end_time})." + ) @commands.command(aliases=("pstream",)) @commands.has_any_role(*MODERATION_ROLES) -- cgit v1.2.3 From 33203cbfe95f28501af28a42e0c49f4d42b6f021 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 9 Feb 2022 10:38:13 +0000 Subject: Cancel help channel claim on 500 from Discord If we get a 500 error from Discord when trying to move the help channel to in use, attempt to let the user know, then cancel the claim. --- bot/exts/help_channels/_cog.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 541c791e5..6d061c8ca 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -111,14 +111,31 @@ class HelpChannels(commands.Cog): """ log.info(f"Channel #{message.channel} was claimed by `{message.author.id}`.") + try: + await self.move_to_in_use(message.channel) + except discord.DiscordServerError: + try: + await message.channel.send( + "The bot encountered a Discord API error while trying to move this channel, please try again later." + ) + except Exception as e: + log.warning("Error occurred while sending fail claim message:", exc_info=e) + log.info( + "500 error from Discord when moving #%s (%d) to in-use for %s (%d). Cancelling claim.", + message.channel.name, + message.channel.id, + message.author.name, + message.author.id, + ) + self.bot.stats.incr("help.failed_claims.500_on_move") + return + embed = discord.Embed( description=f"Channel claimed by {message.author.mention}.", color=constants.Colours.bright_green, ) await message.channel.send(embed=embed) - await self.move_to_in_use(message.channel) - # Handle odd edge case of `message.author` not being a `discord.Member` (see bot#1839) if not isinstance(message.author, discord.Member): log.debug(f"{message.author} ({message.author.id}) isn't a member. Not giving cooldown role or sending DM.") -- cgit v1.2.3 From 6505ab00f0695148e0e52ed1dc6815b37d4b6cae Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Wed, 9 Feb 2022 12:55:55 +0000 Subject: Ensure each in-use channel has a cached claimant on init This avoids issues when a user tries to close a channel, but the cache is empty, so the author check fails. --- bot/exts/help_channels/_channel.py | 34 ++++++++++++++++++++++++++++++++++ bot/exts/help_channels/_cog.py | 1 + 2 files changed, 35 insertions(+) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index e43c1e789..ff9e6a347 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -1,3 +1,4 @@ +import re import typing as t from datetime import timedelta from enum import Enum @@ -16,6 +17,7 @@ log = get_logger(__name__) MAX_CHANNELS_PER_CATEGORY = 50 EXCLUDED_CHANNELS = (constants.Channels.cooldown,) +CLAIMED_BY_RE = re.compile(r"Channel claimed by <@!?(?P\d{17,20})>\.$") class ClosingReason(Enum): @@ -157,3 +159,35 @@ async def move_to_bottom(channel: discord.TextChannel, category_id: int, **optio # Now that the channel is moved, we can edit the other attributes if options: await channel.edit(**options) + + +async def ensure_cached_claimant(channel: discord.TextChannel) -> None: + """ + Ensure there is a claimant cached for each help channel. + + Check the redis cache first, return early if there is already a claimant cached. + If there isn't an entry in redis, search for the "Claimed by X." embed in channel history. + Stopping early if we discover a dormant message first. + + If a claimant could not be found, send a warning to #helpers and set the claimant to the bot. + """ + if await _caches.claimants.get(channel.id): + return + + async for message in channel.history(limit=1000): + if message.author.id != bot.instance.user.id: + # We only care about bot messages + continue + if message.embeds: + if _message._match_bot_embed(message, _message.DORMANT_MSG): + log.info("Hit the dormant message embed before finding a claimant in %s (%d).", channel, channel.id) + break + user_id = CLAIMED_BY_RE.match(message.embeds[0].description).group("user_id") + await _caches.claimants.set(channel.id, int(user_id)) + return + + await bot.instance.get_channel(constants.Channels.helpers).send( + f"I couldn't find a claimant for {channel.mention} in that last 1000 messages. " + "Please use your helper powers to close the channel if/when appropriate." + ) + await _caches.claimants.set(channel.id, bot.instance.user.id) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index 6d061c8ca..b0f1a1dce 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -326,6 +326,7 @@ class HelpChannels(commands.Cog): log.trace("Moving or rescheduling in-use channels.") for channel in _channel.get_category_channels(self.in_use_category): + await _channel.ensure_cached_claimant(channel) await self.move_idle_channel(channel, has_task=False) # Prevent the command from being used until ready. -- cgit v1.2.3 From 4e0c6e73a5d430574ec73257734c78a8c6574fa2 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sun, 20 Feb 2022 14:10:49 +0000 Subject: Handle uncached claimant on unclaim This could be possible during init_available. If there are too many available channels they are made dormant by calling unclaim_channel. However there may not be claimants cached and ensure_claimants wouldn't populate cache, since the channels weren't in use. --- bot/exts/help_channels/_cog.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index b0f1a1dce..f276a7993 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -452,18 +452,21 @@ class HelpChannels(commands.Cog): async def _unclaim_channel( self, channel: discord.TextChannel, - claimant_id: int, + claimant_id: t.Optional[int], closed_on: _channel.ClosingReason ) -> None: """Actual implementation of `unclaim_channel`. See that for full documentation.""" await _caches.claimants.delete(channel.id) await _caches.session_participants.delete(channel.id) - claimant = await members.get_or_fetch_member(self.guild, claimant_id) - if claimant is None: - log.info(f"{claimant_id} left the guild during their help session; the cooldown role won't be removed") + if not claimant_id: + log.info("No claimant given when un-claiming %s (%d). Skipping role removal.", channel, channel.id) else: - await members.handle_role_change(claimant, claimant.remove_roles, self.cooldown_role) + claimant = await members.get_or_fetch_member(self.guild, claimant_id) + if claimant is None: + log.info(f"{claimant_id} left the guild during their help session; the cooldown role won't be removed") + else: + await members.handle_role_change(claimant, claimant.remove_roles, self.cooldown_role) await _message.unpin(channel) await _stats.report_complete_session(channel.id, closed_on) -- cgit v1.2.3 From 75b3a515910f04f295e0b70c208e8383b1783b7d Mon Sep 17 00:00:00 2001 From: minalike Date: Tue, 22 Feb 2022 19:30:20 -0500 Subject: 👌 Fix indentation and update grammar for when only 1 channel remains MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 17 ++++++++++------- config-default.yml | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index f8f10f774..39132b0f1 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -128,8 +128,8 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: """ Send a pinging message in `channel` notifying about there being no dormant channels remaining. - If a notification was sent, return the time at which the message was sent. - Otherwise, return None. + If a notification was sent, return the time at which the message was sent. + Otherwise, return None. Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications @@ -175,7 +175,7 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar This will include the number of dormant channels left `number_of_channels_left` If a notification was sent, return the time at which the message was sent. - Otherwise, return None. + Otherwise, return None. Configuration: * `HelpChannels.notify_minutes` - minimum interval between notifications @@ -200,10 +200,13 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar log.trace("Did not send notify_running notification as the notification channel couldn't be gathered.") try: - await channel.send( - f"There are only {number_of_channels_left} dormant channels left. " - "Consider participating in some help channels so that we don't run out." - ) + if number_of_channels_left == 1: + message = f"There is only {number_of_channels_left} dormant channel left. " + else: + message = f"There are only {number_of_channels_left} dormant channels left. " + message += "Consider participating in some help channels so that we don't run out." + await channel.send(message) + except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about running low of dormant channels!") diff --git a/config-default.yml b/config-default.yml index 6ad471cbd..dae923158 100644 --- a/config-default.yml +++ b/config-default.yml @@ -517,7 +517,7 @@ help_channels: notify_minutes: 15 # Minimum interval between none_remaining or running_low notifications notify_none_remaining: true # Pinging notification for the Helper role when no dormant channels remain - notify_none_remaining_roles: # Mention these roles in the non_remaining notification + notify_none_remaining_roles: # Mention these roles in the none_remaining notification - *HELPERS_ROLE notify_running_low: true # Non-pinging notification which is triggered when the channel count is equal or less than the threshold -- cgit v1.2.3 From 54480a1a166511c8b85dbb46403247144138fd24 Mon Sep 17 00:00:00 2001 From: minalike Date: Tue, 22 Feb 2022 19:45:34 -0500 Subject: 👌 Only send metric if helpers were notified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/exts/help_channels/_message.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 39132b0f1..6e986282e 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -162,10 +162,9 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about lack of dormant channels!") - finally: + else: bot.instance.stats.incr("help.out_of_channel_alerts") - - return arrow.utcnow() + return arrow.utcnow() async def notify_running_low(number_of_channels_left: int, last_notification: Arrow) -> t.Optional[Arrow]: @@ -210,10 +209,9 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about running low of dormant channels!") - finally: + else: bot.instance.stats.incr("help.running_low_alerts") - - return arrow.utcnow() + return arrow.utcnow() async def pin(message: discord.Message) -> None: -- cgit v1.2.3 From 6c7444d6f61fe201f97673de435a63d4b4f67c7d Mon Sep 17 00:00:00 2001 From: minalike Date: Tue, 22 Feb 2022 20:08:29 -0500 Subject: 🐛 Fix to correctly calculate number of seconds from last notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit total_seconds() is the correct method to obtain a time delta in seconds --- bot/exts/help_channels/_message.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 6e986282e..0aabb9bfb 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -139,7 +139,7 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: if not constants.HelpChannels.notify_none_remaining: return None - if (arrow.utcnow() - last_notification).seconds < (constants.HelpChannels.notify_minutes * 60): + if (arrow.utcnow() - last_notification).total_seconds() < (constants.HelpChannels.notify_minutes * 60): log.trace("Did not send none_remaining notification as it hasn't been enough time since the last one.") return None @@ -188,7 +188,7 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar log.trace("Did not send notify_running_low notification as the threshold was not met.") return None - if (arrow.utcnow() - last_notification).seconds < (constants.HelpChannels.notify_minutes * 60): + if (arrow.utcnow() - last_notification).total_seconds() < (constants.HelpChannels.notify_minutes * 60): log.trace("Did not send notify_running_low notification as it hasn't been enough time since the last one.") return None -- cgit v1.2.3 From 01e877947a1a0e73d6197cdb7af5a9dd03387337 Mon Sep 17 00:00:00 2001 From: minalike Date: Tue, 22 Feb 2022 20:42:01 -0500 Subject: Fixup: remove extra blank line --- bot/exts/help_channels/_message.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 0aabb9bfb..7ceed9b4d 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -205,7 +205,6 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar message = f"There are only {number_of_channels_left} dormant channels left. " message += "Consider participating in some help channels so that we don't run out." await channel.send(message) - except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. log.exception("Failed to send notification about running low of dormant channels!") -- cgit v1.2.3 From 940c9048cc95c114d2ea657e3efc3a9d9468d831 Mon Sep 17 00:00:00 2001 From: MarkKoz <1515135+MarkKoz@users.noreply.github.com> Date: Tue, 22 Feb 2022 18:36:26 -0800 Subject: Fix Member fetch in resend infraction command --- bot/exts/moderation/infraction/management.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index c813d1fdc..c12dff928 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -65,9 +65,10 @@ class ModManagement(commands.Cog): await ctx.send(f"{constants.Emojis.failmail} You may not resend hidden infractions.") return - member = await get_or_fetch_member(ctx.guild, infraction["user"]) + member_id = infraction["user"]["id"] + member = await get_or_fetch_member(ctx.guild, member_id) if not member: - await ctx.send(f"{constants.Emojis.failmail} Cannot find member `{infraction['user']}`.") + await ctx.send(f"{constants.Emojis.failmail} Cannot find member `{member_id}` in the guild.") return id_ = infraction["id"] -- cgit v1.2.3 From 9044f5404ed2be1b5eb4160f6d2a86e329f6abf5 Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Wed, 23 Feb 2022 07:30:28 -0500 Subject: fix: Make sure the regex match is not None before adding to claimaints cache If there was a bot message in a help channel that contained an embed that was not the claimed channel message, this would raise an attribute error. --- bot/exts/help_channels/_channel.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index ff9e6a347..ea7d972b5 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -182,9 +182,10 @@ async def ensure_cached_claimant(channel: discord.TextChannel) -> None: if _message._match_bot_embed(message, _message.DORMANT_MSG): log.info("Hit the dormant message embed before finding a claimant in %s (%d).", channel, channel.id) break - user_id = CLAIMED_BY_RE.match(message.embeds[0].description).group("user_id") - await _caches.claimants.set(channel.id, int(user_id)) - return + # Only set the claimant if the first embed matches the claimed channel embed regex + if match := CLAIMED_BY_RE.match(message.embeds[0].description): + await _caches.claimants.set(channel.id, int(match.group("user_id"))) + return await bot.instance.get_channel(constants.Channels.helpers).send( f"I couldn't find a claimant for {channel.mention} in that last 1000 messages. " -- cgit v1.2.3 From 880d42a384e90db34faebbbdeb47d879704c13fd Mon Sep 17 00:00:00 2001 From: ToxicKidz Date: Mon, 28 Feb 2022 16:04:10 -0500 Subject: chore: Disallow code snippets in DMs --- bot/exts/info/code_snippets.py | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index ebc7ce1c6..f2f29020f 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -246,6 +246,9 @@ class CodeSnippets(Cog): if message.author.bot: return + if message.guild is None: + return + message_to_send = await self._parse_snippets(message.content) destination = message.channel @@ -255,30 +258,21 @@ class CodeSnippets(Cog): except discord.NotFound: # Don't send snippets if the original message was deleted. return - except discord.Forbidden as e: - # We still want to send snippets when in DMs, but if we're in guild then - # reraise error since that means there's a permissions issue with the bot. - if message.guild: - raise e - - # If we're in a guild, then check if we need to redirect to #bot-commands - if message.guild: - if len(message_to_send) > 1000 and message.channel.id != Channels.bot_commands: - # Redirects to #bot-commands if the snippet contents are too long - await self.bot.wait_until_guild_available() - destination = self.bot.get_channel(Channels.bot_commands) - - await message.channel.send( - 'The snippet you tried to send was too long. ' - f'Please see {destination.mention} for the full snippet.' - ) - await wait_for_deletion( - await destination.send(message_to_send), - (message.author.id,) + if len(message_to_send) > 1000 and message.channel.id != Channels.bot_commands: + # Redirects to #bot-commands if the snippet contents are too long + await self.bot.wait_until_guild_available() + destination = self.bot.get_channel(Channels.bot_commands) + + await message.channel.send( + 'The snippet you tried to send was too long. ' + f'Please see {destination.mention} for the full snippet.' ) - else: - await destination.send(message_to_send) + + await wait_for_deletion( + await destination.send(message_to_send), + (message.author.id,) + ) def setup(bot: Bot) -> None: -- cgit v1.2.3 From b67ee61ada08ebcd89c07f29a36ae5d0ba2ae057 Mon Sep 17 00:00:00 2001 From: andy Date: Mon, 28 Feb 2022 19:47:57 -0500 Subject: fix: Make help buttons only work for author --- bot/exts/info/help.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index 06799fb71..ad784c87b 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -113,12 +113,22 @@ class CommandView(ui.View): If the command has a parent, a button is added to the view to show that parent's help embed. """ - def __init__(self, help_command: CustomHelpCommand, command: Command): + def __init__(self, help_command: CustomHelpCommand, command: Command, context: Context): + self.context = context super().__init__() if command.parent: self.children.append(GroupButton(help_command, command, emoji="↩️")) + async def interaction_check(self, interaction: Interaction) -> bool: + """Ensures the button only works for the user who spawned the help command.""" + if interaction.user is not None: + + if interaction.user.id == self.context.author.id: + return True + + return False + class GroupView(CommandView): """ @@ -130,8 +140,8 @@ class GroupView(CommandView): MAX_BUTTONS_IN_ROW = 5 MAX_ROWS = 5 - def __init__(self, help_command: CustomHelpCommand, group: Group, subcommands: list[Command]): - super().__init__(help_command, group) + def __init__(self, help_command: CustomHelpCommand, group: Group, subcommands: list[Command], context: Context): + super().__init__(help_command, group, context) # Don't add buttons if only a portion of the subcommands can be shown. if len(subcommands) + len(self.children) > self.MAX_ROWS * self.MAX_BUTTONS_IN_ROW: log.trace(f"Attempted to add navigation buttons for `{group.qualified_name}`, but there was no space.") @@ -302,7 +312,7 @@ class CustomHelpCommand(HelpCommand): embed.description = command_details # If the help is invoked in the context of an error, don't show subcommand navigation. - view = CommandView(self, command) if not self.context.command_failed else None + view = CommandView(self, command, self.context) if not self.context.command_failed else None return embed, view async def send_command_help(self, command: Command) -> None: @@ -347,7 +357,7 @@ class CustomHelpCommand(HelpCommand): embed.description += f"\n**Subcommands:**\n{command_details}" # If the help is invoked in the context of an error, don't show subcommand navigation. - view = GroupView(self, group, commands_) if not self.context.command_failed else None + view = GroupView(self, group, commands_, self.context) if not self.context.command_failed else None return embed, view async def send_group_help(self, group: Group) -> None: -- cgit v1.2.3 From 7637f1c139ba7df50ca6ac56192aef769355cc80 Mon Sep 17 00:00:00 2001 From: andy Date: Mon, 28 Feb 2022 19:53:35 -0500 Subject: docs: Make docstring sound better --- bot/exts/info/help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index ad784c87b..33ce40c88 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -121,7 +121,7 @@ class CommandView(ui.View): self.children.append(GroupButton(help_command, command, emoji="↩️")) async def interaction_check(self, interaction: Interaction) -> bool: - """Ensures the button only works for the user who spawned the help command.""" + """Ensures that the button only works for the user who spawned the help command.""" if interaction.user is not None: if interaction.user.id == self.context.author.id: -- cgit v1.2.3 From 61e2bb4a90202d12c36754e6936b33fbdebb2f7d Mon Sep 17 00:00:00 2001 From: andy Date: Mon, 28 Feb 2022 22:51:21 -0500 Subject: feat: Allow moderators to use buttons in other people's help command --- bot/exts/info/help.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index 33ce40c88..71f244eac 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -123,8 +123,10 @@ class CommandView(ui.View): async def interaction_check(self, interaction: Interaction) -> bool: """Ensures that the button only works for the user who spawned the help command.""" if interaction.user is not None: + if any(role.id in constants.MODERATION_ROLES for role in interaction.user.roles): + return True - if interaction.user.id == self.context.author.id: + elif interaction.user.id == self.context.author.id: return True return False -- cgit v1.2.3 From 9e204117dfd236c0cc2fb8355899df2feab63d29 Mon Sep 17 00:00:00 2001 From: an-dyy Date: Tue, 1 Mar 2022 15:24:28 -0500 Subject: docs: Added docstring for moderator access --- bot/exts/info/help.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index 71f244eac..864e7edd2 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -121,7 +121,11 @@ class CommandView(ui.View): self.children.append(GroupButton(help_command, command, emoji="↩️")) async def interaction_check(self, interaction: Interaction) -> bool: - """Ensures that the button only works for the user who spawned the help command.""" + """ + Ensures that the button only works for the user who spawned the help command. + + Also allows moderators to access buttons even when not the author of message. + """ if interaction.user is not None: if any(role.id in constants.MODERATION_ROLES for role in interaction.user.roles): return True -- cgit v1.2.3 From a30f8856cc24e6b42fc86ab972d605bfe2562075 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 21 Feb 2022 02:13:22 +0000 Subject: Migrate from Discord.py to disnake --- bot/exts/help_channels/_cog.py | 2 +- poetry.lock | 391 +++++++++++++++++++++-------------------- pyproject.toml | 2 +- 3 files changed, 205 insertions(+), 190 deletions(-) diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index a93acffb6..d3d70e252 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -567,7 +567,7 @@ class HelpChannels(commands.Cog): try: log.trace("Help channels have changed, dynamic message has been edited.") await self.bot.http.edit_message( - constants.Channels.how_to_get_help, self.dynamic_message, content=available_channels + constants.Channels.how_to_get_help, self.dynamic_message, content=available_channels, files=None ) except discord.NotFound: pass diff --git a/poetry.lock b/poetry.lock index 6d3bd44bb..a8ee6ef5c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "aio-pika" -version = "6.8.1" +version = "6.8.2" description = "Wrapper for the aiormq for asyncio and humans." category = "main" optional = false @@ -155,7 +155,6 @@ python-versions = "3.9.*" [package.source] type = "url" url = "https://github.com/python-discord/bot-core/archive/511bcba1b0196cd498c707a525ea56921bd971db.zip" - [[package]] name = "certifi" version = "2021.10.8" @@ -193,7 +192,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "charset-normalizer" -version = "2.0.10" +version = "2.0.12" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -283,6 +282,23 @@ voice = ["PyNaCl (>=1.3.0,<1.5)"] type = "url" url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" +[[package]] +name = "disnake" +version = "2.4.0" +description = "A Python wrapper for the Discord API" +category = "main" +optional = false +python-versions = ">=3.8.0" + +[package.dependencies] +aiohttp = ">=3.7.0,<3.9.0" + +[package.extras] +discord = ["discord-disnake"] +docs = ["sphinx (>=4.4.0,<4.5.0)", "sphinxcontrib-trio (==1.1.2)", "sphinx-hoverxref (>=1.0.0,<1.1.0)", "sphinx-autobuild (==2021.3.14)"] +speed = ["orjson (>=3.5.4)", "aiodns (>=1.1)", "brotli", "cchardet"] +voice = ["PyNaCl (>=1.3.0,<1.5)"] + [[package]] name = "distlib" version = "0.3.4" @@ -315,7 +331,7 @@ testing = ["pre-commit"] [[package]] name = "fakeredis" -version = "1.7.0" +version = "1.7.1" description = "Fake implementation of redis API for testing purposes." category = "main" optional = false @@ -323,7 +339,7 @@ python-versions = ">=3.5" [package.dependencies] packaging = "*" -redis = "<4.1.0" +redis = "<4.2.0" six = ">=1.12" sortedcontainers = "*" @@ -344,7 +360,7 @@ sgmllib3k = "*" [[package]] name = "filelock" -version = "3.4.2" +version = "3.6.0" description = "A platform independent file lock." category = "main" optional = false @@ -445,14 +461,14 @@ flake8 = "*" [[package]] name = "flake8-tidy-imports" -version = "4.5.0" +version = "4.6.0" description = "A flake8 plugin that helps you write tidier imports." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] -flake8 = ">=3.8.0,<5" +flake8 = ">=3.8.0" [[package]] name = "flake8-todo" @@ -486,11 +502,11 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "identify" -version = "2.4.2" +version = "2.4.10" description = "File identification library for Python" category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7" [package.extras] license = ["ukkonen"] @@ -527,7 +543,7 @@ plugins = ["setuptools"] [[package]] name = "lxml" -version = "4.7.1" +version = "4.8.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." category = "main" optional = false @@ -577,11 +593,11 @@ python-versions = ">=3.5" [[package]] name = "multidict" -version = "5.2.0" +version = "6.0.2" description = "multidict implementation" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "nodeenv" @@ -593,11 +609,14 @@ python-versions = "*" [[package]] name = "ordered-set" -version = "4.0.2" -description = "A set that remembers its order, and allows looking up its items by their index in that order." +version = "4.1.0" +description = "An OrderedSet is a custom MutableSet that remembers its order, so that every" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" + +[package.extras] +dev = ["pytest", "black", "mypy"] [[package]] name = "packaging" @@ -649,7 +668,7 @@ test = ["docutils", "pytest-cov", "pytest-pycodestyle", "pytest-runner"] [[package]] name = "platformdirs" -version = "2.4.1" +version = "2.5.1" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false @@ -673,7 +692,7 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.16.0" +version = "2.17.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -768,7 +787,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pyparsing" -version = "3.0.6" +version = "3.0.7" description = "Python parsing module" category = "main" optional = false @@ -779,7 +798,7 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyreadline3" -version = "3.3" +version = "3.4.1" description = "A python implementation of GNU readline." category = "main" optional = false @@ -910,17 +929,19 @@ full = ["numpy"] [[package]] name = "redis" -version = "4.0.2" +version = "4.1.4" description = "Python client for Redis database and key-value store" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -deprecated = "*" +deprecated = ">=1.2.3" +packaging = ">=20.4" [package.extras] hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] [[package]] name = "regex" @@ -962,7 +983,7 @@ six = "*" [[package]] name = "sentry-sdk" -version = "1.5.1" +version = "1.5.5" description = "Python client for Sentry (https://sentry.io)" category = "main" optional = false @@ -984,6 +1005,7 @@ flask = ["flask (>=0.11)", "blinker (>=1.1)"] httpx = ["httpx (>=0.16.0)"] pure_eval = ["pure-eval", "executing", "asttokens"] pyspark = ["pyspark (>=2.4.4)"] +quart = ["quart (>=0.16.1)", "blinker (>=1.1)"] rq = ["rq (>=0.6)"] sanic = ["sanic (>=0.8)"] sqlalchemy = ["sqlalchemy (>=1.2)"] @@ -1087,7 +1109,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typing-extensions" -version = "4.0.1" +version = "4.1.1" description = "Backported and Experimental Type Hints for Python 3.6+" category = "main" optional = false @@ -1108,7 +1130,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.13.0" +version = "20.13.1" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1147,12 +1169,12 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "0248fc7488c79af0cdb3a6db9528f4c3129db50b3a8d1dd3ba57dbc31b381c31" +content-hash = "538a4809b9fc6fa93ee1baccf4016515ae311a886f1b7ec9b3d544bb87c830a3" [metadata.files] aio-pika = [ - {file = "aio-pika-6.8.1.tar.gz", hash = "sha256:c2b2b46949a34252ff0e64c3bc208eef1893e5791b51aeefabf1676788d56b66"}, - {file = "aio_pika-6.8.1-py3-none-any.whl", hash = "sha256:059ab8ecc03d73997f64ed28df7269105984232174d0e6406389c4e8ed30941c"}, + {file = "aio-pika-6.8.2.tar.gz", hash = "sha256:d89658148def0d8b8d795868a753fe2906f8d8fccee53e4a1b5093ddd3d2dc5c"}, + {file = "aio_pika-6.8.2-py3-none-any.whl", hash = "sha256:4bf23e54bceb86b789d4b4a72ed65f2d83ede429d5f343de838ca72e54f00475"}, ] aiodns = [ {file = "aiodns-2.0.0-py2.py3-none-any.whl", hash = "sha256:aaa5ac584f40fe778013df0aa6544bf157799bd3f608364b451840ed2c8688de"}, @@ -1295,8 +1317,8 @@ chardet = [ {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.10.tar.gz", hash = "sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd"}, - {file = "charset_normalizer-2.0.10-py3-none-any.whl", hash = "sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455"}, + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, @@ -1369,6 +1391,10 @@ deprecated = [ {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] "discord.py" = [] +disnake = [ + {file = "disnake-2.4.0-py3-none-any.whl", hash = "sha256:390250a55ed8bbcc8c5753a72fb8fff2376a30295476edfebd0d2301855fb919"}, + {file = "disnake-2.4.0.tar.gz", hash = "sha256:d7a9c83d5cbfcec42441dae1d96744f82c2a22403934db5d8862a8279ca4989c"}, +] distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, @@ -1381,16 +1407,16 @@ execnet = [ {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, ] fakeredis = [ - {file = "fakeredis-1.7.0-py3-none-any.whl", hash = "sha256:6f1e04f64557ad3b6835bdc6e5a8d022cbace4bdc24a47ad58f6a72e0fbff760"}, - {file = "fakeredis-1.7.0.tar.gz", hash = "sha256:c9bd12e430336cbd3e189fae0e91eb99997b93e76dbfdd6ed67fa352dc684c71"}, + {file = "fakeredis-1.7.1-py3-none-any.whl", hash = "sha256:be3668e50f6b57d5fc4abfd27f9f655bed07a2c5aecfc8b15d0aad59f997c1ba"}, + {file = "fakeredis-1.7.1.tar.gz", hash = "sha256:7c2c4ba1b42e0a75337c54b777bf0671056b4569650e3ff927e4b9b385afc8ec"}, ] feedparser = [ {file = "feedparser-6.0.8-py3-none-any.whl", hash = "sha256:1b7f57841d9cf85074deb316ed2c795091a238adb79846bc46dccdaf80f9c59a"}, {file = "feedparser-6.0.8.tar.gz", hash = "sha256:5ce0410a05ab248c8c7cfca3a0ea2203968ee9ff4486067379af4827a59f9661"}, ] filelock = [ - {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, - {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, + {file = "filelock-3.6.0-py3-none-any.whl", hash = "sha256:f8314284bfffbdcfa0ff3d7992b023d4c628ced6feb957351d4c48d059f56bc0"}, + {file = "filelock-3.6.0.tar.gz", hash = "sha256:9cd540a9352e432c7246a48fe4e8712b10acb1df2ad1f30e8c070b82ae1fed85"}, ] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, @@ -1421,8 +1447,8 @@ flake8-string-format = [ {file = "flake8_string_format-0.3.0-py2.py3-none-any.whl", hash = "sha256:812ff431f10576a74c89be4e85b8e075a705be39bc40c4b4278b5b13e2afa9af"}, ] flake8-tidy-imports = [ - {file = "flake8-tidy-imports-4.5.0.tar.gz", hash = "sha256:ac637961d0f319012d099e49619f8c928e3221f74e00fe6eb89513bc64c40adb"}, - {file = "flake8_tidy_imports-4.5.0-py3-none-any.whl", hash = "sha256:87eed94ae6a2fda6a5918d109746feadf1311e0eb8274ab7a7920f6db00a41c9"}, + {file = "flake8-tidy-imports-4.6.0.tar.gz", hash = "sha256:3e193d8c4bb4492408a90e956d888b27eed14c698387c9b38230da3dad78058f"}, + {file = "flake8_tidy_imports-4.6.0-py3-none-any.whl", hash = "sha256:6ae9f55d628156e19d19f4c359dd5d3e95431a9bd514f5e2748c53c1398c66b2"}, ] flake8-todo = [ {file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"}, @@ -1475,8 +1501,8 @@ humanfriendly = [ {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, ] identify = [ - {file = "identify-2.4.2-py2.py3-none-any.whl", hash = "sha256:67c1e66225870dce721228176637a8ef965e8dd58450bcc7592249d0dfc4da6c"}, - {file = "identify-2.4.2.tar.gz", hash = "sha256:93e8ec965e888f2212aa5c24b2b662f4832c39acb1d7196a70ea45acb626a05e"}, + {file = "identify-2.4.10-py2.py3-none-any.whl", hash = "sha256:7d10baf6ba6f1912a0a49f4c1c2c49fa1718765c3a37d72d13b07779567c5b85"}, + {file = "identify-2.4.10.tar.gz", hash = "sha256:e12b2aea3cf108de73ae055c2260783bde6601de09718f6768cf8e9f6f6322a6"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -1491,66 +1517,67 @@ isort = [ {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] lxml = [ - {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, - {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, - {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17"}, - {file = "lxml-4.7.1-cp27-cp27m-win32.whl", hash = "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419"}, - {file = "lxml-4.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2"}, - {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5"}, - {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d"}, - {file = "lxml-4.7.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175"}, - {file = "lxml-4.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6"}, - {file = "lxml-4.7.1-cp310-cp310-win32.whl", hash = "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6"}, - {file = "lxml-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e"}, - {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675"}, - {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7"}, - {file = "lxml-4.7.1-cp35-cp35m-win32.whl", hash = "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5"}, - {file = "lxml-4.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03"}, - {file = "lxml-4.7.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6"}, - {file = "lxml-4.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d"}, - {file = "lxml-4.7.1-cp36-cp36m-win32.whl", hash = "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d"}, - {file = "lxml-4.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a"}, - {file = "lxml-4.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944"}, - {file = "lxml-4.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b"}, - {file = "lxml-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4"}, - {file = "lxml-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1"}, - {file = "lxml-4.7.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e"}, - {file = "lxml-4.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9"}, - {file = "lxml-4.7.1-cp38-cp38-win32.whl", hash = "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd"}, - {file = "lxml-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d"}, - {file = "lxml-4.7.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851"}, - {file = "lxml-4.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4"}, - {file = "lxml-4.7.1-cp39-cp39-win32.whl", hash = "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0"}, - {file = "lxml-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, - {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, + {file = "lxml-4.8.0-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b"}, + {file = "lxml-4.8.0-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430"}, + {file = "lxml-4.8.0-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a"}, + {file = "lxml-4.8.0-cp27-cp27m-win32.whl", hash = "sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5"}, + {file = "lxml-4.8.0-cp27-cp27m-win_amd64.whl", hash = "sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9"}, + {file = "lxml-4.8.0-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc"}, + {file = "lxml-4.8.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170"}, + {file = "lxml-4.8.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe"}, + {file = "lxml-4.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa"}, + {file = "lxml-4.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1"}, + {file = "lxml-4.8.0-cp310-cp310-win32.whl", hash = "sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b"}, + {file = "lxml-4.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76"}, + {file = "lxml-4.8.0-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6"}, + {file = "lxml-4.8.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2"}, + {file = "lxml-4.8.0-cp35-cp35m-win32.whl", hash = "sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150"}, + {file = "lxml-4.8.0-cp35-cp35m-win_amd64.whl", hash = "sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654"}, + {file = "lxml-4.8.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e"}, + {file = "lxml-4.8.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613"}, + {file = "lxml-4.8.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33"}, + {file = "lxml-4.8.0-cp36-cp36m-win32.whl", hash = "sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429"}, + {file = "lxml-4.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63"}, + {file = "lxml-4.8.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c"}, + {file = "lxml-4.8.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85"}, + {file = "lxml-4.8.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141"}, + {file = "lxml-4.8.0-cp37-cp37m-win32.whl", hash = "sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63"}, + {file = "lxml-4.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8"}, + {file = "lxml-4.8.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870"}, + {file = "lxml-4.8.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9"}, + {file = "lxml-4.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68"}, + {file = "lxml-4.8.0-cp38-cp38-win32.whl", hash = "sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696"}, + {file = "lxml-4.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939"}, + {file = "lxml-4.8.0-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c"}, + {file = "lxml-4.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87"}, + {file = "lxml-4.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9"}, + {file = "lxml-4.8.0-cp39-cp39-win32.whl", hash = "sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea"}, + {file = "lxml-4.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9"}, + {file = "lxml-4.8.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79"}, + {file = "lxml-4.8.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93"}, + {file = "lxml-4.8.0.tar.gz", hash = "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23"}, ] markdownify = [ {file = "markdownify-0.6.1-py3-none-any.whl", hash = "sha256:7489fd5c601536996a376c4afbcd1dd034db7690af807120681461e82fbc0acc"}, @@ -1569,85 +1596,73 @@ mslex = [ {file = "mslex-0.3.0.tar.gz", hash = "sha256:4a1ac3f25025cad78ad2fe499dd16d42759f7a3801645399cce5c404415daa97"}, ] multidict = [ - {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3822c5894c72e3b35aae9909bef66ec83e44522faf767c0ad39e0e2de11d3b55"}, - {file = "multidict-5.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:28e6d883acd8674887d7edc896b91751dc2d8e87fbdca8359591a13872799e4e"}, - {file = "multidict-5.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b61f85101ef08cbbc37846ac0e43f027f7844f3fade9b7f6dd087178caedeee7"}, - {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9b668c065968c5979fe6b6fa6760bb6ab9aeb94b75b73c0a9c1acf6393ac3bf"}, - {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:517d75522b7b18a3385726b54a081afd425d4f41144a5399e5abd97ccafdf36b"}, - {file = "multidict-5.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b4ac3ba7a97b35a5ccf34f41b5a8642a01d1e55454b699e5e8e7a99b5a3acf5"}, - {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:df23c83398715b26ab09574217ca21e14694917a0c857e356fd39e1c64f8283f"}, - {file = "multidict-5.2.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e58a9b5cc96e014ddf93c2227cbdeca94b56a7eb77300205d6e4001805391747"}, - {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f76440e480c3b2ca7f843ff8a48dc82446b86ed4930552d736c0bac507498a52"}, - {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cfde464ca4af42a629648c0b0d79b8f295cf5b695412451716531d6916461628"}, - {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0fed465af2e0eb6357ba95795d003ac0bdb546305cc2366b1fc8f0ad67cc3fda"}, - {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:b70913cbf2e14275013be98a06ef4b412329fe7b4f83d64eb70dce8269ed1e1a"}, - {file = "multidict-5.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5635bcf1b75f0f6ef3c8a1ad07b500104a971e38d3683167b9454cb6465ac86"}, - {file = "multidict-5.2.0-cp310-cp310-win32.whl", hash = "sha256:77f0fb7200cc7dedda7a60912f2059086e29ff67cefbc58d2506638c1a9132d7"}, - {file = "multidict-5.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:9416cf11bcd73c861267e88aea71e9fcc35302b3943e45e1dbb4317f91a4b34f"}, - {file = "multidict-5.2.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:fd77c8f3cba815aa69cb97ee2b2ef385c7c12ada9c734b0f3b32e26bb88bbf1d"}, - {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ec9aea6223adf46999f22e2c0ab6cf33f5914be604a404f658386a8f1fba37"}, - {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5283c0a00f48e8cafcecadebfa0ed1dac8b39e295c7248c44c665c16dc1138b"}, - {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f79c19c6420962eb17c7e48878a03053b7ccd7b69f389d5831c0a4a7f1ac0a1"}, - {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e4a67f1080123de76e4e97a18d10350df6a7182e243312426d508712e99988d4"}, - {file = "multidict-5.2.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:94b117e27efd8e08b4046c57461d5a114d26b40824995a2eb58372b94f9fca02"}, - {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:2e77282fd1d677c313ffcaddfec236bf23f273c4fba7cdf198108f5940ae10f5"}, - {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:116347c63ba049c1ea56e157fa8aa6edaf5e92925c9b64f3da7769bdfa012858"}, - {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:dc3a866cf6c13d59a01878cd806f219340f3e82eed514485e094321f24900677"}, - {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac42181292099d91217a82e3fa3ce0e0ddf3a74fd891b7c2b347a7f5aa0edded"}, - {file = "multidict-5.2.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:f0bb0973f42ffcb5e3537548e0767079420aefd94ba990b61cf7bb8d47f4916d"}, - {file = "multidict-5.2.0-cp36-cp36m-win32.whl", hash = "sha256:ea21d4d5104b4f840b91d9dc8cbc832aba9612121eaba503e54eaab1ad140eb9"}, - {file = "multidict-5.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:e6453f3cbeb78440747096f239d282cc57a2997a16b5197c9bc839099e1633d0"}, - {file = "multidict-5.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d3def943bfd5f1c47d51fd324df1e806d8da1f8e105cc7f1c76a1daf0f7e17b0"}, - {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35591729668a303a02b06e8dba0eb8140c4a1bfd4c4b3209a436a02a5ac1de11"}, - {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8cacda0b679ebc25624d5de66c705bc53dcc7c6f02a7fb0f3ca5e227d80422"}, - {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:baf1856fab8212bf35230c019cde7c641887e3fc08cadd39d32a421a30151ea3"}, - {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a43616aec0f0d53c411582c451f5d3e1123a68cc7b3475d6f7d97a626f8ff90d"}, - {file = "multidict-5.2.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25cbd39a9029b409167aa0a20d8a17f502d43f2efebfe9e3ac019fe6796c59ac"}, - {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a2cbcfbea6dc776782a444db819c8b78afe4db597211298dd8b2222f73e9cd0"}, - {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3d2d7d1fff8e09d99354c04c3fd5b560fb04639fd45926b34e27cfdec678a704"}, - {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a37e9a68349f6abe24130846e2f1d2e38f7ddab30b81b754e5a1fde32f782b23"}, - {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:637c1896497ff19e1ee27c1c2c2ddaa9f2d134bbb5e0c52254361ea20486418d"}, - {file = "multidict-5.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9815765f9dcda04921ba467957be543423e5ec6a1136135d84f2ae092c50d87b"}, - {file = "multidict-5.2.0-cp37-cp37m-win32.whl", hash = "sha256:8b911d74acdc1fe2941e59b4f1a278a330e9c34c6c8ca1ee21264c51ec9b67ef"}, - {file = "multidict-5.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:380b868f55f63d048a25931a1632818f90e4be71d2081c2338fcf656d299949a"}, - {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e7d81ce5744757d2f05fc41896e3b2ae0458464b14b5a2c1e87a6a9d69aefaa8"}, - {file = "multidict-5.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d1d55cdf706ddc62822d394d1df53573d32a7a07d4f099470d3cb9323b721b6"}, - {file = "multidict-5.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4771d0d0ac9d9fe9e24e33bed482a13dfc1256d008d101485fe460359476065"}, - {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da7d57ea65744d249427793c042094c4016789eb2562576fb831870f9c878d9e"}, - {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdd68778f96216596218b4e8882944d24a634d984ee1a5a049b300377878fa7c"}, - {file = "multidict-5.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecc99bce8ee42dcad15848c7885197d26841cb24fa2ee6e89d23b8993c871c64"}, - {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:067150fad08e6f2dd91a650c7a49ba65085303fcc3decbd64a57dc13a2733031"}, - {file = "multidict-5.2.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:78c106b2b506b4d895ddc801ff509f941119394b89c9115580014127414e6c2d"}, - {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e6c4fa1ec16e01e292315ba76eb1d012c025b99d22896bd14a66628b245e3e01"}, - {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:b227345e4186809d31f22087d0265655114af7cda442ecaf72246275865bebe4"}, - {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:06560fbdcf22c9387100979e65b26fba0816c162b888cb65b845d3def7a54c9b"}, - {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:7878b61c867fb2df7a95e44b316f88d5a3742390c99dfba6c557a21b30180cac"}, - {file = "multidict-5.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:246145bff76cc4b19310f0ad28bd0769b940c2a49fc601b86bfd150cbd72bb22"}, - {file = "multidict-5.2.0-cp38-cp38-win32.whl", hash = "sha256:c30ac9f562106cd9e8071c23949a067b10211917fdcb75b4718cf5775356a940"}, - {file = "multidict-5.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:f19001e790013ed580abfde2a4465388950728861b52f0da73e8e8a9418533c0"}, - {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c1ff762e2ee126e6f1258650ac641e2b8e1f3d927a925aafcfde943b77a36d24"}, - {file = "multidict-5.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd6c9c50bf2ad3f0448edaa1a3b55b2e6866ef8feca5d8dbec10ec7c94371d21"}, - {file = "multidict-5.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc66d4016f6e50ed36fb39cd287a3878ffcebfa90008535c62e0e90a7ab713ae"}, - {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9acb76d5f3dd9421874923da2ed1e76041cb51b9337fd7f507edde1d86535d6"}, - {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfc924a7e946dd3c6360e50e8f750d51e3ef5395c95dc054bc9eab0f70df4f9c"}, - {file = "multidict-5.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fdba7333eb2351fee2596b756d730d62b5827d5e1ab2f84e6cbb287cc67fe0"}, - {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b9aad49466b8d828b96b9e3630006234879c8d3e2b0a9d99219b3121bc5cdb17"}, - {file = "multidict-5.2.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:93de39267c4c676c9ebb2057e98a8138bade0d806aad4d864322eee0803140a0"}, - {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9bef5cff994ca3026fcc90680e326d1a19df9841c5e3d224076407cc21471a1"}, - {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:5f841c4f14331fd1e36cbf3336ed7be2cb2a8f110ce40ea253e5573387db7621"}, - {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:38ba256ee9b310da6a1a0f013ef4e422fca30a685bcbec86a969bd520504e341"}, - {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:3bc3b1621b979621cee9f7b09f024ec76ec03cc365e638126a056317470bde1b"}, - {file = "multidict-5.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6ee908c070020d682e9b42c8f621e8bb10c767d04416e2ebe44e37d0f44d9ad5"}, - {file = "multidict-5.2.0-cp39-cp39-win32.whl", hash = "sha256:1c7976cd1c157fa7ba5456ae5d31ccdf1479680dc9b8d8aa28afabc370df42b8"}, - {file = "multidict-5.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:c9631c642e08b9fff1c6255487e62971d8b8e821808ddd013d8ac058087591ac"}, - {file = "multidict-5.2.0.tar.gz", hash = "sha256:0dd1c93edb444b33ba2274b66f63def8a327d607c6c790772f448a53b6ea59ce"}, + {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, + {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac0e27844758d7177989ce406acc6a83c16ed4524ebc363c1f748cba184d89d3"}, + {file = "multidict-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:041b81a5f6b38244b34dc18c7b6aba91f9cdaf854d9a39e5ff0b58e2b5773b9c"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fdda29a3c7e76a064f2477c9aab1ba96fd94e02e386f1e665bca1807fc5386f"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3368bf2398b0e0fcbf46d85795adc4c259299fec50c1416d0f77c0a843a3eed9"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4f052ee022928d34fe1f4d2bc743f32609fb79ed9c49a1710a5ad6b2198db20"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:225383a6603c086e6cef0f2f05564acb4f4d5f019a4e3e983f572b8530f70c88"}, + {file = "multidict-6.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50bd442726e288e884f7be9071016c15a8742eb689a593a0cac49ea093eef0a7"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:47e6a7e923e9cada7c139531feac59448f1f47727a79076c0b1ee80274cd8eee"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0556a1d4ea2d949efe5fd76a09b4a82e3a4a30700553a6725535098d8d9fb672"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:626fe10ac87851f4cffecee161fc6f8f9853f0f6f1035b59337a51d29ff3b4f9"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:8064b7c6f0af936a741ea1efd18690bacfbae4078c0c385d7c3f611d11f0cf87"}, + {file = "multidict-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2d36e929d7f6a16d4eb11b250719c39560dd70545356365b494249e2186bc389"}, + {file = "multidict-6.0.2-cp310-cp310-win32.whl", hash = "sha256:fcb91630817aa8b9bc4a74023e4198480587269c272c58b3279875ed7235c293"}, + {file = "multidict-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:8cbf0132f3de7cc6c6ce00147cc78e6439ea736cee6bca4f068bcf892b0fd658"}, + {file = "multidict-6.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:05f6949d6169878a03e607a21e3b862eaf8e356590e8bdae4227eedadacf6e51"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2c2e459f7050aeb7c1b1276763364884595d47000c1cddb51764c0d8976e608"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0509e469d48940147e1235d994cd849a8f8195e0bca65f8f5439c56e17872a3"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:514fe2b8d750d6cdb4712346a2c5084a80220821a3e91f3f71eec11cf8d28fd4"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19adcfc2a7197cdc3987044e3f415168fc5dc1f720c932eb1ef4f71a2067e08b"}, + {file = "multidict-6.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9d153e7f1f9ba0b23ad1568b3b9e17301e23b042c23870f9ee0522dc5cc79e8"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aef9cc3d9c7d63d924adac329c33835e0243b5052a6dfcbf7732a921c6e918ba"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4571f1beddff25f3e925eea34268422622963cd8dc395bb8778eb28418248e43"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:d48b8ee1d4068561ce8033d2c344cf5232cb29ee1a0206a7b828c79cbc5982b8"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:45183c96ddf61bf96d2684d9fbaf6f3564d86b34cb125761f9a0ef9e36c1d55b"}, + {file = "multidict-6.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:75bdf08716edde767b09e76829db8c1e5ca9d8bb0a8d4bd94ae1eafe3dac5e15"}, + {file = "multidict-6.0.2-cp37-cp37m-win32.whl", hash = "sha256:a45e1135cb07086833ce969555df39149680e5471c04dfd6a915abd2fc3f6dbc"}, + {file = "multidict-6.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6f3cdef8a247d1eafa649085812f8a310e728bdf3900ff6c434eafb2d443b23a"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0327292e745a880459ef71be14e709aaea2f783f3537588fb4ed09b6c01bca60"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e875b6086e325bab7e680e4316d667fc0e5e174bb5611eb16b3ea121c8951b86"}, + {file = "multidict-6.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:feea820722e69451743a3d56ad74948b68bf456984d63c1a92e8347b7b88452d"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc57c68cb9139c7cd6fc39f211b02198e69fb90ce4bc4a094cf5fe0d20fd8b0"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:497988d6b6ec6ed6f87030ec03280b696ca47dbf0648045e4e1d28b80346560d"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:89171b2c769e03a953d5969b2f272efa931426355b6c0cb508022976a17fd376"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684133b1e1fe91eda8fa7447f137c9490a064c6b7f392aa857bba83a28cfb693"}, + {file = "multidict-6.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd9fc9c4849a07f3635ccffa895d57abce554b467d611a5009ba4f39b78a8849"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e07c8e79d6e6fd37b42f3250dba122053fddb319e84b55dd3a8d6446e1a7ee49"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4070613ea2227da2bfb2c35a6041e4371b0af6b0be57f424fe2318b42a748516"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:47fbeedbf94bed6547d3aa632075d804867a352d86688c04e606971595460227"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:5774d9218d77befa7b70d836004a768fb9aa4fdb53c97498f4d8d3f67bb9cfa9"}, + {file = "multidict-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2957489cba47c2539a8eb7ab32ff49101439ccf78eab724c828c1a54ff3ff98d"}, + {file = "multidict-6.0.2-cp38-cp38-win32.whl", hash = "sha256:e5b20e9599ba74391ca0cfbd7b328fcc20976823ba19bc573983a25b32e92b57"}, + {file = "multidict-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:8004dca28e15b86d1b1372515f32eb6f814bdf6f00952699bdeb541691091f96"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2e4a0785b84fb59e43c18a015ffc575ba93f7d1dbd272b4cdad9f5134b8a006c"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6701bf8a5d03a43375909ac91b6980aea74b0f5402fbe9428fc3f6edf5d9677e"}, + {file = "multidict-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a007b1638e148c3cfb6bf0bdc4f82776cef0ac487191d093cdc316905e504071"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07a017cfa00c9890011628eab2503bee5872f27144936a52eaab449be5eaf032"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c207fff63adcdf5a485969131dc70e4b194327666b7e8a87a97fbc4fd80a53b2"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:373ba9d1d061c76462d74e7de1c0c8e267e9791ee8cfefcf6b0b2495762c370c"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfba7c6d5d7c9099ba21f84662b037a0ffd4a5e6b26ac07d19e423e6fdf965a9"}, + {file = "multidict-6.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19d9bad105dfb34eb539c97b132057a4e709919ec4dd883ece5838bcbf262b80"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:de989b195c3d636ba000ee4281cd03bb1234635b124bf4cd89eeee9ca8fcb09d"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7c40b7bbece294ae3a87c1bc2abff0ff9beef41d14188cda94ada7bcea99b0fb"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:d16cce709ebfadc91278a1c005e3c17dd5f71f5098bfae1035149785ea6e9c68"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:a2c34a93e1d2aa35fbf1485e5010337c72c6791407d03aa5f4eed920343dd360"}, + {file = "multidict-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:feba80698173761cddd814fa22e88b0661e98cb810f9f986c54aa34d281e4937"}, + {file = "multidict-6.0.2-cp39-cp39-win32.whl", hash = "sha256:23b616fdc3c74c9fe01d76ce0d1ce872d2d396d8fa8e4899398ad64fb5aa214a"}, + {file = "multidict-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:4bae31803d708f6f15fd98be6a6ac0b6958fcf68fda3c77a048a4f9073704aae"}, + {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, ] nodeenv = [ {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, ] ordered-set = [ - {file = "ordered-set-4.0.2.tar.gz", hash = "sha256:ba93b2df055bca202116ec44b9bead3df33ea63a7d5827ff8e16738b97f33a95"}, + {file = "ordered-set-4.1.0.tar.gz", hash = "sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"}, + {file = "ordered_set-4.1.0-py3-none-any.whl", hash = "sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562"}, ] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, @@ -1666,16 +1681,16 @@ pip-licenses = [ {file = "pip_licenses-3.5.3-py3-none-any.whl", hash = "sha256:59c148d6a03784bf945d232c0dc0e9de4272a3675acaa0361ad7712398ca86ba"}, ] platformdirs = [ - {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, - {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, + {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, + {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pre-commit = [ - {file = "pre_commit-2.16.0-py2.py3-none-any.whl", hash = "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e"}, - {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"}, + {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, + {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, ] psutil = [ {file = "psutil-5.9.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:55ce319452e3d139e25d6c3f85a1acf12d1607ddedea5e35fb47a552c051161b"}, @@ -1768,12 +1783,12 @@ pyflakes = [ {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] pyparsing = [ - {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, - {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] pyreadline3 = [ - {file = "pyreadline3-3.3-py3-none-any.whl", hash = "sha256:0003fd0079d152ecbd8111202c5a7dfa6a5569ffd65b235e45f3c2ecbee337b4"}, - {file = "pyreadline3-3.3.tar.gz", hash = "sha256:ff3b5a1ac0010d0967869f723e687d42cabc7dccf33b14934c92aa5168d260b3"}, + {file = "pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb"}, + {file = "pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, @@ -1889,8 +1904,8 @@ rapidfuzz = [ {file = "rapidfuzz-1.9.1.tar.gz", hash = "sha256:bd7a4fe33ba49db3417f0f57a8af02462554f1296dedcf35b026cd3525efef74"}, ] redis = [ - {file = "redis-4.0.2-py3-none-any.whl", hash = "sha256:c8481cf414474e3497ec7971a1ba9b998c8efad0f0d289a009a5bbef040894f9"}, - {file = "redis-4.0.2.tar.gz", hash = "sha256:ccf692811f2c1fc7a92b466aa2599e4a6d2d73d5f736a2c70be600657c0da34a"}, + {file = "redis-4.1.4-py3-none-any.whl", hash = "sha256:04629f8e42be942c4f7d1812f2094568f04c612865ad19ad3ace3005da70631a"}, + {file = "redis-4.1.4.tar.gz", hash = "sha256:1d9a0cdf89fdd93f84261733e24f55a7bbd413a9b219fdaf56e3e728ca9a2306"}, ] regex = [ {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, @@ -1944,8 +1959,8 @@ requests-file = [ {file = "requests_file-1.5.1-py2.py3-none-any.whl", hash = "sha256:dfe5dae75c12481f68ba353183c53a65e6044c923e64c24b2209f6c7570ca953"}, ] sentry-sdk = [ - {file = "sentry-sdk-1.5.1.tar.gz", hash = "sha256:2a1757d6611e4bec7d672c2b7ef45afef79fed201d064f53994753303944f5a8"}, - {file = "sentry_sdk-1.5.1-py2.py3-none-any.whl", hash = "sha256:e4cb107e305b2c1b919414775fa73a9997f996447417d22b98e7610ded1e9eb5"}, + {file = "sentry-sdk-1.5.5.tar.gz", hash = "sha256:98fd155fa5d5fec1dbabed32a1a4ae2705f1edaa5dae4e7f7b62a384ba30e759"}, + {file = "sentry_sdk-1.5.5-py2.py3-none-any.whl", hash = "sha256:3817274fba2498c8ebf6b896ee98ac916c5598706340573268c07bf2bb30d831"}, ] sgmllib3k = [ {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, @@ -1987,16 +2002,16 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] urllib3 = [ {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] virtualenv = [ - {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"}, - {file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"}, + {file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"}, + {file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"}, ] wrapt = [ {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, diff --git a/pyproject.toml b/pyproject.toml index c764910c2..90b38ce66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ license = "MIT" [tool.poetry.dependencies] python = "3.9.*" -"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip"} +disnake = "~=2.4" # See https://bot-core.pythondiscord.com/ for docs. bot-core = {url = "https://github.com/python-discord/bot-core/archive/511bcba1b0196cd498c707a525ea56921bd971db.zip"} aio-pika = "~=6.1" -- cgit v1.2.3 From 960619c23300c56c8aaa454edc7241e2badf80ad Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 21 Feb 2022 02:14:07 +0000 Subject: Update all references of discord.py to disnake All of the tag content is out of scope for this PR. --- bot/__init__.py | 2 +- bot/bot.py | 20 ++-- bot/converters.py | 24 ++-- bot/decorators.py | 10 +- bot/errors.py | 2 +- bot/exts/backend/branding/_cog.py | 18 +-- bot/exts/backend/config_verifier.py | 2 +- bot/exts/backend/error_handler.py | 4 +- bot/exts/backend/logging.py | 4 +- bot/exts/backend/sync/_cog.py | 6 +- bot/exts/backend/sync/_syncers.py | 4 +- bot/exts/events/code_jams/_channels.py | 42 +++---- bot/exts/events/code_jams/_cog.py | 24 ++-- bot/exts/filters/antimalware.py | 4 +- bot/exts/filters/antispam.py | 8 +- bot/exts/filters/filter_lists.py | 4 +- bot/exts/filters/filtering.py | 26 ++--- bot/exts/filters/security.py | 2 +- bot/exts/filters/token_remover.py | 6 +- bot/exts/filters/webhook_remover.py | 4 +- bot/exts/fun/duck_pond.py | 22 ++-- bot/exts/fun/off_topic_names.py | 6 +- bot/exts/help_channels/_caches.py | 14 +-- bot/exts/help_channels/_channel.py | 18 +-- bot/exts/help_channels/_cog.py | 64 +++++------ bot/exts/help_channels/_message.py | 36 +++--- bot/exts/help_channels/_name.py | 6 +- bot/exts/info/code_snippets.py | 8 +- bot/exts/info/codeblock/_cog.py | 22 ++-- bot/exts/info/doc/_batch_parser.py | 4 +- bot/exts/info/doc/_cog.py | 18 +-- bot/exts/info/help.py | 4 +- bot/exts/info/information.py | 8 +- bot/exts/info/pep.py | 4 +- bot/exts/info/pypi.py | 6 +- bot/exts/info/python_news.py | 12 +- bot/exts/info/source.py | 4 +- bot/exts/info/stats.py | 6 +- bot/exts/info/subscribe.py | 26 ++--- bot/exts/info/tags.py | 16 +-- bot/exts/moderation/clean.py | 10 +- bot/exts/moderation/defcon.py | 6 +- bot/exts/moderation/dm_relay.py | 6 +- bot/exts/moderation/incidents.py | 100 ++++++++--------- bot/exts/moderation/infraction/_scheduler.py | 14 +-- bot/exts/moderation/infraction/_utils.py | 14 +-- bot/exts/moderation/infraction/infractions.py | 26 ++--- bot/exts/moderation/infraction/management.py | 36 +++--- bot/exts/moderation/infraction/superstarify.py | 6 +- bot/exts/moderation/metabase.py | 2 +- bot/exts/moderation/modlog.py | 72 ++++++------ bot/exts/moderation/modpings.py | 8 +- bot/exts/moderation/silence.py | 8 +- bot/exts/moderation/slowmode.py | 4 +- bot/exts/moderation/stream.py | 24 ++-- bot/exts/moderation/verification.py | 16 +-- bot/exts/moderation/voice_gate.py | 42 +++---- bot/exts/moderation/watchchannels/_watchchannel.py | 12 +- bot/exts/moderation/watchchannels/bigbrother.py | 4 +- bot/exts/recruitment/talentpool/_cog.py | 8 +- bot/exts/recruitment/talentpool/_review.py | 4 +- bot/exts/utils/bot.py | 4 +- bot/exts/utils/extensions.py | 6 +- bot/exts/utils/internal.py | 17 +-- bot/exts/utils/ping.py | 4 +- bot/exts/utils/reminders.py | 32 +++--- bot/exts/utils/snekbox.py | 4 +- bot/exts/utils/thread_bumper.py | 24 ++-- bot/exts/utils/utils.py | 6 +- bot/log.py | 2 +- bot/monkey_patches.py | 6 +- bot/pagination.py | 18 +-- bot/rules/attachments.py | 2 +- bot/rules/burst.py | 2 +- bot/rules/burst_shared.py | 2 +- bot/rules/chars.py | 2 +- bot/rules/discord_emojis.py | 2 +- bot/rules/duplicates.py | 2 +- bot/rules/links.py | 2 +- bot/rules/mentions.py | 2 +- bot/rules/newlines.py | 2 +- bot/rules/role_mentions.py | 2 +- bot/utils/channel.py | 16 +-- bot/utils/checks.py | 4 +- bot/utils/function.py | 6 +- bot/utils/helpers.py | 2 +- bot/utils/members.py | 18 +-- bot/utils/message_cache.py | 2 +- bot/utils/messages.py | 48 ++++---- bot/utils/webhooks.py | 10 +- poetry.lock | 12 +- pyproject.toml | 2 +- tests/README.md | 12 +- tests/base.py | 8 +- tests/bot/exts/backend/sync/test_cog.py | 6 +- tests/bot/exts/backend/sync/test_roles.py | 6 +- tests/bot/exts/backend/sync/test_users.py | 2 +- tests/bot/exts/backend/test_error_handler.py | 2 +- tests/bot/exts/events/test_code_jams.py | 4 +- tests/bot/exts/filters/test_antimalware.py | 2 +- tests/bot/exts/filters/test_security.py | 2 +- tests/bot/exts/filters/test_token_remover.py | 2 +- tests/bot/exts/info/test_information.py | 22 ++-- .../exts/moderation/infraction/test_infractions.py | 2 +- tests/bot/exts/moderation/infraction/test_utils.py | 2 +- tests/bot/exts/moderation/test_incidents.py | 20 ++-- tests/bot/exts/moderation/test_modlog.py | 4 +- tests/bot/exts/moderation/test_silence.py | 4 +- tests/bot/exts/test_cogs.py | 4 +- tests/bot/exts/utils/test_snekbox.py | 4 +- tests/bot/test_converters.py | 2 +- tests/bot/utils/test_checks.py | 2 +- tests/helpers.py | 124 ++++++++++----------- tests/test_helpers.py | 28 ++--- 114 files changed, 735 insertions(+), 734 deletions(-) diff --git a/bot/__init__.py b/bot/__init__.py index 17d99105a..b28513bff 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -3,7 +3,7 @@ import os from functools import partial, partialmethod from typing import TYPE_CHECKING -from discord.ext import commands +from disnake.ext import commands from bot import log, monkey_patches diff --git a/bot/bot.py b/bot/bot.py index 94783a466..2769b7dda 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -6,9 +6,9 @@ from contextlib import suppress from typing import Dict, List, Optional import aiohttp -import discord +import disnake from async_rediscache import RedisSession -from discord.ext import commands +from disnake.ext import commands from sentry_sdk import push_scope from bot import api, constants @@ -28,7 +28,7 @@ class StartupError(Exception): class Bot(commands.Bot): - """A subclass of `discord.ext.commands.Bot` with an aiohttp session and an API client.""" + """A subclass of `disnake.ext.commands.Bot` with an aiohttp session and an API client.""" def __init__(self, *args, redis_session: RedisSession, **kwargs): if "connector" in kwargs: @@ -109,9 +109,9 @@ class Bot(commands.Bot): def create(cls) -> "Bot": """Create and return an instance of a Bot.""" loop = asyncio.get_event_loop() - allowed_roles = list({discord.Object(id_) for id_ in constants.MODERATION_ROLES}) + allowed_roles = list({disnake.Object(id_) for id_ in constants.MODERATION_ROLES}) - intents = discord.Intents.all() + intents = disnake.Intents.all() intents.presences = False intents.dm_typing = False intents.dm_reactions = False @@ -123,10 +123,10 @@ class Bot(commands.Bot): redis_session=_create_redis_session(loop), loop=loop, command_prefix=commands.when_mentioned_or(constants.Bot.prefix), - activity=discord.Game(name=f"Commands: {constants.Bot.prefix}help"), + activity=disnake.Game(name=f"Commands: {constants.Bot.prefix}help"), case_insensitive=True, max_messages=10_000, - allowed_mentions=discord.AllowedMentions(everyone=False, roles=allowed_roles), + allowed_mentions=disnake.AllowedMentions(everyone=False, roles=allowed_roles), intents=intents, ) @@ -258,7 +258,7 @@ class Bot(commands.Bot): await self.stats.create_socket() await super().login(*args, **kwargs) - async def on_guild_available(self, guild: discord.Guild) -> None: + async def on_guild_available(self, guild: disnake.Guild) -> None: """ Set the internal guild available event when constants.Guild.id becomes available. @@ -274,7 +274,7 @@ class Bot(commands.Bot): try: webhook = await self.fetch_webhook(constants.Webhooks.dev_log) - except discord.HTTPException as e: + except disnake.HTTPException as e: log.error(f"Failed to fetch webhook to send empty cache warning: status {e.status}") else: await webhook.send(f"<@&{constants.Roles.admin}> {msg}") @@ -283,7 +283,7 @@ class Bot(commands.Bot): self._guild_available.set() - async def on_guild_unavailable(self, guild: discord.Guild) -> None: + async def on_guild_unavailable(self, guild: disnake.Guild) -> None: """Clear the internal guild available event when constants.Guild.id becomes unavailable.""" if guild.id != constants.Guild.id: return diff --git a/bot/converters.py b/bot/converters.py index 3522a32aa..9d93428ca 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -6,12 +6,12 @@ from datetime import datetime, timezone from ssl import CertificateError import dateutil.parser -import discord +import disnake from aiohttp import ClientConnectorError from botcore.regex import DISCORD_INVITE from dateutil.relativedelta import relativedelta -from discord.ext.commands import BadArgument, Bot, Context, Converter, IDConverter, MemberConverter, UserConverter -from discord.utils import escape_markdown, snowflake_time +from disnake.ext.commands import BadArgument, Bot, Context, Converter, IDConverter, MemberConverter, UserConverter +from disnake.utils import escape_markdown, snowflake_time from bot import exts from bot.api import ResponseCodeError @@ -505,14 +505,14 @@ AMBIGUOUS_ARGUMENT_MSG = ("`{argument}` is not a User mention, a User ID or a Us class UnambiguousUser(UserConverter): """ - Converts to a `discord.User`, but only if a mention, userID or a username (name#discrim) is provided. + Converts to a `disnake.User`, but only if a mention, userID or a username (name#discrim) is provided. Unlike the default `UserConverter`, it doesn't allow conversion from a name. This is useful in cases where that lookup strategy would lead to too much ambiguity. """ - async def convert(self, ctx: Context, argument: str) -> discord.User: - """Convert the `argument` to a `discord.User`.""" + async def convert(self, ctx: Context, argument: str) -> disnake.User: + """Convert the `argument` to a `disnake.User`.""" if _is_an_unambiguous_user_argument(argument): return await super().convert(ctx, argument) else: @@ -521,14 +521,14 @@ class UnambiguousUser(UserConverter): class UnambiguousMember(MemberConverter): """ - Converts to a `discord.Member`, but only if a mention, userID or a username (name#discrim) is provided. + Converts to a `disnake.Member`, but only if a mention, userID or a username (name#discrim) is provided. Unlike the default `MemberConverter`, it doesn't allow conversion from a name or nickname. This is useful in cases where that lookup strategy would lead to too much ambiguity. """ - async def convert(self, ctx: Context, argument: str) -> discord.Member: - """Convert the `argument` to a `discord.Member`.""" + async def convert(self, ctx: Context, argument: str) -> disnake.Member: + """Convert the `argument` to a `disnake.Member`.""" if _is_an_unambiguous_user_argument(argument): return await super().convert(ctx, argument) else: @@ -588,10 +588,10 @@ if t.TYPE_CHECKING: OffTopicName = str # noqa: F811 ISODateTime = datetime # noqa: F811 HushDurationConverter = int # noqa: F811 - UnambiguousUser = discord.User # noqa: F811 - UnambiguousMember = discord.Member # noqa: F811 + UnambiguousUser = disnake.User # noqa: F811 + UnambiguousMember = disnake.Member # noqa: F811 Infraction = t.Optional[dict] # noqa: F811 Expiry = t.Union[Duration, ISODateTime] -MemberOrUser = t.Union[discord.Member, discord.User] +MemberOrUser = t.Union[disnake.Member, disnake.User] UnambiguousMemberOrUser = t.Union[UnambiguousMember, UnambiguousUser] diff --git a/bot/decorators.py b/bot/decorators.py index f4331264f..9ae98442c 100644 --- a/bot/decorators.py +++ b/bot/decorators.py @@ -4,9 +4,9 @@ import types import typing as t from contextlib import suppress -from discord import Member, NotFound -from discord.ext import commands -from discord.ext.commands import Cog, Context +from disnake import Member, NotFound +from disnake.ext import commands +from disnake.ext.commands import Cog, Context from bot.constants import Channels, DEBUG_MODE, RedirectOutput from bot.log import get_logger @@ -179,7 +179,7 @@ def respect_role_hierarchy(member_arg: function.Argument) -> t.Callable: Ensure the highest role of the invoking member is greater than that of the target member. If the condition fails, a warning is sent to the invoking context. A target which is not an - instance of discord.Member will always pass. + instance of disnake.Member will always pass. `member_arg` is the keyword name or position index of the parameter of the decorated command whose value is the target member. @@ -195,7 +195,7 @@ def respect_role_hierarchy(member_arg: function.Argument) -> t.Callable: target = function.get_arg_value(member_arg, bound_args) if not isinstance(target, Member): - log.trace("The target is not a discord.Member; skipping role hierarchy check.") + log.trace("The target is not a disnake.Member; skipping role hierarchy check.") return await func(*args, **kwargs) ctx = function.get_arg_value(1, bound_args) diff --git a/bot/errors.py b/bot/errors.py index 078b645f1..298e7ac2d 100644 --- a/bot/errors.py +++ b/bot/errors.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Hashable, TYPE_CHECKING, Union -from discord.ext.commands import ConversionError, Converter +from disnake.ext.commands import ConversionError, Converter if TYPE_CHECKING: from bot.converters import MemberOrUser diff --git a/bot/exts/backend/branding/_cog.py b/bot/exts/backend/branding/_cog.py index 0c5839a7a..a07e70d58 100644 --- a/bot/exts/backend/branding/_cog.py +++ b/bot/exts/backend/branding/_cog.py @@ -7,10 +7,10 @@ from enum import Enum from operator import attrgetter import async_timeout -import discord +import disnake from arrow import Arrow from async_rediscache import RedisCache -from discord.ext import commands, tasks +from disnake.ext import commands, tasks from bot.bot import Bot from bot.constants import Branding as BrandingConfig, Channels, Colours, Guild, MODERATION_ROLES @@ -42,7 +42,7 @@ def compound_hash(objects: t.Iterable[RemoteObject]) -> str: return "-".join(item.sha for item in objects) -def make_embed(title: str, description: str, *, success: bool) -> discord.Embed: +def make_embed(title: str, description: str, *, success: bool) -> disnake.Embed: """ Construct simple response embed. @@ -51,7 +51,7 @@ def make_embed(title: str, description: str, *, success: bool) -> discord.Embed: For both `title` and `description`, empty string are valid values ~ fields will be empty. """ colour = Colours.soft_green if success else Colours.soft_red - return discord.Embed(title=title[:256], description=description[:4096], colour=colour) + return disnake.Embed(title=title[:256], description=description[:4096], colour=colour) def extract_event_duration(event: Event) -> str: @@ -147,13 +147,13 @@ class Branding(commands.Cog): return False await self.bot.wait_until_guild_available() - pydis: discord.Guild = self.bot.get_guild(Guild.id) + pydis: disnake.Guild = self.bot.get_guild(Guild.id) timeout = 10 # Seconds. try: with async_timeout.timeout(timeout): # Raise after `timeout` seconds. await pydis.edit(**{asset_type.value: file}) - except discord.HTTPException: + except disnake.HTTPException: log.exception("Asset upload to Discord failed.") return False except asyncio.TimeoutError: @@ -277,7 +277,7 @@ class Branding(commands.Cog): log.debug(f"Sending event information event to channel: {channel_id} ({is_notification=}).") await self.bot.wait_until_guild_available() - channel: t.Optional[discord.TextChannel] = self.bot.get_channel(channel_id) + channel: t.Optional[disnake.TextChannel] = self.bot.get_channel(channel_id) if channel is None: log.warning(f"Cannot send event information: channel {channel_id} not found!") @@ -294,7 +294,7 @@ class Branding(commands.Cog): else: content = "Python Discord is entering a new event!" if is_notification else None - embed = discord.Embed(description=description[:4096], colour=discord.Colour.og_blurple()) + embed = disnake.Embed(description=description[:4096], colour=disnake.Colour.og_blurple()) embed.set_footer(text=duration[:4096]) await channel.send(content=content, embed=embed) @@ -573,7 +573,7 @@ class Branding(commands.Cog): await ctx.send(embed=resp) return - embed = discord.Embed(title="Current event calendar", colour=discord.Colour.og_blurple()) + embed = disnake.Embed(title="Current event calendar", colour=disnake.Colour.og_blurple()) # Because Discord embeds can only contain up to 25 fields, we only show the first 25. first_25 = list(available_events.items())[:25] diff --git a/bot/exts/backend/config_verifier.py b/bot/exts/backend/config_verifier.py index dc85a65a2..1ade2bce7 100644 --- a/bot/exts/backend/config_verifier.py +++ b/bot/exts/backend/config_verifier.py @@ -1,4 +1,4 @@ -from discord.ext.commands import Cog +from disnake.ext.commands import Cog from bot import constants from bot.bot import Bot diff --git a/bot/exts/backend/error_handler.py b/bot/exts/backend/error_handler.py index c79c7b2a7..953843a77 100644 --- a/bot/exts/backend/error_handler.py +++ b/bot/exts/backend/error_handler.py @@ -1,7 +1,7 @@ import difflib -from discord import Embed -from discord.ext.commands import ChannelNotFound, Cog, Context, TextChannelConverter, VoiceChannelConverter, errors +from disnake import Embed +from disnake.ext.commands import ChannelNotFound, Cog, Context, TextChannelConverter, VoiceChannelConverter, errors from sentry_sdk import push_scope from bot.api import ResponseCodeError diff --git a/bot/exts/backend/logging.py b/bot/exts/backend/logging.py index 2d03cd580..040fb5d37 100644 --- a/bot/exts/backend/logging.py +++ b/bot/exts/backend/logging.py @@ -1,5 +1,5 @@ -from discord import Embed -from discord.ext.commands import Cog +from disnake import Embed +from disnake.ext.commands import Cog from bot.bot import Bot from bot.constants import Channels, DEBUG_MODE diff --git a/bot/exts/backend/sync/_cog.py b/bot/exts/backend/sync/_cog.py index 80f5750bc..d08e56077 100644 --- a/bot/exts/backend/sync/_cog.py +++ b/bot/exts/backend/sync/_cog.py @@ -1,8 +1,8 @@ from typing import Any, Dict -from discord import Member, Role, User -from discord.ext import commands -from discord.ext.commands import Cog, Context +from disnake import Member, Role, User +from disnake.ext import commands +from disnake.ext.commands import Cog, Context from bot import constants from bot.api import ResponseCodeError diff --git a/bot/exts/backend/sync/_syncers.py b/bot/exts/backend/sync/_syncers.py index 45301b098..48ee3c842 100644 --- a/bot/exts/backend/sync/_syncers.py +++ b/bot/exts/backend/sync/_syncers.py @@ -2,8 +2,8 @@ import abc import typing as t from collections import namedtuple -from discord import Guild -from discord.ext.commands import Context +from disnake import Guild +from disnake.ext.commands import Context from more_itertools import chunked import bot diff --git a/bot/exts/events/code_jams/_channels.py b/bot/exts/events/code_jams/_channels.py index e8cf5f7bf..fc4693bd4 100644 --- a/bot/exts/events/code_jams/_channels.py +++ b/bot/exts/events/code_jams/_channels.py @@ -1,6 +1,6 @@ import typing as t -import discord +import disnake from bot.constants import Categories, Channels, Roles from bot.log import get_logger @@ -11,7 +11,7 @@ MAX_CHANNELS = 50 CATEGORY_NAME = "Code Jam" -async def _get_category(guild: discord.Guild) -> discord.CategoryChannel: +async def _get_category(guild: disnake.Guild) -> disnake.CategoryChannel: """ Return a code jam category. @@ -24,13 +24,13 @@ async def _get_category(guild: discord.Guild) -> discord.CategoryChannel: return await _create_category(guild) -async def _create_category(guild: discord.Guild) -> discord.CategoryChannel: +async def _create_category(guild: disnake.Guild) -> disnake.CategoryChannel: """Create a new code jam category and return it.""" log.info("Creating a new code jam category.") category_overwrites = { - guild.default_role: discord.PermissionOverwrite(read_messages=False), - guild.me: discord.PermissionOverwrite(read_messages=True) + guild.default_role: disnake.PermissionOverwrite(read_messages=False), + guild.me: disnake.PermissionOverwrite(read_messages=True) } category = await guild.create_category_channel( @@ -47,17 +47,17 @@ async def _create_category(guild: discord.Guild) -> discord.CategoryChannel: def _get_overwrites( - members: list[tuple[discord.Member, bool]], - guild: discord.Guild, -) -> dict[t.Union[discord.Member, discord.Role], discord.PermissionOverwrite]: + members: list[tuple[disnake.Member, bool]], + guild: disnake.Guild, +) -> dict[t.Union[disnake.Member, disnake.Role], disnake.PermissionOverwrite]: """Get code jam team channels permission overwrites.""" team_channel_overwrites = { - guild.default_role: discord.PermissionOverwrite(read_messages=False), - guild.get_role(Roles.code_jam_event_team): discord.PermissionOverwrite(read_messages=True) + guild.default_role: disnake.PermissionOverwrite(read_messages=False), + guild.get_role(Roles.code_jam_event_team): disnake.PermissionOverwrite(read_messages=True) } for member, _ in members: - team_channel_overwrites[member] = discord.PermissionOverwrite( + team_channel_overwrites[member] = disnake.PermissionOverwrite( read_messages=True ) @@ -65,10 +65,10 @@ def _get_overwrites( async def create_team_channel( - guild: discord.Guild, + guild: disnake.Guild, team_name: str, - members: list[tuple[discord.Member, bool]], - team_leaders: discord.Role + members: list[tuple[disnake.Member, bool]], + team_leaders: disnake.Role ) -> None: """Create the team's text channel.""" await _add_team_leader_roles(members, team_leaders) @@ -84,29 +84,29 @@ async def create_team_channel( ) -async def create_team_leader_channel(guild: discord.Guild, team_leaders: discord.Role) -> None: +async def create_team_leader_channel(guild: disnake.Guild, team_leaders: disnake.Role) -> None: """Create the Team Leader Chat channel for the Code Jam team leaders.""" - category: discord.CategoryChannel = guild.get_channel(Categories.summer_code_jam) + category: disnake.CategoryChannel = guild.get_channel(Categories.summer_code_jam) team_leaders_chat = await category.create_text_channel( name="team-leaders-chat", overwrites={ - guild.default_role: discord.PermissionOverwrite(read_messages=False), - team_leaders: discord.PermissionOverwrite(read_messages=True) + guild.default_role: disnake.PermissionOverwrite(read_messages=False), + team_leaders: disnake.PermissionOverwrite(read_messages=True) } ) await _send_status_update(guild, f"Created {team_leaders_chat.mention} in the {category} category.") -async def _send_status_update(guild: discord.Guild, message: str) -> None: +async def _send_status_update(guild: disnake.Guild, message: str) -> None: """Inform the events lead with a status update when the command is ran.""" - channel: discord.TextChannel = guild.get_channel(Channels.code_jam_planning) + channel: disnake.TextChannel = guild.get_channel(Channels.code_jam_planning) await channel.send(f"<@&{Roles.events_lead}>\n\n{message}") -async def _add_team_leader_roles(members: list[tuple[discord.Member, bool]], team_leaders: discord.Role) -> None: +async def _add_team_leader_roles(members: list[tuple[disnake.Member, bool]], team_leaders: disnake.Role) -> None: """Assign the team leader role to the team leaders.""" for member, is_leader in members: if is_leader: diff --git a/bot/exts/events/code_jams/_cog.py b/bot/exts/events/code_jams/_cog.py index 452199f5f..5cb11826d 100644 --- a/bot/exts/events/code_jams/_cog.py +++ b/bot/exts/events/code_jams/_cog.py @@ -3,9 +3,9 @@ import csv import typing as t from collections import defaultdict -import discord -from discord import Colour, Embed, Guild, Member -from discord.ext import commands +import disnake +from disnake import Colour, Embed, Guild, Member +from disnake.ext import commands from bot.bot import Bot from bot.constants import Emojis, Roles @@ -85,7 +85,7 @@ class CodeJams(commands.Cog): A confirmation message is displayed with the categories and channels to be deleted.. Pressing the added reaction deletes those channels. """ - def predicate_deletion_emoji_reaction(reaction: discord.Reaction, user: discord.User) -> bool: + def predicate_deletion_emoji_reaction(reaction: disnake.Reaction, user: disnake.User) -> bool: """Return True if the reaction :boom: was added by the context message author on this message.""" return ( reaction.message.id == message.id @@ -124,14 +124,14 @@ class CodeJams(commands.Cog): @staticmethod async def _build_confirmation_message( - categories: dict[discord.CategoryChannel, list[discord.abc.GuildChannel]] + categories: dict[disnake.CategoryChannel, list[disnake.abc.GuildChannel]] ) -> str: """Sends details of the channels to be deleted to the pasting service, and formats the confirmation message.""" - def channel_repr(channel: discord.abc.GuildChannel) -> str: + def channel_repr(channel: disnake.abc.GuildChannel) -> str: """Formats the channel name and ID and a readable format.""" return f"{channel.name} ({channel.id})" - def format_category_info(category: discord.CategoryChannel, channels: list[discord.abc.GuildChannel]) -> str: + def format_category_info(category: disnake.CategoryChannel, channels: list[disnake.abc.GuildChannel]) -> str: """Displays the category and the channels within it in a readable format.""" return f"{channel_repr(category)}:\n" + "\n".join(" - " + channel_repr(channel) for channel in channels) @@ -187,7 +187,7 @@ class CodeJams(commands.Cog): await old_team_channel.set_permissions(member, overwrite=None, reason=f"Participant moved to {new_team_name}") await new_team_channel.set_permissions( member, - overwrite=discord.PermissionOverwrite(read_messages=True), + overwrite=disnake.PermissionOverwrite(read_messages=True), reason=f"Participant moved from {old_team_channel.name}" ) @@ -212,16 +212,16 @@ class CodeJams(commands.Cog): await ctx.send(f"Removed the participant from `{self.team_name(channel)}`.") @staticmethod - def jam_categories(guild: Guild) -> list[discord.CategoryChannel]: + def jam_categories(guild: Guild) -> list[disnake.CategoryChannel]: """Get all the code jam team categories.""" return [category for category in guild.categories if category.name == _channels.CATEGORY_NAME] @staticmethod - def team_channel(guild: Guild, criterion: t.Union[str, Member]) -> t.Optional[discord.TextChannel]: + def team_channel(guild: Guild, criterion: t.Union[str, Member]) -> t.Optional[disnake.TextChannel]: """Get a team channel through either a participant or the team name.""" for category in CodeJams.jam_categories(guild): for channel in category.channels: - if isinstance(channel, discord.TextChannel): + if isinstance(channel, disnake.TextChannel): if ( # If it's a string. criterion == channel.name or criterion == CodeJams.team_name(channel) @@ -231,6 +231,6 @@ class CodeJams(commands.Cog): return channel @staticmethod - def team_name(channel: discord.TextChannel) -> str: + def team_name(channel: disnake.TextChannel) -> str: """Retrieves the team name from the given channel.""" return channel.name.replace("-", " ").title() diff --git a/bot/exts/filters/antimalware.py b/bot/exts/filters/antimalware.py index 6cccf3680..e55ece910 100644 --- a/bot/exts/filters/antimalware.py +++ b/bot/exts/filters/antimalware.py @@ -1,8 +1,8 @@ import typing as t from os.path import splitext -from discord import Embed, Message, NotFound -from discord.ext.commands import Cog +from disnake import Embed, Message, NotFound +from disnake.ext.commands import Cog from bot.bot import Bot from bot.constants import Channels, Filter, URLs diff --git a/bot/exts/filters/antispam.py b/bot/exts/filters/antispam.py index bcd845a43..c887cf5fc 100644 --- a/bot/exts/filters/antispam.py +++ b/bot/exts/filters/antispam.py @@ -8,8 +8,8 @@ from operator import attrgetter, itemgetter from typing import Dict, Iterable, List, Set import arrow -from discord import Colour, Member, Message, NotFound, Object, TextChannel -from discord.ext.commands import Cog +from disnake import Colour, Member, Message, NotFound, Object, TextChannel +from disnake.ext.commands import Cog from bot import rules from bot.bot import Bot @@ -195,7 +195,7 @@ class AntiSpam(Cog): result = await rule_function(message, messages_for_rule, rule_config) # If the rule returns `None`, that means the message didn't violate it. - # If it doesn't, it returns a tuple in the form `(str, Iterable[discord.Member])` + # If it doesn't, it returns a tuple in the form `(str, Iterable[disnake.Member])` # which contains the reason for why the message violated the rule and # an iterable of all members that violated the rule. if result is not None: @@ -265,7 +265,7 @@ class AntiSpam(Cog): # In the rare case where we found messages matching the # spam filter across multiple channels, it is possible # that a single channel will only contain a single message - # to delete. If that should be the case, discord.py will + # to delete. If that should be the case, disnake will # use the "delete single message" endpoint instead of the # bulk delete endpoint, and the single message deletion # endpoint will complain if you give it that does not exist. diff --git a/bot/exts/filters/filter_lists.py b/bot/exts/filters/filter_lists.py index a883ddf54..05910973a 100644 --- a/bot/exts/filters/filter_lists.py +++ b/bot/exts/filters/filter_lists.py @@ -1,8 +1,8 @@ import re from typing import Optional -from discord import Colour, Embed -from discord.ext.commands import BadArgument, Cog, Context, IDConverter, group, has_any_role +from disnake import Colour, Embed +from disnake.ext.commands import BadArgument, Cog, Context, IDConverter, group, has_any_role from bot import constants from bot.api import ResponseCodeError diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index f44b28125..e8c9bab62 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -6,15 +6,15 @@ from typing import Any, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union import arrow import dateutil.parser -import discord.errors +import disnake.errors import regex import tldextract from async_rediscache import RedisCache from botcore.regex import DISCORD_INVITE from dateutil.relativedelta import relativedelta -from discord import Colour, HTTPException, Member, Message, NotFound, TextChannel -from discord.ext.commands import Cog -from discord.utils import escape_markdown +from disnake import Colour, HTTPException, Member, Message, NotFound, TextChannel +from disnake.ext.commands import Cog +from disnake.utils import escape_markdown from bot.api import ResponseCodeError from bot.bot import Bot @@ -63,14 +63,14 @@ AUTO_BAN_REASON = ( ) AUTO_BAN_DURATION = timedelta(days=4) -FilterMatch = Union[re.Match, dict, bool, List[discord.Embed]] +FilterMatch = Union[re.Match, dict, bool, List[disnake.Embed]] class Stats(NamedTuple): """Additional stats on a triggered filter to append to a mod log.""" message_content: str - additional_embeds: Optional[List[discord.Embed]] + additional_embeds: Optional[List[disnake.Embed]] class Filtering(Cog): @@ -339,7 +339,7 @@ class Filtering(Cog): match = result if match: - is_private = msg.channel.type is discord.ChannelType.private + is_private = msg.channel.type is disnake.ChannelType.private # If this is a filter (not a watchlist) and not in a DM, delete the message. if _filter["type"] == "filter" and not is_private: @@ -354,7 +354,7 @@ class Filtering(Cog): # In addition, to avoid sending two notifications to the user, the # logs, and mod_alert, we return if the message no longer exists. await msg.delete() - except discord.errors.NotFound: + except disnake.errors.NotFound: return # Notify the user if the filter specifies @@ -409,14 +409,14 @@ class Filtering(Cog): self, filter_name: str, _filter: Dict[str, Any], - msg: discord.Message, + msg: disnake.Message, stats: Stats, reason: Optional[str] = None, *, is_eval: bool = False, ) -> None: """Send a mod log for a triggered filter.""" - if msg.channel.type is discord.ChannelType.private: + if msg.channel.type is disnake.ChannelType.private: channel_str = "via DM" ping_everyone = False else: @@ -478,7 +478,7 @@ class Filtering(Cog): additional_embeds = [] for _, data in match.items(): reason = f"Reason: {data['reason']} | " if data.get('reason') else "" - embed = discord.Embed(description=( + embed = disnake.Embed(description=( f"**Members:**\n{data['members']}\n" f"**Active:**\n{data['active']}" )) @@ -626,7 +626,7 @@ class Filtering(Cog): return invite_data if invite_data else False @staticmethod - async def _has_rich_embed(msg: Message) -> Union[bool, List[discord.Embed]]: + async def _has_rich_embed(msg: Message) -> Union[bool, List[disnake.Embed]]: """Determines if `msg` contains any rich embeds not auto-generated from a URL.""" if msg.embeds: for embed in msg.embeds: @@ -662,7 +662,7 @@ class Filtering(Cog): """ try: await filtered_member.send(reason) - except discord.errors.Forbidden: + except disnake.errors.Forbidden: await channel.send(f"{filtered_member.mention} {reason}") def schedule_msg_delete(self, msg: dict) -> None: diff --git a/bot/exts/filters/security.py b/bot/exts/filters/security.py index fe3918423..bbb15542f 100644 --- a/bot/exts/filters/security.py +++ b/bot/exts/filters/security.py @@ -1,4 +1,4 @@ -from discord.ext.commands import Cog, Context, NoPrivateMessage +from disnake.ext.commands import Cog, Context, NoPrivateMessage from bot.bot import Bot from bot.log import get_logger diff --git a/bot/exts/filters/token_remover.py b/bot/exts/filters/token_remover.py index 520283ba3..da42bb0aa 100644 --- a/bot/exts/filters/token_remover.py +++ b/bot/exts/filters/token_remover.py @@ -3,8 +3,8 @@ import binascii import re import typing as t -from discord import Colour, Message, NotFound -from discord.ext.commands import Cog +from disnake import Colour, Message, NotFound +from disnake.ext.commands import Cog from bot import utils from bot.bot import Bot @@ -53,7 +53,7 @@ class Token(t.NamedTuple): class TokenRemover(Cog): - """Scans messages for potential discord.py bot tokens and removes them.""" + """Scans messages for potential Discord bot tokens and removes them.""" def __init__(self, bot: Bot): self.bot = bot diff --git a/bot/exts/filters/webhook_remover.py b/bot/exts/filters/webhook_remover.py index 96334317c..a5d51700c 100644 --- a/bot/exts/filters/webhook_remover.py +++ b/bot/exts/filters/webhook_remover.py @@ -1,7 +1,7 @@ import re -from discord import Colour, Message, NotFound -from discord.ext.commands import Cog +from disnake import Colour, Message, NotFound +from disnake.ext.commands import Cog from bot.bot import Bot from bot.constants import Channels, Colours, Event, Icons diff --git a/bot/exts/fun/duck_pond.py b/bot/exts/fun/duck_pond.py index c51656343..55196cd65 100644 --- a/bot/exts/fun/duck_pond.py +++ b/bot/exts/fun/duck_pond.py @@ -1,9 +1,9 @@ import asyncio from typing import Union -import discord -from discord import Color, Embed, Message, RawReactionActionEvent, TextChannel, errors -from discord.ext.commands import Cog, Context, command +import disnake +from disnake import Color, Embed, Message, RawReactionActionEvent, TextChannel, errors +from disnake.ext.commands import Cog, Context, command from bot import constants from bot.bot import Bot @@ -34,7 +34,7 @@ class DuckPond(Cog): try: self.webhook = await self.bot.fetch_webhook(self.webhook_id) - except discord.HTTPException: + except disnake.HTTPException: log.exception(f"Failed to fetch webhook with id `{self.webhook_id}`") @staticmethod @@ -67,7 +67,7 @@ class DuckPond(Cog): return False @staticmethod - def _is_duck_emoji(emoji: Union[str, discord.PartialEmoji, discord.Emoji]) -> bool: + def _is_duck_emoji(emoji: Union[str, disnake.PartialEmoji, disnake.Emoji]) -> bool: """Check if the emoji is a valid duck emoji.""" if isinstance(emoji, str): return emoji == "🦆" @@ -111,7 +111,7 @@ class DuckPond(Cog): username=message.author.display_name, avatar_url=message.author.display_avatar.url ) - except discord.HTTPException: + except disnake.HTTPException: log.exception("Failed to send an attachment to the webhook") async def locked_relay(self, message: Message) -> bool: @@ -133,7 +133,7 @@ class DuckPond(Cog): await message.add_reaction("✅") return True - def _payload_has_duckpond_emoji(self, emoji: discord.PartialEmoji) -> bool: + def _payload_has_duckpond_emoji(self, emoji: disnake.PartialEmoji) -> bool: """Test if the RawReactionActionEvent payload contains a duckpond emoji.""" if emoji.is_unicode_emoji(): # For unicode PartialEmojis, the `name` attribute is just the string @@ -165,7 +165,7 @@ class DuckPond(Cog): if not self._payload_has_duckpond_emoji(payload.emoji): return - channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id) + channel = disnake.utils.get(self.bot.get_all_channels(), id=payload.channel_id) if channel is None: return @@ -175,10 +175,10 @@ class DuckPond(Cog): try: message = await channel.fetch_message(payload.message_id) - except discord.NotFound: + except disnake.NotFound: return # Message was deleted. - member = discord.utils.get(message.guild.members, id=payload.user_id) + member = disnake.utils.get(message.guild.members, id=payload.user_id) if not member: return # Member left or wasn't in the cache. @@ -205,7 +205,7 @@ class DuckPond(Cog): if payload.guild_id != constants.Guild.id: return - channel = discord.utils.get(self.bot.get_all_channels(), id=payload.channel_id) + channel = disnake.utils.get(self.bot.get_all_channels(), id=payload.channel_id) if channel is None: return diff --git a/bot/exts/fun/off_topic_names.py b/bot/exts/fun/off_topic_names.py index 7df1d172d..d49f71320 100644 --- a/bot/exts/fun/off_topic_names.py +++ b/bot/exts/fun/off_topic_names.py @@ -2,9 +2,9 @@ import difflib from datetime import timedelta import arrow -from discord import Colour, Embed -from discord.ext.commands import Cog, Context, group, has_any_role -from discord.utils import sleep_until +from disnake import Colour, Embed +from disnake.ext.commands import Cog, Context, group, has_any_role +from disnake.utils import sleep_until from bot.api import ResponseCodeError from bot.bot import Bot diff --git a/bot/exts/help_channels/_caches.py b/bot/exts/help_channels/_caches.py index 8d45c2466..f4eaf3291 100644 --- a/bot/exts/help_channels/_caches.py +++ b/bot/exts/help_channels/_caches.py @@ -1,24 +1,24 @@ from async_rediscache import RedisCache # This dictionary maps a help channel to the time it was claimed -# RedisCache[discord.TextChannel.id, UtcPosixTimestamp] +# RedisCache[disnake.TextChannel.id, UtcPosixTimestamp] claim_times = RedisCache(namespace="HelpChannels.claim_times") # This cache tracks which channels are claimed by which members. -# RedisCache[discord.TextChannel.id, t.Union[discord.User.id, discord.Member.id]] +# RedisCache[disnake.TextChannel.id, t.Union[disnake.User.id, disnake.Member.id]] claimants = RedisCache(namespace="HelpChannels.help_channel_claimants") # Stores the timestamp of the last message from the claimant of a help channel -# RedisCache[discord.TextChannel.id, UtcPosixTimestamp] +# RedisCache[disnake.TextChannel.id, UtcPosixTimestamp] claimant_last_message_times = RedisCache(namespace="HelpChannels.claimant_last_message_times") # This cache maps a help channel to the timestamp of the last non-claimant message. # This cache being empty for a given help channel indicates the question is unanswered. -# RedisCache[discord.TextChannel.id, UtcPosixTimestamp] +# RedisCache[disnake.TextChannel.id, UtcPosixTimestamp] non_claimant_last_message_times = RedisCache(namespace="HelpChannels.non_claimant_last_message_times") # This cache maps a help channel to original question message in same channel. -# RedisCache[discord.TextChannel.id, discord.Message.id] +# RedisCache[disnake.TextChannel.id, disnake.Message.id] question_messages = RedisCache(namespace="HelpChannels.question_messages") # This cache keeps track of the dynamic message ID for @@ -26,10 +26,10 @@ question_messages = RedisCache(namespace="HelpChannels.question_messages") dynamic_message = RedisCache(namespace="HelpChannels.dynamic_message") # This cache keeps track of who has help-dms on. -# RedisCache[discord.User.id, bool] +# RedisCache[disnake.User.id, bool] help_dm = RedisCache(namespace="HelpChannels.help_dm") # This cache tracks member who are participating and opted in to help channel dms. # serialise the set as a comma separated string to allow usage with redis -# RedisCache[discord.TextChannel.id, str[set[discord.User.id]]] +# RedisCache[disnake.TextChannel.id, str[set[disnake.User.id]]] session_participants = RedisCache(namespace="HelpChannels.session_participants") diff --git a/bot/exts/help_channels/_channel.py b/bot/exts/help_channels/_channel.py index d9cebf215..3c4eaa2b2 100644 --- a/bot/exts/help_channels/_channel.py +++ b/bot/exts/help_channels/_channel.py @@ -4,7 +4,7 @@ from datetime import timedelta from enum import Enum import arrow -import discord +import disnake from arrow import Arrow import bot @@ -31,7 +31,7 @@ class ClosingReason(Enum): CLEANUP = "auto.cleanup" -def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[discord.TextChannel]: +def get_category_channels(category: disnake.CategoryChannel) -> t.Iterable[disnake.TextChannel]: """Yield the text channels of the `category` in an unsorted manner.""" log.trace(f"Getting text channels in the category '{category}' ({category.id}).") @@ -41,7 +41,7 @@ def get_category_channels(category: discord.CategoryChannel) -> t.Iterable[disco yield channel -async def get_closing_time(channel: discord.TextChannel, init_done: bool) -> t.Tuple[Arrow, ClosingReason]: +async def get_closing_time(channel: disnake.TextChannel, init_done: bool) -> t.Tuple[Arrow, ClosingReason]: """ Return the time at which the given help `channel` should be closed along with the reason. @@ -116,12 +116,12 @@ async def get_in_use_time(channel_id: int) -> t.Optional[timedelta]: return arrow.utcnow() - claimed -def is_excluded_channel(channel: discord.abc.GuildChannel) -> bool: +def is_excluded_channel(channel: disnake.abc.GuildChannel) -> bool: """Check if a channel should be excluded from the help channel system.""" - return not isinstance(channel, discord.TextChannel) or channel.id in EXCLUDED_CHANNELS + return not isinstance(channel, disnake.TextChannel) or channel.id in EXCLUDED_CHANNELS -async def move_to_bottom(channel: discord.TextChannel, category_id: int, **options) -> None: +async def move_to_bottom(channel: disnake.TextChannel, category_id: int, **options) -> None: """ Move the `channel` to the bottom position of `category` and edit channel attributes. @@ -130,8 +130,8 @@ async def move_to_bottom(channel: discord.TextChannel, category_id: int, **optio really ends up at the bottom of the category. If `options` are provided, the channel will be edited after the move is completed. This is the - same order of operations that `discord.TextChannel.edit` uses. For information on available - options, see the documentation on `discord.TextChannel.edit`. While possible, position-related + same order of operations that `disnake.TextChannel.edit` uses. For information on available + options, see the documentation on `disnake.TextChannel.edit`. While possible, position-related options should be avoided, as it may interfere with the category move we perform. """ # Get a fresh copy of the category from the bot to avoid the cache mismatch issue we had. @@ -161,7 +161,7 @@ async def move_to_bottom(channel: discord.TextChannel, category_id: int, **optio await channel.edit(**options) -async def ensure_cached_claimant(channel: discord.TextChannel) -> None: +async def ensure_cached_claimant(channel: disnake.TextChannel) -> None: """ Ensure there is a claimant cached for each help channel. diff --git a/bot/exts/help_channels/_cog.py b/bot/exts/help_channels/_cog.py index d3d70e252..fc55fa1df 100644 --- a/bot/exts/help_channels/_cog.py +++ b/bot/exts/help_channels/_cog.py @@ -5,9 +5,9 @@ from datetime import timedelta from operator import attrgetter import arrow -import discord -import discord.abc -from discord.ext import commands +import disnake +import disnake.abc +from disnake.ext import commands from bot import constants from bot.bot import Bot @@ -66,16 +66,16 @@ class HelpChannels(commands.Cog): self.bot = bot self.scheduler = scheduling.Scheduler(self.__class__.__name__) - self.guild: discord.Guild = None - self.cooldown_role: discord.Role = None + self.guild: disnake.Guild = None + self.cooldown_role: disnake.Role = None # Categories - self.available_category: discord.CategoryChannel = None - self.in_use_category: discord.CategoryChannel = None - self.dormant_category: discord.CategoryChannel = None + self.available_category: disnake.CategoryChannel = None + self.in_use_category: disnake.CategoryChannel = None + self.dormant_category: disnake.CategoryChannel = None # Queues - self.channel_queue: asyncio.Queue[discord.TextChannel] = None + self.channel_queue: asyncio.Queue[disnake.TextChannel] = None self.name_queue: t.Deque[str] = None # Notifications @@ -84,7 +84,7 @@ class HelpChannels(commands.Cog): self.last_running_low_notification = arrow.get('1815-12-10T18:00:00.00000+00:00') self.dynamic_message: t.Optional[int] = None - self.available_help_channels: t.Set[discord.TextChannel] = set() + self.available_help_channels: t.Set[disnake.TextChannel] = set() # Asyncio stuff self.queue_tasks: t.List[asyncio.Task] = [] @@ -104,7 +104,7 @@ class HelpChannels(commands.Cog): @lock.lock_arg(NAMESPACE, "message", attrgetter("channel.id")) @lock.lock_arg(NAMESPACE, "message", attrgetter("author.id")) @lock.lock_arg(f"{NAMESPACE}.unclaim", "message", attrgetter("author.id"), wait=True) - async def claim_channel(self, message: discord.Message) -> None: + async def claim_channel(self, message: disnake.Message) -> None: """ Claim the channel in which the question `message` was sent. @@ -116,7 +116,7 @@ class HelpChannels(commands.Cog): try: await self.move_to_in_use(message.channel) - except discord.DiscordServerError: + except disnake.DiscordServerError: try: await message.channel.send( "The bot encountered a Discord API error while trying to move this channel, please try again later." @@ -133,14 +133,14 @@ class HelpChannels(commands.Cog): self.bot.stats.incr("help.failed_claims.500_on_move") return - embed = discord.Embed( + embed = disnake.Embed( description=f"Channel claimed by {message.author.mention}.", color=constants.Colours.bright_green, ) await message.channel.send(embed=embed) - # Handle odd edge case of `message.author` not being a `discord.Member` (see bot#1839) - if not isinstance(message.author, discord.Member): + # Handle odd edge case of `message.author` not being a `disnake.Member` (see bot#1839) + if not isinstance(message.author, disnake.Member): log.debug(f"{message.author} ({message.author.id}) isn't a member. Not giving cooldown role or sending DM.") else: await members.handle_role_change(message.author, message.author.add_roles, self.cooldown_role) @@ -189,7 +189,7 @@ class HelpChannels(commands.Cog): return queue - async def create_dormant(self) -> t.Optional[discord.TextChannel]: + async def create_dormant(self) -> t.Optional[disnake.TextChannel]: """ Create and return a new channel in the Dormant category. @@ -234,12 +234,12 @@ class HelpChannels(commands.Cog): May only be invoked by the channel's claimant or by staff. """ - # Don't use a discord.py check because the check needs to fail silently. + # Don't use a disnake check because the check needs to fail silently. if await self.close_check(ctx): log.info(f"Close command invoked by {ctx.author} in #{ctx.channel}.") await self.unclaim_channel(ctx.channel, closed_on=_channel.ClosingReason.COMMAND) - async def get_available_candidate(self) -> discord.TextChannel: + async def get_available_candidate(self) -> disnake.TextChannel: """ Return a dormant channel to turn into an available channel. @@ -313,7 +313,7 @@ class HelpChannels(commands.Cog): self.dormant_category = await channel_utils.get_or_fetch_channel( constants.Categories.help_dormant ) - except discord.HTTPException: + except disnake.HTTPException: log.exception("Failed to get a category; cog will be removed") self.bot.remove_cog(self.qualified_name) @@ -355,7 +355,7 @@ class HelpChannels(commands.Cog): log.info("Cog is ready!") - async def move_idle_channel(self, channel: discord.TextChannel, has_task: bool = True) -> None: + async def move_idle_channel(self, channel: disnake.TextChannel, has_task: bool = True) -> None: """ Make the `channel` dormant if idle or schedule the move if still active. @@ -416,7 +416,7 @@ class HelpChannels(commands.Cog): _stats.report_counts() - async def move_to_dormant(self, channel: discord.TextChannel) -> None: + async def move_to_dormant(self, channel: disnake.TextChannel) -> None: """Make the `channel` dormant.""" log.info(f"Moving #{channel} ({channel.id}) to the Dormant category.") await _channel.move_to_bottom( @@ -425,7 +425,7 @@ class HelpChannels(commands.Cog): ) log.trace(f"Sending dormant message for #{channel} ({channel.id}).") - embed = discord.Embed( + embed = disnake.Embed( description=_message.DORMANT_MSG.format( dormant=self.dormant_category.name, available=self.available_category.name, @@ -439,7 +439,7 @@ class HelpChannels(commands.Cog): _stats.report_counts() @lock.lock_arg(f"{NAMESPACE}.unclaim", "channel") - async def unclaim_channel(self, channel: discord.TextChannel, *, closed_on: _channel.ClosingReason) -> None: + async def unclaim_channel(self, channel: disnake.TextChannel, *, closed_on: _channel.ClosingReason) -> None: """ Unclaim an in-use help `channel` to make it dormant. @@ -462,7 +462,7 @@ class HelpChannels(commands.Cog): async def _unclaim_channel( self, - channel: discord.TextChannel, + channel: disnake.TextChannel, claimant_id: t.Optional[int], closed_on: _channel.ClosingReason ) -> None: @@ -488,7 +488,7 @@ class HelpChannels(commands.Cog): if closed_on == _channel.ClosingReason.COMMAND: self.scheduler.cancel(channel.id) - async def move_to_in_use(self, channel: discord.TextChannel) -> None: + async def move_to_in_use(self, channel: disnake.TextChannel) -> None: """Make a channel in-use and schedule it to be made dormant.""" log.info(f"Moving #{channel} ({channel.id}) to the In Use category.") @@ -504,7 +504,7 @@ class HelpChannels(commands.Cog): _stats.report_counts() @commands.Cog.listener() - async def on_message(self, message: discord.Message) -> None: + async def on_message(self, message: disnake.Message) -> None: """Move an available channel to the In Use category and replace it with a dormant one.""" if message.author.bot: return # Ignore messages sent by bots. @@ -520,7 +520,7 @@ class HelpChannels(commands.Cog): await _message.update_message_caches(message) @commands.Cog.listener() - async def on_message_delete(self, msg: discord.Message) -> None: + async def on_message_delete(self, msg: disnake.Message) -> None: """ Reschedule an in-use channel to become dormant sooner if the channel is empty. @@ -542,7 +542,7 @@ class HelpChannels(commands.Cog): delay = constants.HelpChannels.deleted_idle_minutes * 60 self.scheduler.schedule_later(delay, msg.channel.id, self.move_idle_channel(msg.channel)) - async def wait_for_dormant_channel(self) -> discord.TextChannel: + async def wait_for_dormant_channel(self) -> disnake.TextChannel: """Wait for a dormant channel to become available in the queue and return it.""" log.trace("Waiting for a dormant channel.") @@ -569,7 +569,7 @@ class HelpChannels(commands.Cog): await self.bot.http.edit_message( constants.Channels.how_to_get_help, self.dynamic_message, content=available_channels, files=None ) - except discord.NotFound: + except disnake.NotFound: pass else: return @@ -593,7 +593,7 @@ class HelpChannels(commands.Cog): @lock.lock_arg(NAMESPACE, "message", attrgetter("channel.id")) @lock.lock_arg(NAMESPACE, "message", attrgetter("author.id")) - async def notify_session_participants(self, message: discord.Message) -> None: + async def notify_session_participants(self, message: disnake.Message) -> None: """ Check if the message author meets the requirements to be notified. @@ -615,7 +615,7 @@ class HelpChannels(commands.Cog): if message.author.id not in session_participants: session_participants.add(message.author.id) - embed = discord.Embed( + embed = disnake.Embed( title="Currently Helping", description=f"You're currently helping in {message.channel.mention}", color=constants.Colours.bright_green, @@ -625,7 +625,7 @@ class HelpChannels(commands.Cog): try: await message.author.send(embed=embed) - except discord.Forbidden: + except disnake.Forbidden: log.trace( f"Failed to send helpdm message to {message.author.id}. DMs Closed/Blocked. " "Removing user from helpdm." diff --git a/bot/exts/help_channels/_message.py b/bot/exts/help_channels/_message.py index 7ceed9b4d..e08043694 100644 --- a/bot/exts/help_channels/_message.py +++ b/bot/exts/help_channels/_message.py @@ -2,7 +2,7 @@ import textwrap import typing as t import arrow -import discord +import disnake from arrow import Arrow import bot @@ -41,7 +41,7 @@ through our guide for **[asking a good question]({ASKING_GUIDE_URL})**. """ -async def update_message_caches(message: discord.Message) -> None: +async def update_message_caches(message: disnake.Message) -> None: """Checks the source of new content in a help channel and updates the appropriate cache.""" channel = message.channel @@ -62,18 +62,18 @@ async def update_message_caches(message: discord.Message) -> None: await _caches.non_claimant_last_message_times.set(channel.id, timestamp) -async def get_last_message(channel: discord.TextChannel) -> t.Optional[discord.Message]: +async def get_last_message(channel: disnake.TextChannel) -> t.Optional[disnake.Message]: """Return the last message sent in the channel or None if no messages exist.""" log.trace(f"Getting the last message in #{channel} ({channel.id}).") try: return await channel.history(limit=1).next() # noqa: B305 - except discord.NoMoreItems: + except disnake.NoMoreItems: log.debug(f"No last message available; #{channel} ({channel.id}) has no messages.") return None -async def is_empty(channel: discord.TextChannel) -> bool: +async def is_empty(channel: disnake.TextChannel) -> bool: """Return True if there's an AVAILABLE_MSG and the messages leading up are bot messages.""" log.trace(f"Checking if #{channel} ({channel.id}) is empty.") @@ -92,13 +92,13 @@ async def is_empty(channel: discord.TextChannel) -> bool: return False -async def dm_on_open(message: discord.Message) -> None: +async def dm_on_open(message: disnake.Message) -> None: """ DM claimant with a link to the claimed channel's first message, with a 100 letter preview of the message. Does nothing if the user has DMs disabled. """ - embed = discord.Embed( + embed = disnake.Embed( title="Help channel opened", description=f"You claimed {message.channel.mention}.", colour=bot.constants.Colours.bright_green, @@ -118,7 +118,7 @@ async def dm_on_open(message: discord.Message) -> None: try: await message.author.send(embed=embed) log.trace(f"Sent DM to {message.author.id} after claiming help channel.") - except discord.errors.Forbidden: + except disnake.errors.Forbidden: log.trace( f"Ignoring to send DM to {message.author.id} after claiming help channel: DMs disabled." ) @@ -146,7 +146,7 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: log.trace("Notifying about lack of channels.") mentions = " ".join(f"<@&{role}>" for role in constants.HelpChannels.notify_none_remaining_roles) - allowed_roles = [discord.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] + allowed_roles = [disnake.Object(id_) for id_ in constants.HelpChannels.notify_none_remaining_roles] channel = bot.instance.get_channel(constants.HelpChannels.notify_channel) if channel is None: @@ -157,7 +157,7 @@ async def notify_none_remaining(last_notification: Arrow) -> t.Optional[Arrow]: f"{mentions} A new available help channel is needed but there " "are no more dormant ones. Consider freeing up some in-use channels manually by " f"using the `{constants.Bot.prefix}dormant` command within the channels.", - allowed_mentions=discord.AllowedMentions(everyone=False, roles=allowed_roles) + allowed_mentions=disnake.AllowedMentions(everyone=False, roles=allowed_roles) ) except Exception: # Handle it here cause this feature isn't critical for the functionality of the system. @@ -213,18 +213,18 @@ async def notify_running_low(number_of_channels_left: int, last_notification: Ar return arrow.utcnow() -async def pin(message: discord.Message) -> None: +async def pin(message: disnake.Message) -> None: """Pin an initial question `message` and store it in a cache.""" if await pin_wrapper(message.id, message.channel, pin=True): await _caches.question_messages.set(message.channel.id, message.id) -async def send_available_message(channel: discord.TextChannel) -> None: +async def send_available_message(channel: disnake.TextChannel) -> None: """Send the available message by editing a dormant message or sending a new message.""" channel_info = f"#{channel} ({channel.id})" log.trace(f"Sending available message in {channel_info}.") - embed = discord.Embed( + embed = disnake.Embed( color=constants.Colours.bright_green, description=AVAILABLE_MSG, ) @@ -240,7 +240,7 @@ async def send_available_message(channel: discord.TextChannel) -> None: await channel.send(embed=embed) -async def unpin(channel: discord.TextChannel) -> None: +async def unpin(channel: disnake.TextChannel) -> None: """Unpin the initial question message sent in `channel`.""" msg_id = await _caches.question_messages.pop(channel.id) if msg_id is None: @@ -249,19 +249,19 @@ async def unpin(channel: discord.TextChannel) -> None: await pin_wrapper(msg_id, channel, pin=False) -def _match_bot_embed(message: t.Optional[discord.Message], description: str) -> bool: +def _match_bot_embed(message: t.Optional[disnake.Message], description: str) -> bool: """Return `True` if the bot's `message`'s embed description matches `description`.""" if not message or not message.embeds: return False bot_msg_desc = message.embeds[0].description - if bot_msg_desc is discord.Embed.Empty: + if bot_msg_desc is disnake.Embed.Empty: log.trace("Last message was a bot embed but it was empty.") return False return message.author == bot.instance.user and bot_msg_desc.strip() == description.strip() -async def pin_wrapper(msg_id: int, channel: discord.TextChannel, *, pin: bool) -> bool: +async def pin_wrapper(msg_id: int, channel: disnake.TextChannel, *, pin: bool) -> bool: """ Pin message `msg_id` in `channel` if `pin` is True or unpin if it's False. @@ -277,7 +277,7 @@ async def pin_wrapper(msg_id: int, channel: discord.TextChannel, *, pin: bool) - try: await func(channel.id, msg_id) - except discord.HTTPException as e: + except disnake.HTTPException as e: if e.code == 10008: log.debug(f"Message {msg_id} in {channel_str} doesn't exist; can't {verb}.") else: diff --git a/bot/exts/help_channels/_name.py b/bot/exts/help_channels/_name.py index a9d9b2df1..50b250cb5 100644 --- a/bot/exts/help_channels/_name.py +++ b/bot/exts/help_channels/_name.py @@ -3,7 +3,7 @@ import typing as t from collections import deque from pathlib import Path -import discord +import disnake from bot import constants from bot.exts.help_channels._channel import MAX_CHANNELS_PER_CATEGORY, get_category_channels @@ -12,7 +12,7 @@ from bot.log import get_logger log = get_logger(__name__) -def create_name_queue(*categories: discord.CategoryChannel) -> deque: +def create_name_queue(*categories: disnake.CategoryChannel) -> deque: """ Return a queue of food names to use for creating new channels. @@ -50,7 +50,7 @@ def _get_names() -> t.List[str]: return all_names[:count] -def _get_used_names(*categories: discord.CategoryChannel) -> t.Set[str]: +def _get_used_names(*categories: disnake.CategoryChannel) -> t.Set[str]: """Return names which are already being used by channels in `categories`.""" log.trace("Getting channel names which are already being used.") diff --git a/bot/exts/info/code_snippets.py b/bot/exts/info/code_snippets.py index f2f29020f..68eb52a59 100644 --- a/bot/exts/info/code_snippets.py +++ b/bot/exts/info/code_snippets.py @@ -4,9 +4,9 @@ import textwrap from typing import Any from urllib.parse import quote_plus -import discord +import disnake from aiohttp import ClientResponseError -from discord.ext.commands import Cog +from disnake.ext.commands import Cog from bot.bot import Bot from bot.constants import Channels @@ -241,7 +241,7 @@ class CodeSnippets(Cog): return '\n'.join(map(lambda x: x[1], sorted(all_snippets))) @Cog.listener() - async def on_message(self, message: discord.Message) -> None: + async def on_message(self, message: disnake.Message) -> None: """Checks if the message has a snippet link, removes the embed, then sends the snippet contents.""" if message.author.bot: return @@ -255,7 +255,7 @@ class CodeSnippets(Cog): if 0 < len(message_to_send) <= 2000 and message_to_send.count('\n') <= 15: try: await message.edit(suppress=True) - except discord.NotFound: + except disnake.NotFound: # Don't send snippets if the original message was deleted. return diff --git a/bot/exts/info/codeblock/_cog.py b/bot/exts/info/codeblock/_cog.py index a859d8cef..cf8c7d0be 100644 --- a/bot/exts/info/codeblock/_cog.py +++ b/bot/exts/info/codeblock/_cog.py @@ -1,9 +1,9 @@ import time from typing import Optional -import discord -from discord import Message, RawMessageUpdateEvent -from discord.ext.commands import Cog +import disnake +from disnake import Message, RawMessageUpdateEvent +from disnake.ext.commands import Cog from bot import constants from bot.bot import Bot @@ -62,9 +62,9 @@ class CodeBlockCog(Cog, name="Code Block"): self.codeblock_message_ids = {} @staticmethod - def create_embed(instructions: str) -> discord.Embed: + def create_embed(instructions: str) -> disnake.Embed: """Return an embed which displays code block formatting `instructions`.""" - return discord.Embed(description=instructions) + return disnake.Embed(description=instructions) async def get_sent_instructions(self, payload: RawMessageUpdateEvent) -> Optional[Message]: """ @@ -78,11 +78,11 @@ class CodeBlockCog(Cog, name="Code Block"): try: return await channel.fetch_message(self.codeblock_message_ids[payload.message_id]) - except discord.NotFound: + except disnake.NotFound: log.debug("Could not find instructions message; it was probably deleted.") return None - def is_on_cooldown(self, channel: discord.TextChannel) -> bool: + def is_on_cooldown(self, channel: disnake.TextChannel) -> bool: """ Return True if an embed was sent too recently for `channel`. @@ -93,7 +93,7 @@ class CodeBlockCog(Cog, name="Code Block"): cooldown = constants.CodeBlock.cooldown_seconds return (time.time() - self.channel_cooldowns.get(channel.id, 0)) < cooldown - def is_valid_channel(self, channel: discord.TextChannel) -> bool: + def is_valid_channel(self, channel: disnake.TextChannel) -> bool: """Return True if `channel` is a help channel, may be on a cooldown, or is whitelisted.""" log.trace(f"Checking if #{channel} qualifies for code block detection.") return ( @@ -102,7 +102,7 @@ class CodeBlockCog(Cog, name="Code Block"): or channel.id in constants.CodeBlock.channel_whitelist ) - async def send_instructions(self, message: discord.Message, instructions: str) -> None: + async def send_instructions(self, message: disnake.Message, instructions: str) -> None: """ Send an embed with `instructions` on fixing an incorrect code block in a `message`. @@ -119,7 +119,7 @@ class CodeBlockCog(Cog, name="Code Block"): # Increase amount of codeblock correction in stats self.bot.stats.incr("codeblock_corrections") - def should_parse(self, message: discord.Message) -> bool: + def should_parse(self, message: disnake.Message) -> bool: """ Return True if `message` should be parsed. @@ -185,5 +185,5 @@ class CodeBlockCog(Cog, name="Code Block"): else: log.info("Message edited but still has invalid code blocks; editing instructions.") await bot_message.edit(embed=self.create_embed(instructions)) - except discord.NotFound: + except disnake.NotFound: log.debug("Could not find instructions message; it was probably deleted.") diff --git a/bot/exts/info/doc/_batch_parser.py b/bot/exts/info/doc/_batch_parser.py index c27f28eac..487a0fd21 100644 --- a/bot/exts/info/doc/_batch_parser.py +++ b/bot/exts/info/doc/_batch_parser.py @@ -7,7 +7,7 @@ from contextlib import suppress from operator import attrgetter from typing import Deque, Dict, List, NamedTuple, Optional, Union -import discord +import disnake from bs4 import BeautifulSoup import bot @@ -48,7 +48,7 @@ class StaleInventoryNotifier: if await self.symbol_counter.increment_for(doc_item) < 3: self._warned_urls.add(doc_item.url) await self._init_task - embed = discord.Embed( + embed = disnake.Embed( description=f"Doc item `{doc_item.symbol_id=}` present in loaded documentation inventories " f"not found on [site]({doc_item.url}), inventories may need to be refreshed." ) diff --git a/bot/exts/info/doc/_cog.py b/bot/exts/info/doc/_cog.py index 4dc5276d9..77fc61389 100644 --- a/bot/exts/info/doc/_cog.py +++ b/bot/exts/info/doc/_cog.py @@ -9,8 +9,8 @@ from types import SimpleNamespace from typing import Dict, NamedTuple, Optional, Tuple, Union import aiohttp -import discord -from discord.ext import commands +import disnake +from disnake.ext import commands from bot.api import ResponseCodeError from bot.bot import Bot @@ -275,7 +275,7 @@ class DocCog(commands.Cog): return "Unable to parse the requested symbol." return markdown - async def create_symbol_embed(self, symbol_name: str) -> Optional[discord.Embed]: + async def create_symbol_embed(self, symbol_name: str) -> Optional[disnake.Embed]: """ Attempt to scrape and fetch the data for the given `symbol_name`, and build an embed from its contents. @@ -304,8 +304,8 @@ class DocCog(commands.Cog): else: footer_text = "" - embed = discord.Embed( - title=discord.utils.escape_markdown(symbol_name), + embed = disnake.Embed( + title=disnake.utils.escape_markdown(symbol_name), url=f"{doc_item.url}#{doc_item.symbol_id}", description=await self.get_symbol_markdown(doc_item) ) @@ -331,9 +331,9 @@ class DocCog(commands.Cog): !docs getdoc aiohttp.ClientSession """ if not symbol_name: - inventory_embed = discord.Embed( + inventory_embed = disnake.Embed( title=f"All inventories (`{len(self.base_urls)}` total)", - colour=discord.Colour.blue() + colour=disnake.Colour.blue() ) lines = sorted(f"• [`{name}`]({url})" for name, url in self.base_urls.items()) @@ -355,7 +355,7 @@ class DocCog(commands.Cog): # Make sure that we won't cause a ghost-ping by deleting the message if not (ctx.message.mentions or ctx.message.role_mentions): - with suppress(discord.NotFound): + with suppress(disnake.NotFound): await ctx.message.delete() await error_message.delete() @@ -449,7 +449,7 @@ class DocCog(commands.Cog): if removed := ", ".join(old_inventories - new_inventories): removed = "- " + removed - embed = discord.Embed( + embed = disnake.Embed( title="Inventories refreshed", description=f"```diff\n{added}\n{removed}```" if added or removed else "" ) diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index 864e7edd2..29d73c564 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -6,8 +6,8 @@ from collections import namedtuple from contextlib import suppress from typing import List, Optional, Union -from discord import ButtonStyle, Colour, Embed, Emoji, Interaction, PartialEmoji, ui -from discord.ext.commands import Bot, Cog, Command, CommandError, Context, DisabledCommand, Group, HelpCommand +from disnake import ButtonStyle, Colour, Embed, Emoji, Interaction, PartialEmoji, ui +from disnake.ext.commands import Bot, Cog, Command, CommandError, Context, DisabledCommand, Group, HelpCommand from rapidfuzz import fuzz, process from rapidfuzz.utils import default_process diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py index e616b9208..44a9b8f1a 100644 --- a/bot/exts/info/information.py +++ b/bot/exts/info/information.py @@ -6,9 +6,9 @@ from textwrap import shorten from typing import Any, DefaultDict, Mapping, Optional, Tuple, Union import rapidfuzz -from discord import AllowedMentions, Colour, Embed, Guild, Message, Role -from discord.ext.commands import BucketType, Cog, Context, Greedy, Paginator, command, group, has_any_role -from discord.utils import escape_markdown +from disnake import AllowedMentions, Colour, Embed, Guild, Message, Role +from disnake.ext.commands import BucketType, Cog, Context, Greedy, Paginator, command, group, has_any_role +from disnake.utils import escape_markdown from bot import constants from bot.api import ResponseCodeError @@ -466,7 +466,7 @@ class Information(Cog): async def send_raw_content(self, ctx: Context, message: Message, json: bool = False) -> None: """ - Send information about the raw API response for a `discord.Message`. + Send information about the raw API response for a `disnake.Message`. If `json` is True, send the information in a copy-pasteable Python format. """ diff --git a/bot/exts/info/pep.py b/bot/exts/info/pep.py index 67866620b..08c693581 100644 --- a/bot/exts/info/pep.py +++ b/bot/exts/info/pep.py @@ -3,8 +3,8 @@ from email.parser import HeaderParser from io import StringIO from typing import Dict, Optional, Tuple -from discord import Colour, Embed -from discord.ext.commands import Cog, Context, command +from disnake import Colour, Embed +from disnake.ext.commands import Cog, Context, command from bot.bot import Bot from bot.constants import Keys diff --git a/bot/exts/info/pypi.py b/bot/exts/info/pypi.py index dacf7bc12..0a7705eb0 100644 --- a/bot/exts/info/pypi.py +++ b/bot/exts/info/pypi.py @@ -3,9 +3,9 @@ import random import re from contextlib import suppress -from discord import Embed, NotFound -from discord.ext.commands import Cog, Context, command -from discord.utils import escape_markdown +from disnake import Embed, NotFound +from disnake.ext.commands import Cog, Context, command +from disnake.utils import escape_markdown from bot.bot import Bot from bot.constants import Colours, NEGATIVE_REPLIES, RedirectOutput diff --git a/bot/exts/info/python_news.py b/bot/exts/info/python_news.py index 2fad9d2ab..7603b402b 100644 --- a/bot/exts/info/python_news.py +++ b/bot/exts/info/python_news.py @@ -2,11 +2,11 @@ import re import typing as t from datetime import date, datetime -import discord +import disnake import feedparser from bs4 import BeautifulSoup -from discord.ext.commands import Cog -from discord.ext.tasks import loop +from disnake.ext.commands import Cog +from disnake.ext.tasks import loop from bot import constants from bot.bot import Bot @@ -40,7 +40,7 @@ class PythonNews(Cog): def __init__(self, bot: Bot): self.bot = bot self.webhook_names = {} - self.webhook: t.Optional[discord.Webhook] = None + self.webhook: t.Optional[disnake.Webhook] = None scheduling.create_task(self.get_webhook_names(), event_loop=self.bot.loop) scheduling.create_task(self.get_webhook_and_channel(), event_loop=self.bot.loop) @@ -119,7 +119,7 @@ class PythonNews(Cog): continue # Build an embed and send a webhook - embed = discord.Embed( + embed = disnake.Embed( title=self.escape_markdown(new["title"]), description=self.escape_markdown(new["summary"]), timestamp=new_datetime, @@ -189,7 +189,7 @@ class PythonNews(Cog): link = THREAD_URL.format(id=thread["href"].split("/")[-2], list=maillist) # Build an embed and send a message to the webhook - embed = discord.Embed( + embed = disnake.Embed( title=self.escape_markdown(thread_information["subject"]), description=content[:1000] + f"... [continue reading]({link})" if len(content) > 1000 else content, timestamp=new_date, diff --git a/bot/exts/info/source.py b/bot/exts/info/source.py index e3e7029ca..6305a9842 100644 --- a/bot/exts/info/source.py +++ b/bot/exts/info/source.py @@ -2,8 +2,8 @@ import inspect from pathlib import Path from typing import Optional, Tuple, Union -from discord import Embed -from discord.ext import commands +from disnake import Embed +from disnake.ext import commands from bot.bot import Bot from bot.constants import URLs diff --git a/bot/exts/info/stats.py b/bot/exts/info/stats.py index 4d8bb645e..08422b38e 100644 --- a/bot/exts/info/stats.py +++ b/bot/exts/info/stats.py @@ -1,8 +1,8 @@ import string -from discord import Member, Message -from discord.ext.commands import Cog, Context -from discord.ext.tasks import loop +from disnake import Member, Message +from disnake.ext.commands import Cog, Context +from disnake.ext.tasks import loop from bot.bot import Bot from bot.constants import Categories, Channels, Guild diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index eff0c13b8..0f285e0cb 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -4,9 +4,9 @@ import typing as t from dataclasses import dataclass import arrow -import discord -from discord.ext import commands -from discord.interactions import Interaction +import disnake +from disnake.ext import commands +from disnake.interactions import Interaction from bot import constants from bot.bot import Bot @@ -58,10 +58,10 @@ DELETE_MESSAGE_AFTER = 300 # Seconds log = get_logger(__name__) -class RoleButtonView(discord.ui.View): +class RoleButtonView(disnake.ui.View): """A list of SingleRoleButtons to show to the member.""" - def __init__(self, member: discord.Member): + def __init__(self, member: disnake.Member): super().__init__() self.interaction_owner = member @@ -76,12 +76,12 @@ class RoleButtonView(discord.ui.View): return True -class SingleRoleButton(discord.ui.Button): +class SingleRoleButton(disnake.ui.Button): """A button that adds or removes a role from the member depending on it's current state.""" - ADD_STYLE = discord.ButtonStyle.success - REMOVE_STYLE = discord.ButtonStyle.red - UNAVAILABLE_STYLE = discord.ButtonStyle.secondary + ADD_STYLE = disnake.ButtonStyle.success + REMOVE_STYLE = disnake.ButtonStyle.red + UNAVAILABLE_STYLE = disnake.ButtonStyle.secondary LABEL_FORMAT = "{action} role {role_name}." CUSTOM_ID_FORMAT = "subscribe-{role_id}" @@ -104,7 +104,7 @@ class SingleRoleButton(discord.ui.Button): async def callback(self, interaction: Interaction) -> None: """Update the member's role and change button text to reflect current text.""" - if isinstance(interaction.user, discord.User): + if isinstance(interaction.user, disnake.User): log.trace("User %s is not a member", interaction.user) await interaction.message.delete() self.view.stop() @@ -117,7 +117,7 @@ class SingleRoleButton(discord.ui.Button): await members.handle_role_change( interaction.user, interaction.user.remove_roles if self.assigned else interaction.user.add_roles, - discord.Object(self.role.role_id), + disnake.Object(self.role.role_id), ) self.assigned = not self.assigned @@ -133,7 +133,7 @@ class SingleRoleButton(discord.ui.Button): self.label = self.LABEL_FORMAT.format(action="Remove" if self.assigned else "Add", role_name=self.role.name) try: await interaction.message.edit(view=self.view) - except discord.NotFound: + except disnake.NotFound: log.debug("Subscribe message for %s removed before buttons could be updated", interaction.user) self.view.stop() @@ -145,7 +145,7 @@ class Subscribe(commands.Cog): self.bot = bot self.init_task = scheduling.create_task(self.init_cog(), event_loop=self.bot.loop) self.assignable_roles: list[AssignableRole] = [] - self.guild: discord.Guild = None + self.guild: disnake.Guild = None async def init_cog(self) -> None: """Initialise the cog by resolving the role IDs in ASSIGNABLE_ROLES to role names.""" diff --git a/bot/exts/info/tags.py b/bot/exts/info/tags.py index f66237c8e..baeb21adb 100644 --- a/bot/exts/info/tags.py +++ b/bot/exts/info/tags.py @@ -6,10 +6,10 @@ import time from pathlib import Path from typing import Callable, Iterable, Literal, NamedTuple, Optional, Union -import discord +import disnake import frontmatter -from discord import Embed, Member -from discord.ext.commands import Cog, Context, group +from disnake import Embed, Member +from disnake.ext.commands import Cog, Context, group from bot import constants from bot.bot import Bot @@ -81,7 +81,7 @@ class Tag: self.content = post.content self.metadata = post.metadata self._restricted_to: set[int] = set(self.metadata.get("restricted_to", ())) - self._cooldowns: dict[discord.TextChannel, float] = {} + self._cooldowns: dict[disnake.TextChannel, float] = {} @property def embed(self) -> Embed: @@ -90,18 +90,18 @@ class Tag: embed.description = self.content return embed - def accessible_by(self, member: discord.Member) -> bool: + def accessible_by(self, member: disnake.Member) -> bool: """Check whether `member` can access the tag.""" return bool( not self._restricted_to or self._restricted_to & {role.id for role in member.roles} ) - def on_cooldown_in(self, channel: discord.TextChannel) -> bool: + def on_cooldown_in(self, channel: disnake.TextChannel) -> bool: """Check whether the tag is on cooldown in `channel`.""" return self._cooldowns.get(channel, float("-inf")) > time.time() - def set_cooldown_for(self, channel: discord.TextChannel) -> None: + def set_cooldown_for(self, channel: disnake.TextChannel) -> None: """Set the tag to be on cooldown in `channel` for `constants.Cooldowns.tags` seconds.""" self._cooldowns[channel] = time.time() + constants.Cooldowns.tags @@ -344,7 +344,7 @@ class Tags(Cog): return result_lines - def accessible_tags_in_group(self, group: str, user: discord.Member) -> list[str]: + def accessible_tags_in_group(self, group: str, user: disnake.Member) -> list[str]: """Return a formatted list of tags in `group`, that are accessible by `user`.""" return sorted( f"**\N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK}** {identifier}" diff --git a/bot/exts/moderation/clean.py b/bot/exts/moderation/clean.py index cb6836258..2e274b23b 100644 --- a/bot/exts/moderation/clean.py +++ b/bot/exts/moderation/clean.py @@ -7,10 +7,10 @@ from datetime import datetime from itertools import takewhile from typing import Callable, Iterable, Literal, Optional, TYPE_CHECKING, Union -from discord import Colour, Message, NotFound, TextChannel, User, errors -from discord.ext.commands import Cog, Context, Converter, Greedy, group, has_any_role -from discord.ext.commands.converter import TextChannelConverter -from discord.ext.commands.errors import BadArgument +from disnake import Colour, Message, NotFound, TextChannel, User, errors +from disnake.ext.commands import Cog, Context, Converter, Greedy, group, has_any_role +from disnake.ext.commands.converter import TextChannelConverter +from disnake.ext.commands.errors import BadArgument from bot.bot import Bot from bot.constants import Channels, CleanMessages, Colours, Emojis, Event, Icons, MODERATION_ROLES @@ -459,7 +459,7 @@ class Clean(Cog): regex: Optional[Regex] = None, bots_only: Optional[bool] = False, *, - channels: CleanChannels = None # "Optional" with discord.py silently ignores incorrect input. + channels: CleanChannels = None # "Optional" with disnake silently ignores incorrect input. ) -> None: """ Commands for cleaning messages in channels. diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py index 178be734d..58e049d4f 100644 --- a/bot/exts/moderation/defcon.py +++ b/bot/exts/moderation/defcon.py @@ -8,9 +8,9 @@ import arrow from aioredis import RedisError from async_rediscache import RedisCache from dateutil.relativedelta import relativedelta -from discord import Colour, Embed, Forbidden, Member, TextChannel, User -from discord.ext import tasks -from discord.ext.commands import Cog, Context, group, has_any_role +from disnake import Colour, Embed, Forbidden, Member, TextChannel, User +from disnake.ext import tasks +from disnake.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_ROLES, Roles diff --git a/bot/exts/moderation/dm_relay.py b/bot/exts/moderation/dm_relay.py index 566422e29..28e131eb4 100644 --- a/bot/exts/moderation/dm_relay.py +++ b/bot/exts/moderation/dm_relay.py @@ -1,5 +1,5 @@ -import discord -from discord.ext.commands import Cog, Context, command, has_any_role +import disnake +from disnake.ext.commands import Cog, Context, command, has_any_role from bot.bot import Bot from bot.constants import Emojis, MODERATION_ROLES @@ -17,7 +17,7 @@ class DMRelay(Cog): self.bot = bot @command(aliases=("relay", "dr")) - async def dmrelay(self, ctx: Context, user: discord.User, limit: int = 100) -> None: + async def dmrelay(self, ctx: Context, user: disnake.User, limit: int = 100) -> None: """Relays the direct message history between the bot and given user.""" log.trace(f"Relaying DMs with {user.name} ({user.id})") diff --git a/bot/exts/moderation/incidents.py b/bot/exts/moderation/incidents.py index b579416a6..c4c03e546 100644 --- a/bot/exts/moderation/incidents.py +++ b/bot/exts/moderation/incidents.py @@ -4,9 +4,9 @@ from datetime import datetime from enum import Enum from typing import Optional -import discord +import disnake from async_rediscache import RedisCache -from discord.ext.commands import Cog, Context, MessageConverter, MessageNotFound +from disnake.ext.commands import Cog, Context, MessageConverter, MessageNotFound from bot.bot import Bot from bot.constants import Channels, Colours, Emojis, Guild, Roles, Webhooks @@ -52,10 +52,10 @@ ALL_SIGNALS: set[str] = {signal.value for signal in Signal} # An embed coupled with an optional file to be dispatched # If the file is not None, the embed attempts to show it in its body -FileEmbed = tuple[discord.Embed, Optional[discord.File]] +FileEmbed = tuple[disnake.Embed, Optional[disnake.File]] -async def download_file(attachment: discord.Attachment) -> Optional[discord.File]: +async def download_file(attachment: disnake.Attachment) -> Optional[disnake.File]: """ Download & return `attachment` file. @@ -65,13 +65,13 @@ async def download_file(attachment: discord.Attachment) -> Optional[discord.File log.debug(f"Attempting to download attachment: {attachment.filename}") try: return await attachment.to_file() - except (discord.NotFound, discord.Forbidden) as exc: + except (disnake.NotFound, disnake.Forbidden) as exc: log.debug(f"Failed to download attachment: {exc}") except Exception: log.exception("Failed to download attachment") -async def make_embed(incident: discord.Message, outcome: Signal, actioned_by: discord.Member) -> FileEmbed: +async def make_embed(incident: disnake.Message, outcome: Signal, actioned_by: disnake.Member) -> FileEmbed: """ Create an embed representation of `incident` for the #incidents-archive channel. @@ -97,7 +97,7 @@ async def make_embed(incident: discord.Message, outcome: Signal, actioned_by: di colour = Colours.soft_red footer = f"Rejected by {actioned_by}" - embed = discord.Embed( + embed = disnake.Embed( description=incident.content, timestamp=datetime.utcnow(), colour=colour, @@ -113,12 +113,12 @@ async def make_embed(incident: discord.Message, outcome: Signal, actioned_by: di else: embed.set_author(name="[Failed to relay attachment]", url=attachment.proxy_url) # Embed links the file else: - file = discord.utils.MISSING + file = disnake.utils.MISSING return embed, file -def is_incident(message: discord.Message) -> bool: +def is_incident(message: disnake.Message) -> bool: """True if `message` qualifies as an incident, False otherwise.""" conditions = ( message.channel.id == Channels.incidents, # Message sent in #incidents @@ -129,12 +129,12 @@ def is_incident(message: discord.Message) -> bool: return all(conditions) -def own_reactions(message: discord.Message) -> set[str]: +def own_reactions(message: disnake.Message) -> set[str]: """Get the set of reactions placed on `message` by the bot itself.""" return {str(reaction.emoji) for reaction in message.reactions if reaction.me} -def has_signals(message: discord.Message) -> bool: +def has_signals(message: disnake.Message) -> bool: """True if `message` already has all `Signal` reactions, False otherwise.""" return ALL_SIGNALS.issubset(own_reactions(message)) @@ -167,9 +167,9 @@ def shorten_text(text: str) -> str: return text -async def make_message_link_embed(ctx: Context, message_link: str) -> Optional[discord.Embed]: +async def make_message_link_embed(ctx: Context, message_link: str) -> Optional[disnake.Embed]: """ - Create an embedded representation of the discord message link contained in the incident report. + Create an embedded representation of the Discord message link contained in the incident report. The Embed would contain the following information --> Author: @Jason Terror ♦ (736234578745884682) @@ -179,23 +179,23 @@ async def make_message_link_embed(ctx: Context, message_link: str) -> Optional[d embed = None try: - message: discord.Message = await MessageConverter().convert(ctx, message_link) + message: disnake.Message = await MessageConverter().convert(ctx, message_link) except MessageNotFound: mod_logs_channel = ctx.bot.get_channel(Channels.mod_log) - last_100_logs: list[discord.Message] = await mod_logs_channel.history(limit=100).flatten() + last_100_logs: list[disnake.Message] = await mod_logs_channel.history(limit=100).flatten() for log_entry in last_100_logs: if not log_entry.embeds: continue - log_embed: discord.Embed = log_entry.embeds[0] + log_embed: disnake.Embed = log_entry.embeds[0] if ( log_embed.author.name == "Message deleted" and f"[Jump to message]({message_link})" in log_embed.description ): - embed = discord.Embed( - colour=discord.Colour.dark_gold(), + embed = disnake.Embed( + colour=disnake.Colour.dark_gold(), title="Deleted Message Link", description=( f"Found <#{Channels.mod_log}> entry for deleted message: " @@ -203,12 +203,12 @@ async def make_message_link_embed(ctx: Context, message_link: str) -> Optional[d ) ) if not embed: - embed = discord.Embed( - colour=discord.Colour.red(), + embed = disnake.Embed( + colour=disnake.Colour.red(), title="Bad Message Link", description=f"Message {message_link} not found." ) - except discord.DiscordException as e: + except disnake.DiscordException as e: log.exception(f"Failed to make message link embed for '{message_link}', raised exception: {e}") else: channel = message.channel @@ -219,12 +219,12 @@ async def make_message_link_embed(ctx: Context, message_link: str) -> Optional[d ) return - embed = discord.Embed( - colour=discord.Colour.gold(), + embed = disnake.Embed( + colour=disnake.Colour.gold(), description=( f"**Author:** {format_user(message.author)}\n" f"**Channel:** {channel.mention} ({channel.category}" - f"{f'/#{channel.parent.name} - ' if isinstance(channel, discord.Thread) else '/#'}" + f"{f'/#{channel.parent.name} - ' if isinstance(channel, disnake.Thread) else '/#'}" f"{channel.name})\n" ), timestamp=message.created_at @@ -242,7 +242,7 @@ async def make_message_link_embed(ctx: Context, message_link: str) -> Optional[d return embed -async def add_signals(incident: discord.Message) -> None: +async def add_signals(incident: disnake.Message) -> None: """ Add `Signal` member emoji to `incident` as reactions. @@ -257,7 +257,7 @@ async def add_signals(incident: discord.Message) -> None: log.trace(f"Adding reaction: {signal_emoji}") try: await incident.add_reaction(signal_emoji.value) - except discord.NotFound as e: + except disnake.NotFound as e: if e.code != 10008: raise @@ -300,7 +300,7 @@ class Incidents(Cog): """ # This dictionary maps an incident report message to the message link embed's ID - # RedisCache[discord.Message.id, discord.Message.id] + # RedisCache[disnake.Message.id, disnake.Message.id] message_link_embeds_cache = RedisCache() def __init__(self, bot: Bot) -> None: @@ -319,7 +319,7 @@ class Incidents(Cog): try: self.incidents_webhook = await self.bot.fetch_webhook(Webhooks.incidents) - except discord.HTTPException: + except disnake.HTTPException: log.error(f"Failed to fetch incidents webhook with id `{Webhooks.incidents}`.") async def crawl_incidents(self) -> None: @@ -335,7 +335,7 @@ class Incidents(Cog): Behaviour is configured by: `CRAWL_LIMIT`, `CRAWL_SLEEP`. """ await self.bot.wait_until_guild_available() - incidents: discord.TextChannel = self.bot.get_channel(Channels.incidents) + incidents: disnake.TextChannel = self.bot.get_channel(Channels.incidents) log.debug(f"Crawling messages in #incidents: {CRAWL_LIMIT=}, {CRAWL_SLEEP=}") async for message in incidents.history(limit=CRAWL_LIMIT): @@ -353,7 +353,7 @@ class Incidents(Cog): log.debug("Crawl task finished!") - async def archive(self, incident: discord.Message, outcome: Signal, actioned_by: discord.Member) -> bool: + async def archive(self, incident: disnake.Message, outcome: Signal, actioned_by: disnake.Member) -> bool: """ Relay an embed representation of `incident` to the #incidents-archive channel. @@ -392,7 +392,7 @@ class Incidents(Cog): log.trace("Message archived successfully!") return True - def make_confirmation_task(self, incident: discord.Message, timeout: int = 5) -> asyncio.Task: + def make_confirmation_task(self, incident: disnake.Message, timeout: int = 5) -> asyncio.Task: """ Create a task to wait `timeout` seconds for `incident` to be deleted. @@ -401,13 +401,13 @@ class Incidents(Cog): """ log.trace(f"Confirmation task will wait {timeout=} seconds for {incident.id=} to be deleted") - def check(payload: discord.RawReactionActionEvent) -> bool: + def check(payload: disnake.RawReactionActionEvent) -> bool: return payload.message_id == incident.id coroutine = self.bot.wait_for(event="raw_message_delete", check=check, timeout=timeout) return scheduling.create_task(coroutine, event_loop=self.bot.loop) - async def process_event(self, reaction: str, incident: discord.Message, member: discord.Member) -> None: + async def process_event(self, reaction: str, incident: disnake.Message, member: disnake.Member) -> None: """ Process a `reaction_add` event in #incidents. @@ -430,7 +430,7 @@ class Incidents(Cog): log.debug(f"Removing invalid reaction: user {member} is not permitted to send signals") try: await incident.remove_reaction(reaction, member) - except discord.NotFound: + except disnake.NotFound: log.trace("Couldn't remove reaction because the reaction or its message was deleted") return @@ -440,7 +440,7 @@ class Incidents(Cog): log.debug(f"Removing invalid reaction: emoji {reaction} is not a valid signal") try: await incident.remove_reaction(reaction, member) - except discord.NotFound: + except disnake.NotFound: log.trace("Couldn't remove reaction because the reaction or its message was deleted") return @@ -461,7 +461,7 @@ class Incidents(Cog): log.trace("Deleting original message") try: await incident.delete() - except discord.NotFound: + except disnake.NotFound: log.trace("Couldn't delete message because it was already deleted") log.trace(f"Awaiting deletion confirmation: {timeout=} seconds") @@ -476,9 +476,9 @@ class Incidents(Cog): # Deletes the message link embeds found in cache from the channel and cache. await self.delete_msg_link_embed(incident.id) - async def resolve_message(self, message_id: int) -> Optional[discord.Message]: + async def resolve_message(self, message_id: int) -> Optional[disnake.Message]: """ - Get `discord.Message` for `message_id` from cache, or API. + Get `disnake.Message` for `message_id` from cache, or API. We first look into the local cache to see if the message is present. @@ -491,7 +491,7 @@ class Incidents(Cog): """ await self.bot.wait_until_guild_available() # First make sure that the cache is ready log.trace(f"Resolving message for: {message_id=}") - message: Optional[discord.Message] = self.bot._connection._get_message(message_id) + message: Optional[disnake.Message] = self.bot._connection._get_message(message_id) if message is not None: log.trace("Message was found in cache") @@ -500,7 +500,7 @@ class Incidents(Cog): log.trace("Message not found, attempting to fetch") try: message = await self.bot.get_channel(Channels.incidents).fetch_message(message_id) - except discord.NotFound: + except disnake.NotFound: log.trace("Message doesn't exist, it was likely already relayed") except Exception: log.exception(f"Failed to fetch message {message_id}!") @@ -509,7 +509,7 @@ class Incidents(Cog): return message @Cog.listener() - async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent) -> None: + async def on_raw_reaction_add(self, payload: disnake.RawReactionActionEvent) -> None: """ Pre-process `payload` and pass it to `process_event` if appropriate. @@ -521,11 +521,11 @@ class Incidents(Cog): Next, we acquire `event_lock` - to prevent racing, events are processed one at a time. - Once we have the lock, the `discord.Message` object for this event must be resolved. + Once we have the lock, the `disnake.Message` object for this event must be resolved. If the lock was previously held by an event which successfully relayed the incident, this will fail and we abort the current event. - Finally, with both the lock and the `discord.Message` instance in our hands, we delegate + Finally, with both the lock and the `disnake.Message` instance in our hands, we delegate to `process_event` to handle the event. The justification for using a raw listener is the need to receive events for messages @@ -554,7 +554,7 @@ class Incidents(Cog): log.trace("Releasing event lock") @Cog.listener() - async def on_message(self, message: discord.Message) -> None: + async def on_message(self, message: disnake.Message) -> None: """ Pass `message` to `add_signals` and `extract_message_links` if it satisfies `is_incident`. @@ -575,7 +575,7 @@ class Incidents(Cog): await self.send_message_link_embeds(embed_list, message, self.incidents_webhook) @Cog.listener() - async def on_raw_message_delete(self, payload: discord.RawMessageDeleteEvent) -> None: + async def on_raw_message_delete(self, payload: disnake.RawMessageDeleteEvent) -> None: """ Delete message link embeds for `payload.message_id`. @@ -584,7 +584,7 @@ class Incidents(Cog): if self.incidents_webhook: await self.delete_msg_link_embed(payload.message_id) - async def extract_message_links(self, message: discord.Message) -> Optional[list[discord.Embed]]: + async def extract_message_links(self, message: disnake.Message) -> Optional[list[disnake.Embed]]: """ Check if there's any message links in the text content. @@ -615,8 +615,8 @@ class Incidents(Cog): async def send_message_link_embeds( self, webhook_embed_list: list, - message: discord.Message, - webhook: discord.Webhook, + message: disnake.Message, + webhook: disnake.Webhook, ) -> Optional[int]: """ Send message link embeds to #incidents channel. @@ -634,7 +634,7 @@ class Incidents(Cog): avatar_url=message.author.display_avatar.url, wait=True, ) - except discord.DiscordException: + except disnake.DiscordException: log.exception( f"Failed to send message link embed {message.id} to #incidents." ) @@ -651,7 +651,7 @@ class Incidents(Cog): if webhook_msg_id: try: await self.incidents_webhook.delete_message(webhook_msg_id) - except discord.errors.NotFound: + except disnake.errors.NotFound: log.trace(f"Incidents message link embed (`{webhook_msg_id}`) has already been deleted, skipping.") await self.message_link_embeds_cache.delete(message_id) diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py index 47b639421..d51009358 100644 --- a/bot/exts/moderation/infraction/_scheduler.py +++ b/bot/exts/moderation/infraction/_scheduler.py @@ -5,8 +5,8 @@ from gettext import ngettext import arrow import dateutil.parser -import discord -from discord.ext.commands import Context +import disnake +from disnake.ext.commands import Context from bot import constants from bot.api import ResponseCodeError @@ -101,7 +101,7 @@ class InfractionScheduler: # Allowing mod log since this is a passive action that should be logged. try: await apply_coro - except discord.HTTPException as e: + except disnake.HTTPException as e: # When user joined and then right after this left again before action completed, this can't apply roles if e.code == 10007 or e.status == 404: log.info( @@ -203,7 +203,7 @@ class InfractionScheduler: if expiry: # Schedule the expiration of the infraction. self.schedule_expiration(infraction) - except discord.HTTPException as e: + except disnake.HTTPException as e: # Accordingly display that applying the infraction failed. # Don't use ctx.message.author; antispam only patches ctx.author. confirm_msg = ":x: failed to apply" @@ -212,7 +212,7 @@ class InfractionScheduler: log_title = "failed to apply" log_msg = f"Failed to apply {' '.join(infr_type.split('_'))} infraction #{id_} to {user}" - if isinstance(e, discord.Forbidden): + if isinstance(e, disnake.Forbidden): log.warning(f"{log_msg}: bot lacks permissions.") elif e.code == 10007 or e.status == 404: log.info( @@ -402,11 +402,11 @@ class InfractionScheduler: raise ValueError( f"Attempted to deactivate an unsupported infraction #{id_} ({type_})!" ) - except discord.Forbidden: + except disnake.Forbidden: log.warning(f"Failed to deactivate infraction #{id_} ({type_}): bot lacks permissions.") log_text["Failure"] = "The bot lacks permissions to do this (role hierarchy?)" log_content = mod_role.mention - except discord.HTTPException as e: + except disnake.HTTPException as e: if e.code == 10007 or e.status == 404: log.info( f"Can't pardon {infraction['type']} for user {infraction['user']} because user left the guild." diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index 4df833ffb..a464f7c87 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,8 +1,8 @@ import typing as t from datetime import datetime -import discord -from discord.ext.commands import Context +import disnake +from disnake.ext.commands import Context from bot.api import ResponseCodeError from bot.bot import Bot @@ -83,7 +83,7 @@ async def post_infraction( dm_sent: bool = False, ) -> t.Optional[dict]: """Posts an infraction to the API.""" - if isinstance(user, (discord.Member, discord.User)) and user.bot: + if isinstance(user, (disnake.Member, disnake.User)) and user.bot: log.trace(f"Posting of {infr_type} infraction for {user} to the API aborted. User is a bot.") raise InvalidInfractedUserError(user) @@ -182,7 +182,7 @@ async def notify_infraction( text += INFRACTION_APPEAL_SERVER_FOOTER if infr_type.lower() == 'ban' else INFRACTION_APPEAL_MODMAIL_FOOTER - embed = discord.Embed( + embed = disnake.Embed( description=text, colour=Colours.soft_red ) @@ -211,7 +211,7 @@ async def notify_pardon( """DM a user about their pardoned infraction and return True if the DM is successful.""" log.trace(f"Sending {user} a DM about their pardoned infraction.") - embed = discord.Embed( + embed = disnake.Embed( description=content, colour=Colours.soft_green ) @@ -221,7 +221,7 @@ async def notify_pardon( return await send_private_embed(user, embed) -async def send_private_embed(user: MemberOrUser, embed: discord.Embed) -> bool: +async def send_private_embed(user: MemberOrUser, embed: disnake.Embed) -> bool: """ A helper method for sending an embed to a user's DMs. @@ -230,7 +230,7 @@ async def send_private_embed(user: MemberOrUser, embed: discord.Embed) -> bool: try: await user.send(embed=embed) return True - except (discord.HTTPException, discord.Forbidden, discord.NotFound): + except (disnake.HTTPException, disnake.Forbidden, disnake.NotFound): log.debug( f"Infraction-related information could not be sent to user {user} ({user.id}). " "The user either could not be retrieved or probably disabled their DMs." diff --git a/bot/exts/moderation/infraction/infractions.py b/bot/exts/moderation/infraction/infractions.py index af42ab1b8..5ff56abde 100644 --- a/bot/exts/moderation/infraction/infractions.py +++ b/bot/exts/moderation/infraction/infractions.py @@ -1,10 +1,10 @@ import textwrap import typing as t -import discord -from discord import Member -from discord.ext import commands -from discord.ext.commands import Context, command +import disnake +from disnake import Member +from disnake.ext import commands +from disnake.ext.commands import Context, command from bot import constants from bot.bot import Bot @@ -35,8 +35,8 @@ class Infractions(InfractionScheduler, commands.Cog): super().__init__(bot, supported_infractions={"ban", "kick", "mute", "note", "warning", "voice_mute"}) self.category = "Moderation" - self._muted_role = discord.Object(constants.Roles.muted) - self._voice_verified_role = discord.Object(constants.Roles.voice_verified) + self._muted_role = disnake.Object(constants.Roles.muted) + self._voice_verified_role = disnake.Object(constants.Roles.voice_verified) @commands.Cog.listener() async def on_member_join(self, member: Member) -> None: @@ -123,7 +123,7 @@ class Infractions(InfractionScheduler, commands.Cog): log.error("Failed to apply ban to user %d", user.id) return - # Calling commands directly skips Discord.py's convertors, so we need to convert args manually. + # Calling commands directly skips disnake's convertors, so we need to convert args manually. clean_time = await Age().convert(ctx, "1h") log_url = await clean_cog._clean_messages( @@ -494,7 +494,7 @@ class Infractions(InfractionScheduler, commands.Cog): async def pardon_mute( self, user_id: int, - guild: discord.Guild, + guild: disnake.Guild, reason: t.Optional[str], *, notify: bool = True @@ -525,16 +525,16 @@ class Infractions(InfractionScheduler, commands.Cog): return log_text - async def pardon_ban(self, user_id: int, guild: discord.Guild, reason: t.Optional[str]) -> t.Dict[str, str]: + async def pardon_ban(self, user_id: int, guild: disnake.Guild, reason: t.Optional[str]) -> t.Dict[str, str]: """Remove a user's ban on the Discord guild and return a log dict.""" - user = discord.Object(user_id) + user = disnake.Object(user_id) log_text = {} self.mod_log.ignore(Event.member_unban, user_id) try: await guild.unban(user, reason=reason) - except discord.NotFound: + except disnake.NotFound: log.info(f"Failed to unban user {user_id}: no active ban found on Discord") log_text["Note"] = "No active ban found on Discord." @@ -543,7 +543,7 @@ class Infractions(InfractionScheduler, commands.Cog): async def pardon_voice_mute( self, user_id: int, - guild: discord.Guild, + guild: disnake.Guild, *, notify: bool = True ) -> t.Dict[str, str]: @@ -597,7 +597,7 @@ class Infractions(InfractionScheduler, commands.Cog): async def cog_command_error(self, ctx: Context, error: Exception) -> None: """Send a notification to the invoking context on a Union failure.""" if isinstance(error, commands.BadUnionArgument): - if discord.User in error.converters or Member in error.converters: + if disnake.User in error.converters or Member in error.converters: await ctx.send(str(error.errors[0])) error.handled = True diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py index dda3fadae..875e8ef34 100644 --- a/bot/exts/moderation/infraction/management.py +++ b/bot/exts/moderation/infraction/management.py @@ -1,10 +1,10 @@ import textwrap import typing as t -import discord -from discord.ext import commands -from discord.ext.commands import Context -from discord.utils import escape_markdown +import disnake +from disnake.ext import commands +from disnake.ext.commands import Context +from disnake.utils import escape_markdown from bot import constants from bot.bot import Bot @@ -53,9 +53,9 @@ class ModManagement(commands.Cog): await ctx.send_help(ctx.command) return - embed = discord.Embed( + embed = disnake.Embed( title=f"Infraction #{infraction['id']}", - colour=discord.Colour.orange() + colour=disnake.Colour.orange() ) await self.send_infraction_list(ctx, embed, [infraction]) @@ -199,7 +199,7 @@ class ModManagement(commands.Cog): await self.mod_log.send_log_message( icon_url=constants.Icons.pencil, - colour=discord.Colour.og_blurple(), + colour=disnake.Colour.og_blurple(), title="Infraction edited", thumbnail=thumbnail, text=textwrap.dedent(f""" @@ -217,21 +217,21 @@ class ModManagement(commands.Cog): async def infraction_search_group(self, ctx: Context, query: t.Union[UnambiguousUser, Snowflake, str]) -> None: """Searches for infractions in the database.""" if isinstance(query, int): - await self.search_user(ctx, discord.Object(query)) + await self.search_user(ctx, disnake.Object(query)) elif isinstance(query, str): await self.search_reason(ctx, query) else: await self.search_user(ctx, query) @infraction_search_group.command(name="user", aliases=("member", "userid")) - async def search_user(self, ctx: Context, user: t.Union[MemberOrUser, discord.Object]) -> None: + async def search_user(self, ctx: Context, user: t.Union[MemberOrUser, disnake.Object]) -> None: """Search for infractions by member.""" infraction_list = await self.bot.api_client.get( 'bot/infractions/expanded', params={'user__id': str(user.id)} ) - if isinstance(user, (discord.Member, discord.User)): + if isinstance(user, (disnake.Member, disnake.User)): user_str = escape_markdown(str(user)) else: if infraction_list: @@ -241,9 +241,9 @@ class ModManagement(commands.Cog): user_str = str(user.id) formatted_infraction_count = self.format_infraction_count(len(infraction_list)) - embed = discord.Embed( + embed = disnake.Embed( title=f"Infractions for {user_str} ({formatted_infraction_count} total)", - colour=discord.Colour.orange() + colour=disnake.Colour.orange() ) await self.send_infraction_list(ctx, embed, infraction_list) @@ -256,9 +256,9 @@ class ModManagement(commands.Cog): ) formatted_infraction_count = self.format_infraction_count(len(infraction_list)) - embed = discord.Embed( + embed = disnake.Embed( title=f"Infractions matching `{reason}` ({formatted_infraction_count} total)", - colour=discord.Colour.orange() + colour=disnake.Colour.orange() ) await self.send_infraction_list(ctx, embed, infraction_list) @@ -296,9 +296,9 @@ class ModManagement(commands.Cog): ) formatted_infraction_count = self.format_infraction_count(len(infraction_list)) - embed = discord.Embed( + embed = disnake.Embed( title=f"Infractions by {actor} ({formatted_infraction_count} total)", - colour=discord.Colour.orange() + colour=disnake.Colour.orange() ) await self.send_infraction_list(ctx, embed, infraction_list) @@ -321,7 +321,7 @@ class ModManagement(commands.Cog): async def send_infraction_list( self, ctx: Context, - embed: discord.Embed, + embed: disnake.Embed, infractions: t.Iterable[t.Dict[str, t.Any]] ) -> None: """Send a paginated embed of infractions for the specified user.""" @@ -410,7 +410,7 @@ class ModManagement(commands.Cog): async def cog_command_error(self, ctx: Context, error: commands.CommandError) -> None: """Handles errors for commands within this cog.""" if isinstance(error, commands.BadUnionArgument): - if discord.User in error.converters: + if disnake.User in error.converters: await ctx.send(str(error.errors[0])) error.handled = True diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py index 3f1bffd76..1d357d441 100644 --- a/bot/exts/moderation/infraction/superstarify.py +++ b/bot/exts/moderation/infraction/superstarify.py @@ -4,9 +4,9 @@ import textwrap import typing as t from pathlib import Path -from discord import Embed, Member -from discord.ext.commands import Cog, Context, command, has_any_role -from discord.utils import escape_markdown +from disnake import Embed, Member +from disnake.ext.commands import Cog, Context, command, has_any_role +from disnake.utils import escape_markdown from bot import constants from bot.bot import Bot diff --git a/bot/exts/moderation/metabase.py b/bot/exts/moderation/metabase.py index ce9c220b3..482d49b83 100644 --- a/bot/exts/moderation/metabase.py +++ b/bot/exts/moderation/metabase.py @@ -8,7 +8,7 @@ import arrow from aiohttp.client_exceptions import ClientResponseError from arrow import Arrow from async_rediscache import RedisCache -from discord.ext.commands import Cog, Context, group, has_any_role +from disnake.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.constants import Metabase as MetabaseConfig, Roles diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 32ea0dc6a..a96638e53 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -5,13 +5,13 @@ import typing as t from datetime import datetime, timezone from itertools import zip_longest -import discord +import disnake from dateutil.relativedelta import relativedelta from deepdiff import DeepDiff -from discord import Colour, Message, Thread -from discord.abc import GuildChannel -from discord.ext.commands import Cog, Context -from discord.utils import escape_markdown +from disnake import Colour, Message, Thread +from disnake.abc import GuildChannel +from disnake.ext.commands import Cog, Context +from disnake.utils import escape_markdown from bot.bot import Bot from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, Roles, URLs @@ -21,7 +21,7 @@ from bot.utils.messages import format_user log = get_logger(__name__) -GUILD_CHANNEL = t.Union[discord.CategoryChannel, discord.TextChannel, discord.VoiceChannel] +GUILD_CHANNEL = t.Union[disnake.CategoryChannel, disnake.TextChannel, disnake.VoiceChannel] CHANNEL_CHANGES_UNSUPPORTED = ("permissions",) CHANNEL_CHANGES_SUPPRESSED = ("_overwrites", "position") @@ -45,7 +45,7 @@ class ModLog(Cog, name="ModLog"): async def upload_log( self, - messages: t.Iterable[discord.Message], + messages: t.Iterable[disnake.Message], actor_id: int, attachments: t.Iterable[t.List[str]] = None ) -> str: @@ -83,22 +83,22 @@ class ModLog(Cog, name="ModLog"): async def send_log_message( self, icon_url: t.Optional[str], - colour: t.Union[discord.Colour, int], + colour: t.Union[disnake.Colour, int], title: t.Optional[str], text: str, - thumbnail: t.Optional[t.Union[str, discord.Asset]] = None, + thumbnail: t.Optional[t.Union[str, disnake.Asset]] = None, channel_id: int = Channels.mod_log, ping_everyone: bool = False, - files: t.Optional[t.List[discord.File]] = None, + files: t.Optional[t.List[disnake.File]] = None, content: t.Optional[str] = None, - additional_embeds: t.Optional[t.List[discord.Embed]] = None, + additional_embeds: t.Optional[t.List[disnake.Embed]] = None, timestamp_override: t.Optional[datetime] = None, footer: t.Optional[str] = None, ) -> Context: """Generate log embed and send to logging channel.""" await self.bot.wait_until_guild_available() # Truncate string directly here to avoid removing newlines - embed = discord.Embed( + embed = disnake.Embed( description=text[:4093] + "..." if len(text) > 4096 else text ) @@ -143,10 +143,10 @@ class ModLog(Cog, name="ModLog"): if channel.guild.id != GuildConstant.id: return - if isinstance(channel, discord.CategoryChannel): + if isinstance(channel, disnake.CategoryChannel): title = "Category created" message = f"{channel.name} (`{channel.id}`)" - elif isinstance(channel, discord.VoiceChannel): + elif isinstance(channel, disnake.VoiceChannel): title = "Voice channel created" if channel.category: @@ -169,14 +169,14 @@ class ModLog(Cog, name="ModLog"): if channel.guild.id != GuildConstant.id: return - if isinstance(channel, discord.CategoryChannel): + if isinstance(channel, disnake.CategoryChannel): title = "Category deleted" - elif isinstance(channel, discord.VoiceChannel): + elif isinstance(channel, disnake.VoiceChannel): title = "Voice channel deleted" else: title = "Text channel deleted" - if channel.category and not isinstance(channel, discord.CategoryChannel): + if channel.category and not isinstance(channel, disnake.CategoryChannel): message = f"{channel.category}/{channel.name} (`{channel.id}`)" else: message = f"{channel.name} (`{channel.id}`)" @@ -256,7 +256,7 @@ class ModLog(Cog, name="ModLog"): ) @Cog.listener() - async def on_guild_role_create(self, role: discord.Role) -> None: + async def on_guild_role_create(self, role: disnake.Role) -> None: """Log role create event to mod log.""" if role.guild.id != GuildConstant.id: return @@ -267,7 +267,7 @@ class ModLog(Cog, name="ModLog"): ) @Cog.listener() - async def on_guild_role_delete(self, role: discord.Role) -> None: + async def on_guild_role_delete(self, role: disnake.Role) -> None: """Log role delete event to mod log.""" if role.guild.id != GuildConstant.id: return @@ -278,7 +278,7 @@ class ModLog(Cog, name="ModLog"): ) @Cog.listener() - async def on_guild_role_update(self, before: discord.Role, after: discord.Role) -> None: + async def on_guild_role_update(self, before: disnake.Role, after: disnake.Role) -> None: """Log role update event to mod log.""" if before.guild.id != GuildConstant.id: return @@ -331,7 +331,7 @@ class ModLog(Cog, name="ModLog"): ) @Cog.listener() - async def on_guild_update(self, before: discord.Guild, after: discord.Guild) -> None: + async def on_guild_update(self, before: disnake.Guild, after: disnake.Guild) -> None: """Log guild update event to mod log.""" if before.id != GuildConstant.id: return @@ -382,7 +382,7 @@ class ModLog(Cog, name="ModLog"): ) @Cog.listener() - async def on_member_ban(self, guild: discord.Guild, member: discord.Member) -> None: + async def on_member_ban(self, guild: disnake.Guild, member: disnake.Member) -> None: """Log ban event to user log.""" if guild.id != GuildConstant.id: return @@ -399,7 +399,7 @@ class ModLog(Cog, name="ModLog"): ) @Cog.listener() - async def on_member_join(self, member: discord.Member) -> None: + async def on_member_join(self, member: disnake.Member) -> None: """Log member join event to user log.""" if member.guild.id != GuildConstant.id: return @@ -420,7 +420,7 @@ class ModLog(Cog, name="ModLog"): ) @Cog.listener() - async def on_member_remove(self, member: discord.Member) -> None: + async def on_member_remove(self, member: disnake.Member) -> None: """Log member leave event to user log.""" if member.guild.id != GuildConstant.id: return @@ -437,7 +437,7 @@ class ModLog(Cog, name="ModLog"): ) @Cog.listener() - async def on_member_unban(self, guild: discord.Guild, member: discord.User) -> None: + async def on_member_unban(self, guild: disnake.Guild, member: disnake.User) -> None: """Log member unban event to mod log.""" if guild.id != GuildConstant.id: return @@ -454,7 +454,7 @@ class ModLog(Cog, name="ModLog"): ) @staticmethod - def get_role_diff(before: t.List[discord.Role], after: t.List[discord.Role]) -> t.List[str]: + def get_role_diff(before: t.List[disnake.Role], after: t.List[disnake.Role]) -> t.List[str]: """Return a list of strings describing the roles added and removed.""" changes = [] before_roles = set(before) @@ -469,7 +469,7 @@ class ModLog(Cog, name="ModLog"): return changes @Cog.listener() - async def on_member_update(self, before: discord.Member, after: discord.Member) -> None: + async def on_member_update(self, before: disnake.Member, after: disnake.Member) -> None: """Log member update event to user log.""" if before.guild.id != GuildConstant.id: return @@ -552,7 +552,7 @@ class ModLog(Cog, name="ModLog"): return channel.id in GuildConstant.modlog_blacklist - async def log_cached_deleted_message(self, message: discord.Message) -> None: + async def log_cached_deleted_message(self, message: disnake.Message) -> None: """ Log the message's details to message change log. @@ -608,7 +608,7 @@ class ModLog(Cog, name="ModLog"): channel_id=Channels.message_log ) - async def log_uncached_deleted_message(self, event: discord.RawMessageDeleteEvent) -> None: + async def log_uncached_deleted_message(self, event: disnake.RawMessageDeleteEvent) -> None: """ Log the message's details to message change log. @@ -648,7 +648,7 @@ class ModLog(Cog, name="ModLog"): ) @Cog.listener() - async def on_raw_message_delete(self, event: discord.RawMessageDeleteEvent) -> None: + async def on_raw_message_delete(self, event: disnake.RawMessageDeleteEvent) -> None: """Log message deletions to message change log.""" if event.cached_message is not None: await self.log_cached_deleted_message(event.cached_message) @@ -656,7 +656,7 @@ class ModLog(Cog, name="ModLog"): await self.log_uncached_deleted_message(event) @Cog.listener() - async def on_message_edit(self, msg_before: discord.Message, msg_after: discord.Message) -> None: + async def on_message_edit(self, msg_before: disnake.Message, msg_after: disnake.Message) -> None: """Log message edit event to message change log.""" if self.is_message_blacklisted(msg_before): return @@ -727,7 +727,7 @@ class ModLog(Cog, name="ModLog"): ) @Cog.listener() - async def on_raw_message_edit(self, event: discord.RawMessageUpdateEvent) -> None: + async def on_raw_message_edit(self, event: disnake.RawMessageUpdateEvent) -> None: """Log raw message edit event to message change log.""" if event.guild_id is None: return # ignore DM edits @@ -736,7 +736,7 @@ class ModLog(Cog, name="ModLog"): try: channel = self.bot.get_channel(int(event.data["channel_id"])) message = await channel.fetch_message(event.message_id) - except discord.NotFound: # Was deleted before we got the event + except disnake.NotFound: # Was deleted before we got the event return if self.is_message_blacklisted(message): @@ -860,9 +860,9 @@ class ModLog(Cog, name="ModLog"): @Cog.listener() async def on_voice_state_update( self, - member: discord.Member, - before: discord.VoiceState, - after: discord.VoiceState + member: disnake.Member, + before: disnake.VoiceState, + after: disnake.VoiceState ) -> None: """Log member voice state changes to the voice log channel.""" if ( diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index b5cd29b12..51d161d84 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -4,8 +4,8 @@ import datetime import arrow from async_rediscache import RedisCache from dateutil.parser import isoparse, parse as dateutil_parse -from discord import Embed, Member -from discord.ext.commands import Cog, Context, group, has_any_role +from disnake import Embed, Member +from disnake.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.constants import Colours, Emojis, Guild, Icons, MODERATION_ROLES, Roles @@ -22,12 +22,12 @@ MAXIMUM_WORK_LIMIT = 16 class ModPings(Cog): """Commands for a moderator to turn moderator pings on and off.""" - # RedisCache[discord.Member.id, 'Naïve ISO 8601 string'] + # RedisCache[disnake.Member.id, 'Naïve ISO 8601 string'] # The cache's keys are mods who have pings off. # The cache's values are the times when the role should be re-applied to them, stored in ISO format. pings_off_mods = RedisCache() - # RedisCache[discord.Member.id, 'start timestamp|total worktime in seconds'] + # RedisCache[disnake.Member.id, 'start timestamp|total worktime in seconds'] # The cache's keys are mod's ID # The cache's values are their pings on schedule timestamp and the total seconds (work time) until pings off modpings_schedule = RedisCache() diff --git a/bot/exts/moderation/silence.py b/bot/exts/moderation/silence.py index 511520252..0b677dddb 100644 --- a/bot/exts/moderation/silence.py +++ b/bot/exts/moderation/silence.py @@ -5,10 +5,10 @@ from datetime import datetime, timedelta, timezone from typing import Optional, OrderedDict, Union from async_rediscache import RedisCache -from discord import Guild, PermissionOverwrite, TextChannel, Thread, VoiceChannel -from discord.ext import commands, tasks -from discord.ext.commands import Context -from discord.utils import MISSING +from disnake import Guild, PermissionOverwrite, TextChannel, Thread, VoiceChannel +from disnake.ext import commands, tasks +from disnake.ext.commands import Context +from disnake.utils import MISSING from bot import constants from bot.bot import Bot diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py index b6a771441..7fcafc01c 100644 --- a/bot/exts/moderation/slowmode.py +++ b/bot/exts/moderation/slowmode.py @@ -1,8 +1,8 @@ from typing import Optional from dateutil.relativedelta import relativedelta -from discord import TextChannel -from discord.ext.commands import Cog, Context, group, has_any_role +from disnake import TextChannel +from disnake.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.constants import Channels, Emojis, MODERATION_ROLES diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py index 985cc6eb1..7afd9f71d 100644 --- a/bot/exts/moderation/stream.py +++ b/bot/exts/moderation/stream.py @@ -2,10 +2,10 @@ from datetime import timedelta, timezone from operator import itemgetter import arrow -import discord +import disnake from arrow import Arrow from async_rediscache import RedisCache -from discord.ext import commands +from disnake.ext import commands from bot.bot import Bot from bot.constants import ( @@ -24,7 +24,7 @@ class Stream(commands.Cog): """Grant and revoke streaming permissions from members.""" # Stores tasks to remove streaming permission - # RedisCache[discord.Member.id, UtcPosixTimestamp] + # RedisCache[disnake.Member.id, UtcPosixTimestamp] task_cache = RedisCache() def __init__(self, bot: Bot): @@ -37,10 +37,10 @@ class Stream(commands.Cog): self.reload_task.cancel() self.reload_task.add_done_callback(lambda _: self.scheduler.cancel_all()) - async def _revoke_streaming_permission(self, member: discord.Member) -> None: + async def _revoke_streaming_permission(self, member: disnake.Member) -> None: """Remove the streaming permission from the given Member.""" await self.task_cache.delete(member.id) - await member.remove_roles(discord.Object(Roles.video), reason="Streaming access revoked") + await member.remove_roles(disnake.Object(Roles.video), reason="Streaming access revoked") async def _reload_tasks_from_redis(self) -> None: """Reload outstanding tasks from redis on startup, delete the task if the member has since left the server.""" @@ -66,7 +66,7 @@ class Stream(commands.Cog): self._revoke_streaming_permission(member) ) - async def _suspend_stream(self, ctx: commands.Context, member: discord.Member) -> None: + async def _suspend_stream(self, ctx: commands.Context, member: disnake.Member) -> None: """Suspend a member's stream.""" await self.bot.wait_until_guild_available() voice_state = member.voice @@ -90,7 +90,7 @@ class Stream(commands.Cog): @commands.command(aliases=("streaming",)) @commands.has_any_role(*MODERATION_ROLES) - async def stream(self, ctx: commands.Context, member: discord.Member, duration: Expiry = None) -> None: + async def stream(self, ctx: commands.Context, member: disnake.Member, duration: Expiry = None) -> None: """ Temporarily grant streaming permissions to a member for a given duration. @@ -128,7 +128,7 @@ class Stream(commands.Cog): self.scheduler.schedule_at(duration, member.id, self._revoke_streaming_permission(member)) await self.task_cache.set(member.id, duration.timestamp()) - await member.add_roles(discord.Object(Roles.video), reason="Temporary streaming access granted") + await member.add_roles(disnake.Object(Roles.video), reason="Temporary streaming access granted") await ctx.send(f"{Emojis.check_mark} {member.mention} can now stream until {time.discord_timestamp(duration)}.") @@ -142,7 +142,7 @@ class Stream(commands.Cog): @commands.command(aliases=("pstream",)) @commands.has_any_role(*MODERATION_ROLES) - async def permanentstream(self, ctx: commands.Context, member: discord.Member) -> None: + async def permanentstream(self, ctx: commands.Context, member: disnake.Member) -> None: """Permanently grants the given member the permission to stream.""" log.trace(f"Attempting to give permanent streaming permission to {member} ({member.id}).") @@ -163,13 +163,13 @@ class Stream(commands.Cog): log.debug(f"{member} ({member.id}) already had permanent streaming permission.") return - await member.add_roles(discord.Object(Roles.video), reason="Permanent streaming access granted") + await member.add_roles(disnake.Object(Roles.video), reason="Permanent streaming access granted") await ctx.send(f"{Emojis.check_mark} Permanently granted {member.mention} the permission to stream.") log.debug(f"Successfully gave {member} ({member.id}) permanent streaming permission.") @commands.command(aliases=("unstream", "rstream")) @commands.has_any_role(*MODERATION_ROLES) - async def revokestream(self, ctx: commands.Context, member: discord.Member) -> None: + async def revokestream(self, ctx: commands.Context, member: disnake.Member) -> None: """Revoke the permission to stream from the given member.""" log.trace(f"Attempting to remove streaming permission from {member} ({member.id}).") @@ -222,7 +222,7 @@ class Stream(commands.Cog): # Only output the message in the pagination lines = [line[1] for line in streamer_info] - embed = discord.Embed( + embed = disnake.Embed( title=f"Members with streaming permission (`{len(lines)}` total)", colour=Colours.soft_green ) diff --git a/bot/exts/moderation/verification.py b/bot/exts/moderation/verification.py index 37338d19c..c958aa160 100644 --- a/bot/exts/moderation/verification.py +++ b/bot/exts/moderation/verification.py @@ -1,7 +1,7 @@ import typing as t -import discord -from discord.ext.commands import Cog, Context, command, has_any_role +import disnake +from disnake.ext.commands import Cog, Context, command, has_any_role from bot import constants from bot.bot import Bot @@ -51,7 +51,7 @@ async def safe_dm(coro: t.Coroutine) -> None: """ try: await coro - except discord.HTTPException as discord_exc: + except disnake.HTTPException as discord_exc: log.trace(f"DM dispatch failed on status {discord_exc.status} with code: {discord_exc.code}") if discord_exc.code != 50_007: # If any reason other than disabled DMs raise @@ -72,7 +72,7 @@ class Verification(Cog): # region: listeners @Cog.listener() - async def on_member_join(self, member: discord.Member) -> None: + async def on_member_join(self, member: disnake.Member) -> None: """Attempt to send initial direct message to each new member.""" if member.guild.id != constants.Guild.id: return # Only listen for PyDis events @@ -87,11 +87,11 @@ class Verification(Cog): log.trace(f"Sending on join message to new member: {member.id}") try: await safe_dm(member.send(ON_JOIN_MESSAGE)) - except discord.HTTPException: + except disnake.HTTPException: log.exception("DM dispatch failed on unexpected error code") @Cog.listener() - async def on_member_update(self, before: discord.Member, after: discord.Member) -> None: + async def on_member_update(self, before: disnake.Member, after: disnake.Member) -> None: """Check if we need to send a verification DM to a gated user.""" if before.pending is True and after.pending is False: try: @@ -100,7 +100,7 @@ class Verification(Cog): # our alternate welcome DM which includes info such as our welcome # video. await safe_dm(after.send(VERIFIED_MESSAGE)) - except discord.HTTPException: + except disnake.HTTPException: log.exception("DM dispatch failed on unexpected error code") # endregion @@ -108,7 +108,7 @@ class Verification(Cog): @command(name='verify') @has_any_role(*constants.MODERATION_ROLES) - async def perform_manual_verification(self, ctx: Context, user: discord.Member) -> None: + async def perform_manual_verification(self, ctx: Context, user: disnake.Member) -> None: """Command for moderators to verify any user.""" log.trace(f'verify command called by {ctx.author} for {user.id}.') diff --git a/bot/exts/moderation/voice_gate.py b/bot/exts/moderation/voice_gate.py index fa66b00dd..24ae86bdd 100644 --- a/bot/exts/moderation/voice_gate.py +++ b/bot/exts/moderation/voice_gate.py @@ -3,10 +3,10 @@ from contextlib import suppress from datetime import timedelta import arrow -import discord +import disnake from async_rediscache import RedisCache -from discord import Colour, Member, VoiceState -from discord.ext.commands import Cog, Context, command +from disnake import Colour, Member, VoiceState +from disnake.ext.commands import Cog, Context, command from bot.api import ResponseCodeError from bot.bot import Bot @@ -51,7 +51,7 @@ VOICE_PING_DM = ( class VoiceGate(Cog): """Voice channels verification management.""" - # RedisCache[t.Union[discord.User.id, discord.Member.id], t.Union[discord.Message.id, int]] + # RedisCache[t.Union[disnake.User.id, disnake.Member.id], t.Union[disnake.Message.id, int]] # The cache's keys are the IDs of members who are verified or have joined a voice channel # The cache's values are either the message ID of the ping message or 0 (NO_MSG) if no message is present redis_cache = RedisCache() @@ -75,14 +75,14 @@ class VoiceGate(Cog): """ if message_id := await self.redis_cache.get(member_id): log.trace(f"Removing voice gate reminder message for user: {member_id}") - with suppress(discord.NotFound): + with suppress(disnake.NotFound): await self.bot.http.delete_message(Channels.voice_gate, message_id) await self.redis_cache.set(member_id, NO_MSG) else: log.trace(f"Voice gate reminder message for user {member_id} was already removed") @redis_cache.atomic_transaction - async def _ping_newcomer(self, member: discord.Member) -> tuple: + async def _ping_newcomer(self, member: disnake.Member) -> tuple: """ See if `member` should be sent a voice verification notification, and send it if so. @@ -91,7 +91,7 @@ class VoiceGate(Cog): * The `member` is already voice-verified Otherwise, the notification message ID is stored in `redis_cache` and return (True, channel). - channel is either [discord.TextChannel, discord.DMChannel]. + channel is either [disnake.TextChannel, disnake.DMChannel]. """ if await self.redis_cache.contains(member.id): log.trace("User already in cache. Ignore.") @@ -111,7 +111,7 @@ class VoiceGate(Cog): try: message = await member.send(VOICE_PING_DM.format(channel_mention=voice_verification_channel.mention)) - except discord.Forbidden: + except disnake.Forbidden: log.trace("DM failed for Voice ping message. Sending in channel.") message = await voice_verification_channel.send(f"Hello, {member.mention}! {VOICE_PING}") @@ -137,7 +137,7 @@ class VoiceGate(Cog): data = await self.bot.api_client.get(f"bot/users/{ctx.author.id}/metricity_data") except ResponseCodeError as e: if e.status == 404: - embed = discord.Embed( + embed = disnake.Embed( title="Not found", description=( "We were unable to find user data for you. " @@ -148,7 +148,7 @@ class VoiceGate(Cog): ) log.info(f"Unable to find Metricity data about {ctx.author} ({ctx.author.id})") else: - embed = discord.Embed( + embed = disnake.Embed( title="Unexpected response", description=( "We encountered an error while attempting to find data for your user. " @@ -159,7 +159,7 @@ class VoiceGate(Cog): log.warning(f"Got response code {e.status} while trying to get {ctx.author.id} Metricity data.") try: await ctx.author.send(embed=embed) - except discord.Forbidden: + except disnake.Forbidden: log.info("Could not send user DM. Sending in voice-verify channel and scheduling delete.") await ctx.send(embed=embed) @@ -179,7 +179,7 @@ class VoiceGate(Cog): [self.bot.stats.incr(f"voice_gate.failed.{key}") for key, value in checks.items() if value is True] if failed: - embed = discord.Embed( + embed = disnake.Embed( title="Voice Gate failed", description=FAILED_MESSAGE.format(reasons="\n".join(f'• You {reason}.' for reason in failed_reasons)), color=Colour.red() @@ -187,12 +187,12 @@ class VoiceGate(Cog): try: await ctx.author.send(embed=embed) await ctx.send(f"{ctx.author}, please check your DMs.") - except discord.Forbidden: + except disnake.Forbidden: await ctx.channel.send(ctx.author.mention, embed=embed) return self.mod_log.ignore(Event.member_update, ctx.author.id) - embed = discord.Embed( + embed = disnake.Embed( title="Voice gate passed", description="You have been granted permission to use voice channels in Python Discord.", color=Colour.green() @@ -204,17 +204,17 @@ class VoiceGate(Cog): try: await ctx.author.send(embed=embed) await ctx.send(f"{ctx.author}, please check your DMs.") - except discord.Forbidden: + except disnake.Forbidden: await ctx.channel.send(ctx.author.mention, embed=embed) # wait a little bit so those who don't get DMs see the response in-channel before losing perms to see it. await asyncio.sleep(3) - await ctx.author.add_roles(discord.Object(Roles.voice_verified), reason="Voice Gate passed") + await ctx.author.add_roles(disnake.Object(Roles.voice_verified), reason="Voice Gate passed") self.bot.stats.incr("voice_gate.passed") @Cog.listener() - async def on_message(self, message: discord.Message) -> None: + async def on_message(self, message: disnake.Message) -> None: """Delete all non-staff messages from voice gate channel that don't invoke voice verify command.""" # Check is channel voice gate if message.channel.id != Channels.voice_gate: @@ -229,7 +229,7 @@ class VoiceGate(Cog): if message.content.endswith(VOICE_PING): log.trace("Message is the voice verification ping. Ignore.") return - with suppress(discord.NotFound): + with suppress(disnake.NotFound): await message.delete(delay=GateConf.bot_message_delete_delay) return @@ -242,7 +242,7 @@ class VoiceGate(Cog): if ctx.command is not None and ctx.command.name == "voice_verify": self.mod_log.ignore(Event.message_delete, message.id) - with suppress(discord.NotFound): + with suppress(disnake.NotFound): await message.delete() @Cog.listener() @@ -257,7 +257,7 @@ class VoiceGate(Cog): log.trace("User not in a voice channel. Ignore.") return - if isinstance(after.channel, discord.StageChannel): + if isinstance(after.channel, disnake.StageChannel): log.trace("User joined a stage channel. Ignore.") return @@ -267,7 +267,7 @@ class VoiceGate(Cog): # Schedule the channel ping notification to be deleted after the configured delay, which is # again delegated to an atomic helper - if notification_sent and isinstance(message_channel, discord.TextChannel): + if notification_sent and isinstance(message_channel, disnake.TextChannel): await asyncio.sleep(GateConf.voice_ping_delete_delay) await self._delete_ping(member.id) diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py index ee9b6ba45..88669ccaa 100644 --- a/bot/exts/moderation/watchchannels/_watchchannel.py +++ b/bot/exts/moderation/watchchannels/_watchchannel.py @@ -6,9 +6,9 @@ from collections import defaultdict, deque from dataclasses import dataclass from typing import Any, Dict, Optional -import discord -from discord import Color, DMChannel, Embed, HTTPException, Message, errors -from discord.ext.commands import Cog, Context +import disnake +from disnake import Color, DMChannel, Embed, HTTPException, Message, errors +from disnake.ext.commands import Cog, Context from bot.api import ResponseCodeError from bot.bot import Bot @@ -104,7 +104,7 @@ class WatchChannel(metaclass=CogABCMeta): try: self.webhook = await self.bot.fetch_webhook(self.webhook_id) - except discord.HTTPException: + except disnake.HTTPException: self.log.exception(f"Failed to fetch webhook with id `{self.webhook_id}`") if self.channel is None or self.webhook is None: @@ -217,7 +217,7 @@ class WatchChannel(metaclass=CogABCMeta): username = messages.sub_clyde(username) try: await self.webhook.send(content=content, username=username, avatar_url=avatar_url, embed=embed) - except discord.HTTPException as exc: + except disnake.HTTPException as exc: self.log.exception( "Failed to send a message to the webhook", exc_info=exc @@ -265,7 +265,7 @@ class WatchChannel(metaclass=CogABCMeta): username=msg.author.display_name, avatar_url=msg.author.display_avatar.url ) - except discord.HTTPException as exc: + except disnake.HTTPException as exc: self.log.exception( "Failed to send an attachment to the webhook", exc_info=exc diff --git a/bot/exts/moderation/watchchannels/bigbrother.py b/bot/exts/moderation/watchchannels/bigbrother.py index ab37b1b80..b0a48ceff 100644 --- a/bot/exts/moderation/watchchannels/bigbrother.py +++ b/bot/exts/moderation/watchchannels/bigbrother.py @@ -1,7 +1,7 @@ import textwrap from collections import ChainMap -from discord.ext.commands import Cog, Context, group, has_any_role +from disnake.ext.commands import Cog, Context, group, has_any_role from bot.bot import Bot from bot.constants import Channels, MODERATION_ROLES, Webhooks @@ -94,7 +94,7 @@ class BigBrother(WatchChannel, Cog, name="Big Brother"): await ctx.send(f":x: {user.mention} is already being watched.") return - # discord.User instances don't have a roles attribute + # disnake.User instances don't have a roles attribute if hasattr(user, "roles") and any(role.id in MODERATION_ROLES for role in user.roles): await ctx.send(f":x: I'm sorry {ctx.author}, I'm afraid I can't do that. I must be kind to my masters.") return diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py index 0554bf37a..3d784ef77 100644 --- a/bot/exts/recruitment/talentpool/_cog.py +++ b/bot/exts/recruitment/talentpool/_cog.py @@ -3,10 +3,10 @@ from collections import ChainMap, defaultdict from io import StringIO from typing import Optional, Union -import discord +import disnake from async_rediscache import RedisCache -from discord import Color, Embed, Member, PartialMessage, RawReactionActionEvent, User -from discord.ext.commands import BadArgument, Cog, Context, group, has_any_role +from disnake import Color, Embed, Member, PartialMessage, RawReactionActionEvent, User +from disnake.ext.commands import BadArgument, Cog, Context, group, has_any_role from bot.api import ResponseCodeError from bot.bot import Bot @@ -483,7 +483,7 @@ class TalentPool(Cog, name="Talentpool"): async def get_review(self, ctx: Context, user_id: int) -> None: """Get the user's review as a markdown file.""" review, _, _ = await self.reviewer.make_review(user_id) - file = discord.File(StringIO(review), f"{user_id}_review.md") + file = disnake.File(StringIO(review), f"{user_id}_review.md") await ctx.send(file=file) @nomination_group.command(aliases=('review',)) diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py index b4d177622..d496d0eb2 100644 --- a/bot/exts/recruitment/talentpool/_review.py +++ b/bot/exts/recruitment/talentpool/_review.py @@ -10,8 +10,8 @@ from typing import List, Optional, Union import arrow from dateutil.parser import isoparse -from discord import Embed, Emoji, Member, Message, NoMoreItems, NotFound, PartialMessage, TextChannel -from discord.ext.commands import Context +from disnake import Embed, Emoji, Member, Message, NoMoreItems, NotFound, PartialMessage, TextChannel +from disnake.ext.commands import Context from bot.api import ResponseCodeError from bot.bot import Bot diff --git a/bot/exts/utils/bot.py b/bot/exts/utils/bot.py index 8f0094bc9..7d18c0ed3 100644 --- a/bot/exts/utils/bot.py +++ b/bot/exts/utils/bot.py @@ -1,7 +1,7 @@ from typing import Optional -from discord import Embed, TextChannel -from discord.ext.commands import Cog, Context, command, group, has_any_role +from disnake import Embed, TextChannel +from disnake.ext.commands import Cog, Context, command, group, has_any_role from bot.bot import Bot from bot.constants import Guild, MODERATION_ROLES, URLs diff --git a/bot/exts/utils/extensions.py b/bot/exts/utils/extensions.py index fda1e49e2..3d12ae848 100644 --- a/bot/exts/utils/extensions.py +++ b/bot/exts/utils/extensions.py @@ -2,9 +2,9 @@ import functools import typing as t from enum import Enum -from discord import Colour, Embed -from discord.ext import commands -from discord.ext.commands import Context, group +from disnake import Colour, Embed +from disnake.ext import commands +from disnake.ext.commands import Context, group from bot import exts from bot.bot import Bot diff --git a/bot/exts/utils/internal.py b/bot/exts/utils/internal.py index e7113c09c..28c1867ad 100644 --- a/bot/exts/utils/internal.py +++ b/bot/exts/utils/internal.py @@ -9,8 +9,8 @@ from io import StringIO from typing import Any, Optional, Tuple import arrow -import discord -from discord.ext.commands import Cog, Context, group, has_any_role, is_owner +import disnake +from disnake.ext.commands import Cog, Context, group, has_any_role, is_owner from bot.bot import Bot from bot.constants import DEBUG_MODE, Roles @@ -42,7 +42,7 @@ class Internal(Cog): self.socket_event_total += 1 self.socket_events[event_type] += 1 - def _format(self, inp: str, out: Any) -> Tuple[str, Optional[discord.Embed]]: + def _format(self, inp: str, out: Any) -> Tuple[str, Optional[disnake.Embed]]: """Format the eval output into a string & attempt to format it into an Embed.""" self._ = out @@ -103,7 +103,7 @@ class Internal(Cog): res += f"Out[{self.ln}]: " - if isinstance(out, discord.Embed): + if isinstance(out, disnake.Embed): # We made an embed? Send that as embed res += "" res = (res, out) @@ -136,7 +136,7 @@ class Internal(Cog): return res # Return (text, embed) - async def _eval(self, ctx: Context, code: str) -> Optional[discord.Message]: + async def _eval(self, ctx: Context, code: str) -> Optional[disnake.Message]: """Eval the input code string & send an embed to the invoking context.""" self.ln += 1 @@ -154,7 +154,8 @@ class Internal(Cog): "self": self, "bot": self.bot, "inspect": inspect, - "discord": discord, + "discord": disnake, + "disnake": disnake, "contextlib": contextlib } @@ -240,10 +241,10 @@ async def func(): # (None,) -> Any per_s = self.socket_event_total / running_s - stats_embed = discord.Embed( + stats_embed = disnake.Embed( title="WebSocket statistics", description=f"Receiving {per_s:0.2f} events per second.", - color=discord.Color.og_blurple() + color=disnake.Color.og_blurple() ) for event_type, count in self.socket_events.most_common(25): diff --git a/bot/exts/utils/ping.py b/bot/exts/utils/ping.py index 9fb5b7b8f..eeb1d5ff5 100644 --- a/bot/exts/utils/ping.py +++ b/bot/exts/utils/ping.py @@ -1,7 +1,7 @@ import arrow from aiohttp import client_exceptions -from discord import Embed -from discord.ext import commands +from disnake import Embed +from disnake.ext import commands from bot.bot import Bot from bot.constants import Channels, STAFF_PARTNERS_COMMUNITY_ROLES, URLs diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py index ad82d49c9..bf0e9d2ac 100644 --- a/bot/exts/utils/reminders.py +++ b/bot/exts/utils/reminders.py @@ -4,9 +4,9 @@ import typing as t from datetime import datetime, timezone from operator import itemgetter -import discord +import disnake from dateutil.parser import isoparse -from discord.ext.commands import Cog, Context, Greedy, group +from disnake.ext.commands import Cog, Context, Greedy, group from bot.bot import Bot from bot.constants import Guild, Icons, MODERATION_ROLES, POSITIVE_REPLIES, Roles, STAFF_PARTNERS_COMMUNITY_ROLES @@ -26,8 +26,8 @@ LOCK_NAMESPACE = "reminder" WHITELISTED_CHANNELS = Guild.reminder_whitelist MAXIMUM_REMINDERS = 5 -Mentionable = t.Union[discord.Member, discord.Role] -ReminderMention = t.Union[UnambiguousUser, discord.Role] +Mentionable = t.Union[disnake.Member, disnake.Role] +ReminderMention = t.Union[UnambiguousUser, disnake.Role] class Reminders(Cog): @@ -66,7 +66,7 @@ class Reminders(Cog): else: self.schedule_reminder(reminder) - def ensure_valid_reminder(self, reminder: dict) -> t.Tuple[bool, discord.TextChannel]: + def ensure_valid_reminder(self, reminder: dict) -> t.Tuple[bool, disnake.TextChannel]: """Ensure reminder channel can be fetched otherwise delete the reminder.""" channel = self.bot.get_channel(reminder['channel_id']) is_valid = True @@ -87,9 +87,9 @@ class Reminders(Cog): reminder_id: t.Union[str, int] ) -> None: """Send an embed confirming the reminder change was made successfully.""" - embed = discord.Embed( + embed = disnake.Embed( description=on_success, - colour=discord.Colour.green(), + colour=disnake.Colour.green(), title=random.choice(POSITIVE_REPLIES) ) @@ -113,7 +113,7 @@ class Reminders(Cog): if await has_no_roles_check(ctx, *STAFF_PARTNERS_COMMUNITY_ROLES): return False, "members/roles" elif await has_no_roles_check(ctx, *MODERATION_ROLES): - return all(isinstance(mention, (discord.User, discord.Member)) for mention in mentions), "roles" + return all(isinstance(mention, (disnake.User, disnake.Member)) for mention in mentions), "roles" else: return True, "" @@ -173,15 +173,15 @@ class Reminders(Cog): if not is_valid: # No need to cancel the task too; it'll simply be done once this coroutine returns. return - embed = discord.Embed() + embed = disnake.Embed() if expected_time: - embed.colour = discord.Colour.red() + embed.colour = disnake.Colour.red() embed.set_author( icon_url=Icons.remind_red, name="Sorry, your reminder should have arrived earlier!" ) else: - embed.colour = discord.Colour.og_blurple() + embed.colour = disnake.Colour.og_blurple() embed.set_author( icon_url=Icons.remind_blurple, name="It has arrived!" @@ -200,7 +200,7 @@ class Reminders(Cog): partial_message = channel.get_partial_message(int(jump_url.split("/")[-1])) try: await partial_message.reply(content=f"{additional_mentions}", embed=embed) - except discord.HTTPException as e: + except disnake.HTTPException as e: log.info( f"There was an error when trying to reply to a reminder invocation message, {e}, " "fall back to using jump_url" @@ -284,7 +284,7 @@ class Reminders(Cog): # If `content` isn't provided then we try to get message content of a replied message if not content: if reference := ctx.message.reference: - if isinstance((resolved_message := reference.resolved), discord.Message): + if isinstance((resolved_message := reference.resolved), disnake.Message): content = resolved_message.content # If we weren't able to get the content of a replied message if content is None: @@ -361,8 +361,8 @@ class Reminders(Cog): lines.append(text) - embed = discord.Embed() - embed.colour = discord.Colour.og_blurple() + embed = disnake.Embed() + embed.colour = disnake.Colour.og_blurple() embed.title = f"Reminders for {ctx.author}" # Remind the user that they have no reminders :^) @@ -372,7 +372,7 @@ class Reminders(Cog): return # Construct the embed and paginate it. - embed.colour = discord.Colour.og_blurple() + embed.colour = disnake.Colour.og_blurple() await LinePaginator.paginate( lines, diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index cc3a2e1d7..07d824f87 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -8,8 +8,8 @@ from signal import Signals from typing import Optional, Tuple from botcore.regex import FORMATTED_CODE_REGEX, RAW_CODE_REGEX -from discord import AllowedMentions, HTTPException, Message, NotFound, Reaction, User -from discord.ext.commands import Cog, Context, command, guild_only +from disnake import AllowedMentions, HTTPException, Message, NotFound, Reaction, User +from disnake.ext.commands import Cog, Context, command, guild_only from bot.bot import Bot from bot.constants import Categories, Channels, Roles, URLs diff --git a/bot/exts/utils/thread_bumper.py b/bot/exts/utils/thread_bumper.py index 35057f1fe..d37b3b51c 100644 --- a/bot/exts/utils/thread_bumper.py +++ b/bot/exts/utils/thread_bumper.py @@ -1,8 +1,8 @@ import typing as t -import discord +import disnake from async_rediscache import RedisCache -from discord.ext import commands +from disnake.ext import commands from bot import constants from bot.bot import Bot @@ -16,14 +16,14 @@ log = get_logger(__name__) class ThreadBumper(commands.Cog): """Cog that allow users to add the current thread to a list that get reopened on archive.""" - # RedisCache[discord.Thread.id, "sentinel"] + # RedisCache[disnake.Thread.id, "sentinel"] threads_to_bump = RedisCache() def __init__(self, bot: Bot): self.bot = bot self.init_task = scheduling.create_task(self.ensure_bumped_threads_are_active(), event_loop=self.bot.loop) - async def unarchive_threads_not_manually_archived(self, threads: list[discord.Thread]) -> None: + async def unarchive_threads_not_manually_archived(self, threads: list[disnake.Thread]) -> None: """ Iterate through and unarchive any threads that weren't manually archived recently. @@ -35,7 +35,7 @@ class ThreadBumper(commands.Cog): guild = self.bot.get_guild(constants.Guild.id) recent_manually_archived_thread_ids = [] - async for thread_update in guild.audit_logs(limit=200, action=discord.AuditLogAction.thread_update): + async for thread_update in guild.audit_logs(limit=200, action=disnake.AuditLogAction.thread_update): if getattr(thread_update.after, "archived", False): recent_manually_archived_thread_ids.append(thread_update.target.id) @@ -58,7 +58,7 @@ class ThreadBumper(commands.Cog): for thread_id, _ in await self.threads_to_bump.items(): try: thread = await channel.get_or_fetch_channel(thread_id) - except discord.NotFound: + except disnake.NotFound: log.info("Thread %d has been deleted, removing from bumped threads.", thread_id) await self.threads_to_bump.delete(thread_id) continue @@ -75,12 +75,12 @@ class ThreadBumper(commands.Cog): await ctx.send_help(ctx.command) @thread_bump_group.command(name="add", aliases=("a",)) - async def add_thread_to_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None: + async def add_thread_to_bump_list(self, ctx: commands.Context, thread: t.Optional[disnake.Thread]) -> None: """Add a thread to the bump list.""" await self.init_task if not thread: - if isinstance(ctx.channel, discord.Thread): + if isinstance(ctx.channel, disnake.Thread): thread = ctx.channel else: raise commands.BadArgument("You must provide a thread, or run this command within a thread.") @@ -92,12 +92,12 @@ class ThreadBumper(commands.Cog): await ctx.send(f":ok_hand:{thread.mention} has been added to the bump list.") @thread_bump_group.command(name="remove", aliases=("r", "rem", "d", "del", "delete")) - async def remove_thread_from_bump_list(self, ctx: commands.Context, thread: t.Optional[discord.Thread]) -> None: + async def remove_thread_from_bump_list(self, ctx: commands.Context, thread: t.Optional[disnake.Thread]) -> None: """Remove a thread from the bump list.""" await self.init_task if not thread: - if isinstance(ctx.channel, discord.Thread): + if isinstance(ctx.channel, disnake.Thread): thread = ctx.channel else: raise commands.BadArgument("You must provide a thread, or run this command within a thread.") @@ -114,14 +114,14 @@ class ThreadBumper(commands.Cog): await self.init_task lines = [f"<#{k}>" for k, _ in await self.threads_to_bump.items()] - embed = discord.Embed( + embed = disnake.Embed( title="Threads in the bump list", colour=constants.Colours.blue ) await LinePaginator.paginate(lines, ctx, embed) @commands.Cog.listener() - async def on_thread_update(self, _: discord.Thread, after: discord.Thread) -> None: + async def on_thread_update(self, _: disnake.Thread, after: disnake.Thread) -> None: """ Listen for thread updates and check if the thread has been archived. diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py index 2a074788e..77be3315c 100644 --- a/bot/exts/utils/utils.py +++ b/bot/exts/utils/utils.py @@ -3,9 +3,9 @@ import re import unicodedata from typing import Tuple, Union -from discord import Colour, Embed, utils -from discord.ext.commands import BadArgument, Cog, Context, clean_content, command, has_any_role -from discord.utils import snowflake_time +from disnake import Colour, Embed, utils +from disnake.ext.commands import BadArgument, Cog, Context, clean_content, command, has_any_role +from disnake.utils import snowflake_time from bot.bot import Bot from bot.constants import Channels, MODERATION_ROLES, Roles, STAFF_PARTNERS_COMMUNITY_ROLES diff --git a/bot/log.py b/bot/log.py index 100cd06f6..0b1d1aca6 100644 --- a/bot/log.py +++ b/bot/log.py @@ -74,7 +74,7 @@ def setup() -> None: coloredlogs.install(level=TRACE_LEVEL, logger=root_log, stream=sys.stdout) root_log.setLevel(logging.DEBUG if constants.DEBUG_MODE else logging.INFO) - get_logger("discord").setLevel(logging.WARNING) + get_logger("disnake").setLevel(logging.WARNING) get_logger("websockets").setLevel(logging.WARNING) get_logger("chardet").setLevel(logging.WARNING) get_logger("async_rediscache").setLevel(logging.WARNING) diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py index 4840fa454..590be22a2 100644 --- a/bot/monkey_patches.py +++ b/bot/monkey_patches.py @@ -2,8 +2,8 @@ import re from datetime import timedelta import arrow -from discord import Forbidden, http -from discord.ext import commands +from disnake import Forbidden, http +from disnake.ext import commands from bot.log import get_logger @@ -13,7 +13,7 @@ MESSAGE_ID_RE = re.compile(r'(?P[0-9]{15,20})$') class Command(commands.Command): """ - A `discord.ext.commands.Command` subclass which supports root aliases. + A `disnake.ext.commands.Command` subclass which supports root aliases. A `root_aliases` keyword argument is added, which is a sequence of alias names that will act as top-level commands rather than being aliases of the command's group. It's stored as an attribute diff --git a/bot/pagination.py b/bot/pagination.py index 8f4353eb1..1a014daa1 100644 --- a/bot/pagination.py +++ b/bot/pagination.py @@ -3,9 +3,9 @@ import typing as t from contextlib import suppress from functools import partial -import discord -from discord.abc import User -from discord.ext.commands import Context, Paginator +import disnake +from disnake.abc import User +from disnake.ext.commands import Context, Paginator from bot import constants from bot.log import get_logger @@ -55,7 +55,7 @@ class LinePaginator(Paginator): linesep: str = "\n" ) -> None: """ - This function overrides the Paginator.__init__ from inside discord.ext.commands. + This function overrides the Paginator.__init__ from inside disnake.ext.commands. It overrides in order to allow us to configure the maximum number of lines per page. """ @@ -99,7 +99,7 @@ class LinePaginator(Paginator): effort to avoid breaking up single lines across pages, while keeping the total length of the page at a reasonable size. - This function overrides the `Paginator.add_line` from inside `discord.ext.commands`. + This function overrides the `Paginator.add_line` from inside `disnake.ext.commands`. It overrides in order to allow us to configure the maximum number of lines per page. """ @@ -192,7 +192,7 @@ class LinePaginator(Paginator): cls, lines: t.List[str], ctx: Context, - embed: discord.Embed, + embed: disnake.Embed, prefix: str = "", suffix: str = "", max_lines: t.Optional[int] = None, @@ -204,7 +204,7 @@ class LinePaginator(Paginator): footer_text: str = None, url: str = None, exception_on_empty_embed: bool = False, - ) -> t.Optional[discord.Message]: + ) -> t.Optional[disnake.Message]: """ Use a paginator and set of reactions to provide pagination over a set of lines. @@ -219,7 +219,7 @@ class LinePaginator(Paginator): to any user with a moderation role. Example: - >>> embed = discord.Embed() + >>> embed = disnake.Embed() >>> embed.set_author(name="Some Operation", url=url, icon_url=icon) >>> await LinePaginator.paginate([line for line in lines], ctx, embed) """ @@ -367,5 +367,5 @@ class LinePaginator(Paginator): await message.edit(embed=embed) log.debug("Ending pagination and clearing reactions.") - with suppress(discord.NotFound): + with suppress(disnake.NotFound): await message.clear_reactions() diff --git a/bot/rules/attachments.py b/bot/rules/attachments.py index 8903c385c..9c890e569 100644 --- a/bot/rules/attachments.py +++ b/bot/rules/attachments.py @@ -1,6 +1,6 @@ from typing import Dict, Iterable, List, Optional, Tuple -from discord import Member, Message +from disnake import Member, Message async def apply( diff --git a/bot/rules/burst.py b/bot/rules/burst.py index 25c5a2f33..a943cfdeb 100644 --- a/bot/rules/burst.py +++ b/bot/rules/burst.py @@ -1,6 +1,6 @@ from typing import Dict, Iterable, List, Optional, Tuple -from discord import Member, Message +from disnake import Member, Message async def apply( diff --git a/bot/rules/burst_shared.py b/bot/rules/burst_shared.py index bbe9271b3..dee857e18 100644 --- a/bot/rules/burst_shared.py +++ b/bot/rules/burst_shared.py @@ -1,6 +1,6 @@ from typing import Dict, Iterable, List, Optional, Tuple -from discord import Member, Message +from disnake import Member, Message async def apply( diff --git a/bot/rules/chars.py b/bot/rules/chars.py index 1f587422c..6d2f6eb83 100644 --- a/bot/rules/chars.py +++ b/bot/rules/chars.py @@ -1,6 +1,6 @@ from typing import Dict, Iterable, List, Optional, Tuple -from discord import Member, Message +from disnake import Member, Message async def apply( diff --git a/bot/rules/discord_emojis.py b/bot/rules/discord_emojis.py index d979ac5e7..4fe4e88f9 100644 --- a/bot/rules/discord_emojis.py +++ b/bot/rules/discord_emojis.py @@ -1,7 +1,7 @@ import re from typing import Dict, Iterable, List, Optional, Tuple -from discord import Member, Message +from disnake import Member, Message from emoji import demojize DISCORD_EMOJI_RE = re.compile(r"<:\w+:\d+>|:\w+:") diff --git a/bot/rules/duplicates.py b/bot/rules/duplicates.py index 8e4fbc12d..77e393db0 100644 --- a/bot/rules/duplicates.py +++ b/bot/rules/duplicates.py @@ -1,6 +1,6 @@ from typing import Dict, Iterable, List, Optional, Tuple -from discord import Member, Message +from disnake import Member, Message async def apply( diff --git a/bot/rules/links.py b/bot/rules/links.py index c46b783c5..92c13b3f4 100644 --- a/bot/rules/links.py +++ b/bot/rules/links.py @@ -1,7 +1,7 @@ import re from typing import Dict, Iterable, List, Optional, Tuple -from discord import Member, Message +from disnake import Member, Message LINK_RE = re.compile(r"(https?://[^\s]+)") diff --git a/bot/rules/mentions.py b/bot/rules/mentions.py index 6f5addad1..7ee66be31 100644 --- a/bot/rules/mentions.py +++ b/bot/rules/mentions.py @@ -1,6 +1,6 @@ from typing import Dict, Iterable, List, Optional, Tuple -from discord import Member, Message +from disnake import Member, Message async def apply( diff --git a/bot/rules/newlines.py b/bot/rules/newlines.py index 4e66e1359..45266648e 100644 --- a/bot/rules/newlines.py +++ b/bot/rules/newlines.py @@ -1,7 +1,7 @@ import re from typing import Dict, Iterable, List, Optional, Tuple -from discord import Member, Message +from disnake import Member, Message async def apply( diff --git a/bot/rules/role_mentions.py b/bot/rules/role_mentions.py index 0649540b6..1f7a6a74d 100644 --- a/bot/rules/role_mentions.py +++ b/bot/rules/role_mentions.py @@ -1,6 +1,6 @@ from typing import Dict, Iterable, List, Optional, Tuple -from discord import Member, Message +from disnake import Member, Message async def apply( diff --git a/bot/utils/channel.py b/bot/utils/channel.py index 954a10e56..ee0c87311 100644 --- a/bot/utils/channel.py +++ b/bot/utils/channel.py @@ -1,6 +1,6 @@ from typing import Union -import discord +import disnake import bot from bot import constants @@ -10,7 +10,7 @@ from bot.log import get_logger log = get_logger(__name__) -def is_help_channel(channel: discord.TextChannel) -> bool: +def is_help_channel(channel: disnake.TextChannel) -> bool: """Return True if `channel` is in one of the help categories (excluding dormant).""" log.trace(f"Checking if #{channel} is a help channel.") categories = (Categories.help_available, Categories.help_in_use) @@ -18,9 +18,9 @@ def is_help_channel(channel: discord.TextChannel) -> bool: return any(is_in_category(channel, category) for category in categories) -def is_mod_channel(channel: Union[discord.TextChannel, discord.Thread]) -> bool: +def is_mod_channel(channel: Union[disnake.TextChannel, disnake.Thread]) -> bool: """True if channel, or channel.parent for threads, is considered a mod channel.""" - if isinstance(channel, discord.Thread): + if isinstance(channel, disnake.Thread): channel = channel.parent if channel.id in constants.MODERATION_CHANNELS: @@ -36,11 +36,11 @@ def is_mod_channel(channel: Union[discord.TextChannel, discord.Thread]) -> bool: return False -def is_staff_channel(channel: discord.TextChannel) -> bool: +def is_staff_channel(channel: disnake.TextChannel) -> bool: """True if `channel` is considered a staff channel.""" guild = bot.instance.get_guild(constants.Guild.id) - if channel.type is discord.ChannelType.category: + if channel.type is disnake.ChannelType.category: return False # Channel is staff-only if staff have explicit read allow perms @@ -52,12 +52,12 @@ def is_staff_channel(channel: discord.TextChannel) -> bool: ) -def is_in_category(channel: discord.TextChannel, category_id: int) -> bool: +def is_in_category(channel: disnake.TextChannel, category_id: int) -> bool: """Return True if `channel` is within a category with `category_id`.""" return getattr(channel, "category_id", None) == category_id -async def get_or_fetch_channel(channel_id: int) -> discord.abc.GuildChannel: +async def get_or_fetch_channel(channel_id: int) -> disnake.abc.GuildChannel: """Attempt to get or fetch a channel and return it.""" log.trace(f"Getting the channel {channel_id}.") diff --git a/bot/utils/checks.py b/bot/utils/checks.py index 188285684..9aa9bdc14 100644 --- a/bot/utils/checks.py +++ b/bot/utils/checks.py @@ -1,6 +1,6 @@ from typing import Callable, Container, Iterable, Optional, Union -from discord.ext.commands import ( +from disnake.ext.commands import ( BucketType, CheckFailure, Cog, Command, CommandOnCooldown, Context, Cooldown, CooldownMapping, NoPrivateMessage, has_any_role ) @@ -135,7 +135,7 @@ def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketTy if any(role.id in bypass for role in ctx.author.roles): return - # cooldown logic, taken from discord.py internals + # cooldown logic, taken from disnake's internals current = ctx.message.created_at.timestamp() bucket = buckets.get_bucket(ctx.message) retry_after = bucket.update_rate_limit(current) diff --git a/bot/utils/function.py b/bot/utils/function.py index 55115d7d3..bb6d8afe3 100644 --- a/bot/utils/function.py +++ b/bot/utils/function.py @@ -94,7 +94,7 @@ def update_wrapper_globals( """ Update globals of `wrapper` with the globals from `wrapped`. - For forwardrefs in command annotations discordpy uses the __global__ attribute of the function + For forwardrefs in command annotations disnake uses the __global__ attribute of the function to resolve their values, with decorators that replace the function this breaks because they have their own globals. @@ -103,7 +103,7 @@ def update_wrapper_globals( An exception will be raised in case `wrapper` and `wrapped` share a global name that is used by `wrapped`'s typehints and is not in `ignored_conflict_names`, - as this can cause incorrect objects being used by discordpy's converters. + as this can cause incorrect objects being used by disnake's converters. """ annotation_global_names = ( ann.split(".", maxsplit=1)[0] for ann in wrapped.__annotations__.values() if isinstance(ann, str) @@ -136,7 +136,7 @@ def command_wraps( *, ignored_conflict_names: t.Set[str] = frozenset(), ) -> t.Callable[[types.FunctionType], types.FunctionType]: - """Update the decorated function to look like `wrapped` and update globals for discordpy forwardref evaluation.""" + """Update the decorated function to look like `wrapped` and update globals for disnake forwardref evaluation.""" def decorator(wrapper: types.FunctionType) -> types.FunctionType: return functools.update_wrapper( update_wrapper_globals(wrapper, wrapped, ignored_conflict_names=ignored_conflict_names), diff --git a/bot/utils/helpers.py b/bot/utils/helpers.py index 3501a3933..859f53fdb 100644 --- a/bot/utils/helpers.py +++ b/bot/utils/helpers.py @@ -1,7 +1,7 @@ from abc import ABCMeta from typing import Optional -from discord.ext.commands import CogMeta +from disnake.ext.commands import CogMeta class CogABCMeta(CogMeta, ABCMeta): diff --git a/bot/utils/members.py b/bot/utils/members.py index 693286045..d46baae5b 100644 --- a/bot/utils/members.py +++ b/bot/utils/members.py @@ -1,13 +1,13 @@ import typing as t -import discord +import disnake from bot.log import get_logger log = get_logger(__name__) -async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> t.Optional[discord.Member]: +async def get_or_fetch_member(guild: disnake.Guild, member_id: int) -> t.Optional[disnake.Member]: """ Attempt to get a member from cache; on failure fetch from the API. @@ -18,7 +18,7 @@ async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> t.Optiona else: try: member = await guild.fetch_member(member_id) - except discord.errors.NotFound: + except disnake.errors.NotFound: log.trace("Failed to fetch %d from API.", member_id) return None log.trace("%s fetched from API.", member) @@ -26,23 +26,23 @@ async def get_or_fetch_member(guild: discord.Guild, member_id: int) -> t.Optiona async def handle_role_change( - member: discord.Member, + member: disnake.Member, coro: t.Callable[..., t.Coroutine], - role: discord.Role + role: disnake.Role ) -> None: """ Change `member`'s cooldown role via awaiting `coro` and handle errors. - `coro` is intended to be `discord.Member.add_roles` or `discord.Member.remove_roles`. + `coro` is intended to be `disnake.Member.add_roles` or `disnake.Member.remove_roles`. """ try: await coro(role) - except discord.NotFound: + except disnake.NotFound: log.debug(f"Failed to change role for {member} ({member.id}): member not found") - except discord.Forbidden: + except disnake.Forbidden: log.debug( f"Forbidden to change role for {member} ({member.id}); " f"possibly due to role hierarchy" ) - except discord.HTTPException as e: + except disnake.HTTPException as e: log.error(f"Failed to change role for {member} ({member.id}): {e.status} {e.code}") diff --git a/bot/utils/message_cache.py b/bot/utils/message_cache.py index f68d280c9..edf2111e9 100644 --- a/bot/utils/message_cache.py +++ b/bot/utils/message_cache.py @@ -1,7 +1,7 @@ import typing as t from math import ceil -from discord import Message +from disnake import Message class MessageCache: diff --git a/bot/utils/messages.py b/bot/utils/messages.py index e55c07062..0bdb00a29 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -5,8 +5,8 @@ from functools import partial from io import BytesIO from typing import Callable, List, Optional, Sequence, Union -import discord -from discord.ext.commands import Context +import disnake +from disnake.ext.commands import Context import bot from bot.constants import Emojis, MODERATION_ROLES, NEGATIVE_REPLIES @@ -17,8 +17,8 @@ log = get_logger(__name__) def reaction_check( - reaction: discord.Reaction, - user: discord.abc.User, + reaction: disnake.Reaction, + user: disnake.abc.User, *, message_id: int, allowed_emoji: Sequence[str], @@ -51,14 +51,14 @@ def reaction_check( log.trace(f"Removing reaction {reaction} by {user} on {reaction.message.id}: disallowed user.") scheduling.create_task( reaction.message.remove_reaction(reaction.emoji, user), - suppressed_exceptions=(discord.HTTPException,), + suppressed_exceptions=(disnake.HTTPException,), name=f"remove_reaction-{reaction}-{reaction.message.id}-{user}" ) return False async def wait_for_deletion( - message: discord.Message, + message: disnake.Message, user_ids: Sequence[int], deletion_emojis: Sequence[str] = (Emojis.trashcan,), timeout: float = 60 * 5, @@ -82,7 +82,7 @@ async def wait_for_deletion( for emoji in deletion_emojis: try: await message.add_reaction(emoji) - except discord.NotFound: + except disnake.NotFound: log.trace(f"Aborting wait_for_deletion: message {message.id} deleted prematurely.") return @@ -101,13 +101,13 @@ async def wait_for_deletion( await message.clear_reactions() else: await message.delete() - except discord.NotFound: + except disnake.NotFound: log.trace(f"wait_for_deletion: message {message.id} deleted prematurely.") async def send_attachments( - message: discord.Message, - destination: Union[discord.TextChannel, discord.Webhook], + message: disnake.Message, + destination: Union[disnake.TextChannel, disnake.Webhook], link_large: bool = True, use_cached: bool = False, **kwargs @@ -140,9 +140,9 @@ async def send_attachments( if attachment.size <= destination.guild.filesize_limit - 512: with BytesIO() as file: await attachment.save(file, use_cached=use_cached) - attachment_file = discord.File(file, filename=attachment.filename) + attachment_file = disnake.File(file, filename=attachment.filename) - if isinstance(destination, discord.TextChannel): + if isinstance(destination, disnake.TextChannel): msg = await destination.send(file=attachment_file, **kwargs) urls.append(msg.attachments[0].url) else: @@ -151,7 +151,7 @@ async def send_attachments( large.append(attachment) else: log.info(f"{failure_msg} because it's too large.") - except discord.HTTPException as e: + except disnake.HTTPException as e: if link_large and e.status == 413: large.append(attachment) else: @@ -159,10 +159,10 @@ async def send_attachments( if link_large and large: desc = "\n".join(f"[{attachment.filename}]({attachment.url})" for attachment in large) - embed = discord.Embed(description=desc) + embed = disnake.Embed(description=desc) embed.set_footer(text="Attachments exceed upload size limit.") - if isinstance(destination, discord.TextChannel): + if isinstance(destination, disnake.TextChannel): await destination.send(embed=embed, **kwargs) else: await destination.send(embed=embed, **webhook_send_kwargs) @@ -171,9 +171,9 @@ async def send_attachments( async def count_unique_users_reaction( - message: discord.Message, - reaction_predicate: Callable[[discord.Reaction], bool] = lambda _: True, - user_predicate: Callable[[discord.User], bool] = lambda _: True, + message: disnake.Message, + reaction_predicate: Callable[[disnake.Reaction], bool] = lambda _: True, + user_predicate: Callable[[disnake.User], bool] = lambda _: True, count_bots: bool = True ) -> int: """ @@ -193,7 +193,7 @@ async def count_unique_users_reaction( return len(unique_users) -async def pin_no_system_message(message: discord.Message) -> bool: +async def pin_no_system_message(message: disnake.Message) -> bool: """Pin the given message, wait a couple of seconds and try to delete the system message.""" await message.pin() @@ -201,7 +201,7 @@ async def pin_no_system_message(message: discord.Message) -> bool: await asyncio.sleep(2) # Search for the system message in the last 10 messages async for historical_message in message.channel.history(limit=10): - if historical_message.type == discord.MessageType.pins_add: + if historical_message.type == disnake.MessageType.pins_add: await historical_message.delete() return True @@ -225,16 +225,16 @@ def sub_clyde(username: Optional[str]) -> Optional[str]: return username # Empty string or None -async def send_denial(ctx: Context, reason: str) -> discord.Message: +async def send_denial(ctx: Context, reason: str) -> disnake.Message: """Send an embed denying the user with the given reason.""" - embed = discord.Embed() - embed.colour = discord.Colour.red() + embed = disnake.Embed() + embed.colour = disnake.Colour.red() embed.title = random.choice(NEGATIVE_REPLIES) embed.description = reason return await ctx.send(embed=embed) -def format_user(user: discord.abc.User) -> str: +def format_user(user: disnake.abc.User) -> str: """Return a string for `user` which has their mention and ID.""" return f"{user.mention} (`{user.id}`)" diff --git a/bot/utils/webhooks.py b/bot/utils/webhooks.py index 9c916b63a..8ef929b79 100644 --- a/bot/utils/webhooks.py +++ b/bot/utils/webhooks.py @@ -1,7 +1,7 @@ from typing import Optional -import discord -from discord import Embed +import disnake +from disnake import Embed from bot.log import get_logger from bot.utils.messages import sub_clyde @@ -10,13 +10,13 @@ log = get_logger(__name__) async def send_webhook( - webhook: discord.Webhook, + webhook: disnake.Webhook, content: Optional[str] = None, username: Optional[str] = None, avatar_url: Optional[str] = None, embed: Optional[Embed] = None, wait: Optional[bool] = False -) -> discord.Message: +) -> disnake.Message: """ Send a message using the provided webhook. @@ -30,5 +30,5 @@ async def send_webhook( embed=embed, wait=wait, ) - except discord.HTTPException: + except disnake.HTTPException: log.exception("Failed to send a message to the webhook!") diff --git a/poetry.lock b/poetry.lock index a8ee6ef5c..087fd739c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -143,7 +143,7 @@ lxml = ["lxml"] [[package]] name = "bot-core" -version = "1.2.0" +version = "2.1.0" description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community." category = "main" optional = false @@ -154,7 +154,7 @@ python-versions = "3.9.*" [package.source] type = "url" -url = "https://github.com/python-discord/bot-core/archive/511bcba1b0196cd498c707a525ea56921bd971db.zip" +url = "https://github.com/python-discord/bot-core/archive/refs/tags/v2.1.0.zip" [[package]] name = "certifi" version = "2021.10.8" @@ -1074,7 +1074,7 @@ toml = ">=0.10.0,<0.11.0" [[package]] name = "testfixtures" -version = "6.18.3" +version = "6.18.5" description = "A collection of helpers and mock objects for unit tests and doc tests." category = "dev" optional = false @@ -1169,7 +1169,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "538a4809b9fc6fa93ee1baccf4016515ae311a886f1b7ec9b3d544bb87c830a3" +content-hash = "b8b28311c13f7a66f028041bae889131d3916ca7f667c9a7539871d21bbcd077" [metadata.files] aio-pika = [ @@ -1990,8 +1990,8 @@ taskipy = [ {file = "taskipy-1.7.0.tar.gz", hash = "sha256:960e480b1004971e76454ecd1a0484e640744a30073a1069894a311467f85ed8"}, ] testfixtures = [ - {file = "testfixtures-6.18.3-py2.py3-none-any.whl", hash = "sha256:6ddb7f56a123e1a9339f130a200359092bd0a6455e31838d6c477e8729bb7763"}, - {file = "testfixtures-6.18.3.tar.gz", hash = "sha256:2600100ae96ffd082334b378e355550fef8b4a529a6fa4c34f47130905c7426d"}, + {file = "testfixtures-6.18.5-py2.py3-none-any.whl", hash = "sha256:7de200e24f50a4a5d6da7019fb1197aaf5abd475efb2ec2422fdcf2f2eb98c1d"}, + {file = "testfixtures-6.18.5.tar.gz", hash = "sha256:02dae883f567f5b70fd3ad3c9eefb95912e78ac90be6c7444b5e2f46bf572c84"}, ] tldextract = [ {file = "tldextract-3.1.2-py2.py3-none-any.whl", hash = "sha256:f55e05f6bf4cc952a87d13594386d32ad2dd265630a8bdfc3df03bd60425c6b0"}, diff --git a/pyproject.toml b/pyproject.toml index 90b38ce66..1f02818ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" python = "3.9.*" disnake = "~=2.4" # See https://bot-core.pythondiscord.com/ for docs. -bot-core = {url = "https://github.com/python-discord/bot-core/archive/511bcba1b0196cd498c707a525ea56921bd971db.zip"} +bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v2.1.0.zip"} aio-pika = "~=6.1" aiodns = "~=2.0" aiohttp = "~=3.7" diff --git a/tests/README.md b/tests/README.md index b7fddfaa2..fc03b3d43 100644 --- a/tests/README.md +++ b/tests/README.md @@ -121,9 +121,9 @@ As we are trying to test our "units" of code independently, we want to make sure However, the features that we are trying to test often depend on those objects generated by external pieces of code. It would be difficult to test a bot command without having access to a `Context` instance. Fortunately, there's a solution for that: we use fake objects that act like the true object. We call these fake objects "mocks". -To create these mock object, we mainly use the [`unittest.mock`](https://docs.python.org/3/library/unittest.mock.html) module. In addition, we have also defined a couple of specialized mock objects that mock specific `discord.py` types (see the section on the below.). +To create these mock object, we mainly use the [`unittest.mock`](https://docs.python.org/3/library/unittest.mock.html) module. In addition, we have also defined a couple of specialized mock objects that mock specific `disnake` types (see the section on the below.). -An example of mocking is when we provide a command with a mocked version of `discord.ext.commands.Context` object instead of a real `Context` object. This makes sure we can then check (_assert_) if the `send` method of the mocked Context object was called with the correct message content (without having to send a real message to the Discord API!): +An example of mocking is when we provide a command with a mocked version of `disnake.ext.commands.Context` object instead of a real `Context` object. This makes sure we can then check (_assert_) if the `send` method of the mocked Context object was called with the correct message content (without having to send a real message to the Discord API!): ```py import asyncio @@ -152,15 +152,15 @@ class BotCogTests(unittest.TestCase): By default, the `unittest.mock.Mock` and `unittest.mock.MagicMock` classes cannot mock coroutines, since the `__call__` method they provide is synchronous. The [`AsyncMock`](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.AsyncMock) that has been [introduced in Python 3.8](https://docs.python.org/3.9/whatsnew/3.8.html#unittest) is an asynchronous version of `MagicMock` that can be used anywhere a coroutine is expected. -### Special mocks for some `discord.py` types +### Special mocks for some `disnake` types To quote Ned Batchelder, Mock objects are "automatic chameleons". This means that they will happily allow the access to any attribute or method and provide a mocked value in return. One downside to this is that if the code you are testing gets the name of the attribute wrong, your mock object will not complain and the test may still pass. -In order to avoid that, we have defined a number of Mock types in [`helpers.py`](/tests/helpers.py) that follow the specifications of the actual Discord types they are mocking. This means that trying to access an attribute or method on a mocked object that does not exist on the equivalent `discord.py` object will result in an `AttributeError`. In addition, these mocks have some sensible defaults and **pass `isinstance` checks for the types they are mocking**. +In order to avoid that, we have defined a number of Mock types in [`helpers.py`](/tests/helpers.py) that follow the specifications of the actual disnake types they are mocking. This means that trying to access an attribute or method on a mocked object that does not exist on the equivalent `disnake` object will result in an `AttributeError`. In addition, these mocks have some sensible defaults and **pass `isinstance` checks for the types they are mocking**. These special mocks are added when they are needed, so if you think it would be sensible to add another one, feel free to propose one in your PR. -**Note:** These mock types only "know" the attributes that are set by default when these `discord.py` types are first initialized. If you need to work with dynamically set attributes that are added after initialization, you can still explicitly mock them: +**Note:** These mock types only "know" the attributes that are set by default when these `disnake` types are first initialized. If you need to work with dynamically set attributes that are added after initialization, you can still explicitly mock them: ```py import unittest.mock @@ -245,7 +245,7 @@ All in all, it's not only important to consider if all statements or branches we ### Unit Testing vs Integration Testing -Another restriction of unit testing is that it tests, well, in units. Even if we can guarantee that the units work as they should independently, we have no guarantee that they will actually work well together. Even more, while the mocking described above gives us a lot of flexibility in factoring out external code, we are work under the implicit assumption that we fully understand those external parts and utilize it correctly. What if our mocked `Context` object works with a `send` method, but `discord.py` has changed it to a `send_message` method in a recent update? It could mean our tests are passing, but the code it's testing still doesn't work in production. +Another restriction of unit testing is that it tests, well, in units. Even if we can guarantee that the units work as they should independently, we have no guarantee that they will actually work well together. Even more, while the mocking described above gives us a lot of flexibility in factoring out external code, we are work under the implicit assumption that we fully understand those external parts and utilize it correctly. What if our mocked `Context` object works with a `send` method, but `disnake` has changed it to a `send_message` method in a recent update? It could mean our tests are passing, but the code it's testing still doesn't work in production. The answer to this is that we also need to make sure that the individual parts come together into a working application. In addition, we will also need to make sure that the application communicates correctly with external applications. Since we currently have no automated integration tests or functional tests, that means **it's still very important to fire up the bot and test the code you've written manually** in addition to the unit tests you've written. diff --git a/tests/base.py b/tests/base.py index 5e304ea9d..dea7dd678 100644 --- a/tests/base.py +++ b/tests/base.py @@ -3,8 +3,8 @@ import unittest from contextlib import contextmanager from typing import Dict -import discord -from discord.ext import commands +import disnake +from disnake.ext import commands from bot.log import get_logger from tests import helpers @@ -80,7 +80,7 @@ class LoggingTestsMixin: class CommandTestCase(unittest.IsolatedAsyncioTestCase): - """TestCase with additional assertions that are useful for testing Discord commands.""" + """TestCase with additional assertions that are useful for testing disnake commands.""" async def assertHasPermissionsCheck( # noqa: N802 self, @@ -98,7 +98,7 @@ class CommandTestCase(unittest.IsolatedAsyncioTestCase): permissions = {k: not v for k, v in permissions.items()} ctx = helpers.MockContext() - ctx.channel.permissions_for.return_value = discord.Permissions(**permissions) + ctx.channel.permissions_for.return_value = disnake.Permissions(**permissions) with self.assertRaises(commands.MissingPermissions) as cm: await cmd.can_run(ctx) diff --git a/tests/bot/exts/backend/sync/test_cog.py b/tests/bot/exts/backend/sync/test_cog.py index fdd0ab74a..4ed7de64d 100644 --- a/tests/bot/exts/backend/sync/test_cog.py +++ b/tests/bot/exts/backend/sync/test_cog.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -import discord +import disnake from bot import constants from bot.api import ResponseCodeError @@ -257,9 +257,9 @@ class SyncCogListenerTests(SyncCogTestCase): self.assertTrue(self.cog.on_member_update.__cog_listener__) subtests = ( - ("activities", discord.Game("Pong"), discord.Game("Frogger")), + ("activities", disnake.Game("Pong"), disnake.Game("Frogger")), ("nick", "old nick", "new nick"), - ("status", discord.Status.online, discord.Status.offline), + ("status", disnake.Status.online, disnake.Status.offline), ) for attribute, old_value, new_value in subtests: diff --git a/tests/bot/exts/backend/sync/test_roles.py b/tests/bot/exts/backend/sync/test_roles.py index 541074336..9ecb8fae0 100644 --- a/tests/bot/exts/backend/sync/test_roles.py +++ b/tests/bot/exts/backend/sync/test_roles.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -import discord +import disnake from bot.exts.backend.sync._syncers import RoleSyncer, _Diff, _Role from tests import helpers @@ -34,8 +34,8 @@ class RoleSyncerDiffTests(unittest.IsolatedAsyncioTestCase): for role in roles: mock_role = helpers.MockRole(**role) - mock_role.colour = discord.Colour(role["colour"]) - mock_role.permissions = discord.Permissions(role["permissions"]) + mock_role.colour = disnake.Colour(role["colour"]) + mock_role.permissions = disnake.Permissions(role["permissions"]) guild.roles.append(mock_role) return guild diff --git a/tests/bot/exts/backend/sync/test_users.py b/tests/bot/exts/backend/sync/test_users.py index 2fc97af2d..f55f5360f 100644 --- a/tests/bot/exts/backend/sync/test_users.py +++ b/tests/bot/exts/backend/sync/test_users.py @@ -1,7 +1,7 @@ import unittest from unittest import mock -from discord.errors import NotFound +from disnake.errors import NotFound from bot.exts.backend.sync._syncers import UserSyncer, _Diff from tests import helpers diff --git a/tests/bot/exts/backend/test_error_handler.py b/tests/bot/exts/backend/test_error_handler.py index 35fa0ee59..83b5f2749 100644 --- a/tests/bot/exts/backend/test_error_handler.py +++ b/tests/bot/exts/backend/test_error_handler.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import AsyncMock, MagicMock, call, patch -from discord.ext.commands import errors +from disnake.ext.commands import errors from bot.api import ResponseCodeError from bot.errors import InvalidInfractedUserError, LockedResourceError diff --git a/tests/bot/exts/events/test_code_jams.py b/tests/bot/exts/events/test_code_jams.py index 0856546af..fdff36b61 100644 --- a/tests/bot/exts/events/test_code_jams.py +++ b/tests/bot/exts/events/test_code_jams.py @@ -1,8 +1,8 @@ import unittest from unittest.mock import AsyncMock, MagicMock, create_autospec, patch -from discord import CategoryChannel -from discord.ext.commands import BadArgument +from disnake import CategoryChannel +from disnake.ext.commands import BadArgument from bot.constants import Roles from bot.exts.events import code_jams diff --git a/tests/bot/exts/filters/test_antimalware.py b/tests/bot/exts/filters/test_antimalware.py index 06d78de9d..0cab405d0 100644 --- a/tests/bot/exts/filters/test_antimalware.py +++ b/tests/bot/exts/filters/test_antimalware.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import AsyncMock, Mock -from discord import NotFound +from disnake import NotFound from bot.constants import Channels, STAFF_ROLES from bot.exts.filters import antimalware diff --git a/tests/bot/exts/filters/test_security.py b/tests/bot/exts/filters/test_security.py index c0c3baa42..46fa82fd7 100644 --- a/tests/bot/exts/filters/test_security.py +++ b/tests/bot/exts/filters/test_security.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import MagicMock -from discord.ext.commands import NoPrivateMessage +from disnake.ext.commands import NoPrivateMessage from bot.exts.filters import security from tests.helpers import MockBot, MockContext diff --git a/tests/bot/exts/filters/test_token_remover.py b/tests/bot/exts/filters/test_token_remover.py index 4db27269a..dd56c10dd 100644 --- a/tests/bot/exts/filters/test_token_remover.py +++ b/tests/bot/exts/filters/test_token_remover.py @@ -3,7 +3,7 @@ from re import Match from unittest import mock from unittest.mock import MagicMock -from discord import Colour, NotFound +from disnake import Colour, NotFound from bot import constants from bot.exts.filters import token_remover diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py index d896b7652..9a35de7a9 100644 --- a/tests/bot/exts/info/test_information.py +++ b/tests/bot/exts/info/test_information.py @@ -3,7 +3,7 @@ import unittest import unittest.mock from datetime import datetime -import discord +import disnake from bot import constants from bot.exts.info import information @@ -43,7 +43,7 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase): embed = kwargs.pop('embed') self.assertEqual(embed.title, "Role information (Total 1 role)") - self.assertEqual(embed.colour, discord.Colour.og_blurple()) + self.assertEqual(embed.colour, disnake.Colour.og_blurple()) self.assertEqual(embed.description, f"\n`{self.moderator_role.id}` - {self.moderator_role.mention}\n") async def test_role_info_command(self): @@ -51,19 +51,19 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase): dummy_role = helpers.MockRole( name="Dummy", id=112233445566778899, - colour=discord.Colour.og_blurple(), + colour=disnake.Colour.og_blurple(), position=10, members=[self.ctx.author], - permissions=discord.Permissions(0) + permissions=disnake.Permissions(0) ) admin_role = helpers.MockRole( name="Admins", id=998877665544332211, - colour=discord.Colour.red(), + colour=disnake.Colour.red(), position=3, members=[self.ctx.author], - permissions=discord.Permissions(0), + permissions=disnake.Permissions(0), ) self.ctx.guild.roles.extend([dummy_role, admin_role]) @@ -81,7 +81,7 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase): admin_embed = admin_kwargs["embed"] self.assertEqual(dummy_embed.title, "Dummy info") - self.assertEqual(dummy_embed.colour, discord.Colour.og_blurple()) + self.assertEqual(dummy_embed.colour, disnake.Colour.og_blurple()) self.assertEqual(dummy_embed.fields[0].value, str(dummy_role.id)) self.assertEqual(dummy_embed.fields[1].value, f"#{dummy_role.colour.value:0>6x}") @@ -91,7 +91,7 @@ class InformationCogTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(dummy_embed.fields[5].value, "0") self.assertEqual(admin_embed.title, "Admins info") - self.assertEqual(admin_embed.colour, discord.Colour.red()) + self.assertEqual(admin_embed.colour, disnake.Colour.red()) class UserInfractionHelperMethodTests(unittest.IsolatedAsyncioTestCase): @@ -449,7 +449,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): user.created_at = user.joined_at = datetime.utcnow() embed = await self.cog.create_user_embed(ctx, user, False) - self.assertEqual(embed.colour, discord.Colour(100)) + self.assertEqual(embed.colour, disnake.Colour(100)) @unittest.mock.patch( f"{COG_PATH}.basic_user_infraction_counts", @@ -463,11 +463,11 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase): """The embed should be created with the og blurple colour if the user has no assigned roles.""" ctx = helpers.MockContext() - user = helpers.MockMember(id=217, colour=discord.Colour.default()) + user = helpers.MockMember(id=217, colour=disnake.Colour.default()) user.created_at = user.joined_at = datetime.utcnow() embed = await self.cog.create_user_embed(ctx, user, False) - self.assertEqual(embed.colour, discord.Colour.og_blurple()) + self.assertEqual(embed.colour, disnake.Colour.og_blurple()) @unittest.mock.patch( f"{COG_PATH}.basic_user_infraction_counts", diff --git a/tests/bot/exts/moderation/infraction/test_infractions.py b/tests/bot/exts/moderation/infraction/test_infractions.py index 052048053..b85d086c9 100644 --- a/tests/bot/exts/moderation/infraction/test_infractions.py +++ b/tests/bot/exts/moderation/infraction/test_infractions.py @@ -3,7 +3,7 @@ import textwrap import unittest from unittest.mock import ANY, AsyncMock, DEFAULT, MagicMock, Mock, patch -from discord.errors import NotFound +from disnake.errors import NotFound from bot.constants import Event from bot.exts.moderation.clean import Clean diff --git a/tests/bot/exts/moderation/infraction/test_utils.py b/tests/bot/exts/moderation/infraction/test_utils.py index 350274ecd..6601b9d25 100644 --- a/tests/bot/exts/moderation/infraction/test_utils.py +++ b/tests/bot/exts/moderation/infraction/test_utils.py @@ -3,7 +3,7 @@ from collections import namedtuple from datetime import datetime from unittest.mock import AsyncMock, MagicMock, call, patch -from discord import Embed, Forbidden, HTTPException, NotFound +from disnake import Embed, Forbidden, HTTPException, NotFound from bot.api import ResponseCodeError from bot.constants import Colours, Icons diff --git a/tests/bot/exts/moderation/test_incidents.py b/tests/bot/exts/moderation/test_incidents.py index cfe0c4b03..725455bbe 100644 --- a/tests/bot/exts/moderation/test_incidents.py +++ b/tests/bot/exts/moderation/test_incidents.py @@ -7,7 +7,7 @@ from unittest import mock from unittest.mock import AsyncMock, MagicMock, Mock, call, patch import aiohttp -import discord +import disnake from async_rediscache import RedisSession from bot.constants import Colours @@ -24,7 +24,7 @@ class MockAsyncIterable: Helper for mocking asynchronous for loops. It does not appear that the `unittest` library currently provides anything that would - allow us to simply mock an async iterator, such as `discord.TextChannel.history`. + allow us to simply mock an async iterator, such as `disnake.TextChannel.history`. We therefore write our own helper to wrap a regular synchronous iterable, and feed its values via `__anext__` rather than `__next__`. @@ -60,7 +60,7 @@ class MockSignal(enum.Enum): B = "B" -mock_404 = discord.NotFound( +mock_404 = disnake.NotFound( response=MagicMock(aiohttp.ClientResponse), # Mock the erroneous response message="Not found", ) @@ -70,8 +70,8 @@ class TestDownloadFile(unittest.IsolatedAsyncioTestCase): """Collection of tests for the `download_file` helper function.""" async def test_download_file_success(self): - """If `to_file` succeeds, function returns the acquired `discord.File`.""" - file = MagicMock(discord.File, filename="bigbadlemon.jpg") + """If `to_file` succeeds, function returns the acquired `disnake.File`.""" + file = MagicMock(disnake.File, filename="bigbadlemon.jpg") attachment = MockAttachment(to_file=AsyncMock(return_value=file)) acquired_file = await incidents.download_file(attachment) @@ -86,7 +86,7 @@ class TestDownloadFile(unittest.IsolatedAsyncioTestCase): async def test_download_file_fail(self): """If `to_file` fails on a non-404 error, function logs the exception & returns None.""" - arbitrary_error = discord.HTTPException(MagicMock(aiohttp.ClientResponse), "Arbitrary API error") + arbitrary_error = disnake.HTTPException(MagicMock(aiohttp.ClientResponse), "Arbitrary API error") attachment = MockAttachment(to_file=AsyncMock(side_effect=arbitrary_error)) with self.assertLogs(logger=incidents.log, level=logging.ERROR): @@ -121,7 +121,7 @@ class TestMakeEmbed(unittest.IsolatedAsyncioTestCase): async def test_make_embed_with_attachment_succeeds(self): """Incident's attachment is downloaded and displayed in the embed's image field.""" - file = MagicMock(discord.File, filename="bigbadjoe.jpg") + file = MagicMock(disnake.File, filename="bigbadjoe.jpg") attachment = MockAttachment(filename="bigbadjoe.jpg") incident = MockMessage(content="this is an incident", attachments=[attachment]) @@ -394,7 +394,7 @@ class TestArchive(TestIncidents): author=MockUser(name="author_name", display_avatar=Mock(url="author_avatar")), id=123, ) - built_embed = MagicMock(discord.Embed, id=123) # We patch `make_embed` to return this + built_embed = MagicMock(disnake.Embed, id=123) # We patch `make_embed` to return this with patch("bot.exts.moderation.incidents.make_embed", AsyncMock(return_value=(built_embed, None))): archive_return = await self.cog_instance.archive(incident, MagicMock(value="A"), MockMember()) @@ -616,7 +616,7 @@ class TestResolveMessage(TestIncidents): """ self.cog_instance.bot._connection._get_message = MagicMock(return_value=None) # Cache returns None - arbitrary_error = discord.HTTPException( + arbitrary_error = disnake.HTTPException( response=MagicMock(aiohttp.ClientResponse), message="Arbitrary error", ) @@ -649,7 +649,7 @@ class TestOnRawReactionAdd(TestIncidents): super().setUp() # Ensure `cog_instance` is assigned self.payload = MagicMock( - discord.RawReactionActionEvent, + disnake.RawReactionActionEvent, channel_id=123, # Patched at class level message_id=456, member=MockMember(bot=False), diff --git a/tests/bot/exts/moderation/test_modlog.py b/tests/bot/exts/moderation/test_modlog.py index 79e04837d..6c9ebed95 100644 --- a/tests/bot/exts/moderation/test_modlog.py +++ b/tests/bot/exts/moderation/test_modlog.py @@ -1,6 +1,6 @@ import unittest -import discord +import disnake from bot.exts.moderation.modlog import ModLog from tests.helpers import MockBot, MockTextChannel @@ -19,7 +19,7 @@ class ModLogTests(unittest.IsolatedAsyncioTestCase): self.bot.get_channel.return_value = self.channel await self.cog.send_log_message( icon_url="foo", - colour=discord.Colour.blue(), + colour=disnake.Colour.blue(), title="bar", text="foo bar" * 3000 ) diff --git a/tests/bot/exts/moderation/test_silence.py b/tests/bot/exts/moderation/test_silence.py index 92ce3418a..539651d6c 100644 --- a/tests/bot/exts/moderation/test_silence.py +++ b/tests/bot/exts/moderation/test_silence.py @@ -7,7 +7,7 @@ from unittest import mock from unittest.mock import AsyncMock, Mock from async_rediscache import RedisSession -from discord import PermissionOverwrite +from disnake import PermissionOverwrite from bot.constants import Channels, Guild, MODERATION_ROLES, Roles from bot.exts.moderation import silence @@ -152,7 +152,7 @@ class SilenceCogTests(unittest.IsolatedAsyncioTestCase): # It's too annoying to test cancel_all since it's a done callback and wrapped in a lambda. self.assertTrue(self.cog._init_task.cancelled()) - @autospec("discord.ext.commands", "has_any_role") + @autospec("disnake.ext.commands", "has_any_role") @mock.patch.object(silence.constants, "MODERATION_ROLES", new=(1, 2, 3)) async def test_cog_check(self, role_check): """Role check was called with `MODERATION_ROLES`""" diff --git a/tests/bot/exts/test_cogs.py b/tests/bot/exts/test_cogs.py index f8e120262..5cb071d58 100644 --- a/tests/bot/exts/test_cogs.py +++ b/tests/bot/exts/test_cogs.py @@ -8,7 +8,7 @@ from collections import defaultdict from types import ModuleType from unittest import mock -from discord.ext import commands +from disnake.ext import commands from bot import exts @@ -34,7 +34,7 @@ class CommandNameTests(unittest.TestCase): raise ImportError(name=name) # pragma: no cover # The mock prevents asyncio.get_event_loop() from being called. - with mock.patch("discord.ext.tasks.loop"): + with mock.patch("disnake.ext.tasks.loop"): prefix = f"{exts.__name__}." for module in pkgutil.walk_packages(exts.__path__, prefix, onerror=on_error): if not module.ispkg: diff --git a/tests/bot/exts/utils/test_snekbox.py b/tests/bot/exts/utils/test_snekbox.py index 8bdeedd27..bec7574fb 100644 --- a/tests/bot/exts/utils/test_snekbox.py +++ b/tests/bot/exts/utils/test_snekbox.py @@ -2,8 +2,8 @@ import asyncio import unittest from unittest.mock import AsyncMock, MagicMock, Mock, call, create_autospec, patch -from discord import AllowedMentions -from discord.ext import commands +from disnake import AllowedMentions +from disnake.ext import commands from bot import constants from bot.exts.utils import snekbox diff --git a/tests/bot/test_converters.py b/tests/bot/test_converters.py index 1bb678db2..afb8a973d 100644 --- a/tests/bot/test_converters.py +++ b/tests/bot/test_converters.py @@ -4,7 +4,7 @@ from datetime import MAXYEAR, datetime, timezone from unittest.mock import MagicMock, patch from dateutil.relativedelta import relativedelta -from discord.ext.commands import BadArgument +from disnake.ext.commands import BadArgument from bot.converters import Duration, HushDurationConverter, ISODateTime, PackageName diff --git a/tests/bot/utils/test_checks.py b/tests/bot/utils/test_checks.py index 4ae11d5d3..5675e10ec 100644 --- a/tests/bot/utils/test_checks.py +++ b/tests/bot/utils/test_checks.py @@ -1,7 +1,7 @@ import unittest from unittest.mock import MagicMock -from discord import DMChannel +from disnake import DMChannel from bot.utils import checks from bot.utils.checks import InWhitelistCheckFailure diff --git a/tests/helpers.py b/tests/helpers.py index 9d4988d23..bd1418ab9 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -7,9 +7,9 @@ import unittest.mock from asyncio import AbstractEventLoop from typing import Iterable, Optional -import discord +import disnake from aiohttp import ClientSession -from discord.ext.commands import Context +from disnake.ext.commands import Context from bot.api import APIClient from bot.async_stats import AsyncStatsClient @@ -26,11 +26,11 @@ for logger in logging.Logger.manager.loggerDict.values(): logger.setLevel(logging.CRITICAL) -class HashableMixin(discord.mixins.EqualityComparable): +class HashableMixin(disnake.mixins.EqualityComparable): """ - Mixin that provides similar hashing and equality functionality as discord.py's `Hashable` mixin. + Mixin that provides similar hashing and equality functionality as disnake's `Hashable` mixin. - Note: discord.py`s `Hashable` mixin bit-shifts `self.id` (`>> 22`); to prevent hash-collisions + Note: disnake`s `Hashable` mixin bit-shifts `self.id` (`>> 22`); to prevent hash-collisions for the relative small `id` integers we generally use in tests, this bit-shift is omitted. """ @@ -39,22 +39,22 @@ class HashableMixin(discord.mixins.EqualityComparable): class ColourMixin: - """A mixin for Mocks that provides the aliasing of (accent_)color->(accent_)colour like discord.py does.""" + """A mixin for Mocks that provides the aliasing of (accent_)color->(accent_)colour like disnake does.""" @property - def color(self) -> discord.Colour: + def color(self) -> disnake.Colour: return self.colour @color.setter - def color(self, color: discord.Colour) -> None: + def color(self, color: disnake.Colour) -> None: self.colour = color @property - def accent_color(self) -> discord.Colour: + def accent_color(self) -> disnake.Colour: return self.accent_colour @accent_color.setter - def accent_color(self, color: discord.Colour) -> None: + def accent_color(self, color: disnake.Colour) -> None: self.accent_colour = color @@ -63,7 +63,7 @@ class CustomMockMixin: Provides common functionality for our custom Mock types. The `_get_child_mock` method automatically returns an AsyncMock for coroutine methods of the mock - object. As discord.py also uses synchronous methods that nonetheless return coroutine objects, the + object. As disnake also uses synchronous methods that nonetheless return coroutine objects, the class attribute `additional_spec_asyncs` can be overwritten with an iterable containing additional attribute names that should also mocked with an AsyncMock instead of a regular MagicMock/Mock. The class method `spec_set` can be overwritten with the object that should be uses as the specification @@ -119,7 +119,7 @@ class CustomMockMixin: return klass(**kw) -# Create a guild instance to get a realistic Mock of `discord.Guild` +# Create a guild instance to get a realistic Mock of `disnake.Guild` guild_data = { 'id': 1, 'name': 'guild', @@ -139,20 +139,20 @@ guild_data = { 'owner_id': 1, 'afk_channel_id': 464033278631084042, } -guild_instance = discord.Guild(data=guild_data, state=unittest.mock.MagicMock()) +guild_instance = disnake.Guild(data=guild_data, state=unittest.mock.MagicMock()) class MockGuild(CustomMockMixin, unittest.mock.Mock, HashableMixin): """ - A `Mock` subclass to mock `discord.Guild` objects. + A `Mock` subclass to mock `disnake.Guild` objects. - A MockGuild instance will follow the specifications of a `discord.Guild` instance. This means + A MockGuild instance will follow the specifications of a `disnake.Guild` instance. This means that if the code you're testing tries to access an attribute or method that normally does not - exist for a `discord.Guild` object this will raise an `AttributeError`. This is to make sure our - tests fail if the code we're testing uses a `discord.Guild` object in the wrong way. + exist for a `disnake.Guild` object this will raise an `AttributeError`. This is to make sure our + tests fail if the code we're testing uses a `disnake.Guild` object in the wrong way. One restriction of that is that if the code tries to access an attribute that normally does not - exist for `discord.Guild` instance but was added dynamically, this will raise an exception with + exist for `disnake.Guild` instance but was added dynamically, this will raise an exception with the mocked object. To get around that, you can set the non-standard attribute explicitly for the instance of `MockGuild`: @@ -160,10 +160,10 @@ class MockGuild(CustomMockMixin, unittest.mock.Mock, HashableMixin): >>> guild.attribute_that_normally_does_not_exist = unittest.mock.MagicMock() In addition to attribute simulation, mocked guild object will pass an `isinstance` check against - `discord.Guild`: + `disnake.Guild`: >>> guild = MockGuild() - >>> isinstance(guild, discord.Guild) + >>> isinstance(guild, disnake.Guild) True For more info, see the `Mocking` section in `tests/README.md`. @@ -179,16 +179,16 @@ class MockGuild(CustomMockMixin, unittest.mock.Mock, HashableMixin): self.roles.extend(roles) -# Create a Role instance to get a realistic Mock of `discord.Role` +# Create a Role instance to get a realistic Mock of `disnake.Role` role_data = {'name': 'role', 'id': 1} -role_instance = discord.Role(guild=guild_instance, state=unittest.mock.MagicMock(), data=role_data) +role_instance = disnake.Role(guild=guild_instance, state=unittest.mock.MagicMock(), data=role_data) class MockRole(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): """ - A Mock subclass to mock `discord.Role` objects. + A Mock subclass to mock `disnake.Role` objects. - Instances of this class will follow the specifications of `discord.Role` instances. For more + Instances of this class will follow the specifications of `disnake.Role` instances. For more information, see the `MockGuild` docstring. """ spec_set = role_instance @@ -198,40 +198,40 @@ class MockRole(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): 'id': next(self.discord_id), 'name': 'role', 'position': 1, - 'colour': discord.Colour(0xdeadbf), - 'permissions': discord.Permissions(), + 'colour': disnake.Colour(0xdeadbf), + 'permissions': disnake.Permissions(), } super().__init__(**collections.ChainMap(kwargs, default_kwargs)) if isinstance(self.colour, int): - self.colour = discord.Colour(self.colour) + self.colour = disnake.Colour(self.colour) if isinstance(self.permissions, int): - self.permissions = discord.Permissions(self.permissions) + self.permissions = disnake.Permissions(self.permissions) if 'mention' not in kwargs: self.mention = f'&{self.name}' def __lt__(self, other): - """Simplified position-based comparisons similar to those of `discord.Role`.""" + """Simplified position-based comparisons similar to those of `disnake.Role`.""" return self.position < other.position def __ge__(self, other): - """Simplified position-based comparisons similar to those of `discord.Role`.""" + """Simplified position-based comparisons similar to those of `disnake.Role`.""" return self.position >= other.position -# Create a Member instance to get a realistic Mock of `discord.Member` +# Create a Member instance to get a realistic Mock of `disnake.Member` member_data = {'user': 'lemon', 'roles': [1]} state_mock = unittest.mock.MagicMock() -member_instance = discord.Member(data=member_data, guild=guild_instance, state=state_mock) +member_instance = disnake.Member(data=member_data, guild=guild_instance, state=state_mock) class MockMember(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): """ A Mock subclass to mock Member objects. - Instances of this class will follow the specifications of `discord.Member` instances. For more + Instances of this class will follow the specifications of `disnake.Member` instances. For more information, see the `MockGuild` docstring. """ spec_set = member_instance @@ -249,11 +249,11 @@ class MockMember(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin self.mention = f"@{self.name}" -# Create a User instance to get a realistic Mock of `discord.User` +# Create a User instance to get a realistic Mock of `disnake.User` _user_data_mock = collections.defaultdict(unittest.mock.MagicMock, { "accent_color": 0 }) -user_instance = discord.User( +user_instance = disnake.User( data=unittest.mock.MagicMock(get=unittest.mock.Mock(side_effect=_user_data_mock.get)), state=unittest.mock.MagicMock() ) @@ -263,7 +263,7 @@ class MockUser(CustomMockMixin, unittest.mock.Mock, ColourMixin, HashableMixin): """ A Mock subclass to mock User objects. - Instances of this class will follow the specifications of `discord.User` instances. For more + Instances of this class will follow the specifications of `disnake.User` instances. For more information, see the `MockGuild` docstring. """ spec_set = user_instance @@ -305,7 +305,7 @@ class MockBot(CustomMockMixin, unittest.mock.MagicMock): """ A MagicMock subclass to mock Bot objects. - Instances of this class will follow the specifications of `discord.ext.commands.Bot` instances. + Instances of this class will follow the specifications of `disnake.ext.commands.Bot` instances. For more information, see the `MockGuild` docstring. """ spec_set = Bot( @@ -324,7 +324,7 @@ class MockBot(CustomMockMixin, unittest.mock.MagicMock): self.stats = unittest.mock.create_autospec(spec=AsyncStatsClient, spec_set=True) -# Create a TextChannel instance to get a realistic MagicMock of `discord.TextChannel` +# Create a TextChannel instance to get a realistic MagicMock of `disnake.TextChannel` channel_data = { 'id': 1, 'type': 'TextChannel', @@ -337,17 +337,17 @@ channel_data = { } state = unittest.mock.MagicMock() guild = unittest.mock.MagicMock() -text_channel_instance = discord.TextChannel(state=state, guild=guild, data=channel_data) +text_channel_instance = disnake.TextChannel(state=state, guild=guild, data=channel_data) channel_data["type"] = "VoiceChannel" -voice_channel_instance = discord.VoiceChannel(state=state, guild=guild, data=channel_data) +voice_channel_instance = disnake.VoiceChannel(state=state, guild=guild, data=channel_data) class MockTextChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): """ A MagicMock subclass to mock TextChannel objects. - Instances of this class will follow the specifications of `discord.TextChannel` instances. For + Instances of this class will follow the specifications of `disnake.TextChannel` instances. For more information, see the `MockGuild` docstring. """ spec_set = text_channel_instance @@ -364,7 +364,7 @@ class MockVoiceChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): """ A MagicMock subclass to mock VoiceChannel objects. - Instances of this class will follow the specifications of `discord.VoiceChannel` instances. For + Instances of this class will follow the specifications of `disnake.VoiceChannel` instances. For more information, see the `MockGuild` docstring. """ spec_set = voice_channel_instance @@ -381,14 +381,14 @@ class MockVoiceChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): state = unittest.mock.MagicMock() me = unittest.mock.MagicMock() dm_channel_data = {"id": 1, "recipients": [unittest.mock.MagicMock()]} -dm_channel_instance = discord.DMChannel(me=me, state=state, data=dm_channel_data) +dm_channel_instance = disnake.DMChannel(me=me, state=state, data=dm_channel_data) class MockDMChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): """ A MagicMock subclass to mock TextChannel objects. - Instances of this class will follow the specifications of `discord.TextChannel` instances. For + Instances of this class will follow the specifications of `disnake.TextChannel` instances. For more information, see the `MockGuild` docstring. """ spec_set = dm_channel_instance @@ -398,17 +398,17 @@ class MockDMChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): super().__init__(**collections.ChainMap(kwargs, default_kwargs)) -# Create CategoryChannel instance to get a realistic MagicMock of `discord.CategoryChannel` +# Create CategoryChannel instance to get a realistic MagicMock of `disnake.CategoryChannel` category_channel_data = { 'id': 1, - 'type': discord.ChannelType.category, + 'type': disnake.ChannelType.category, 'name': 'category', 'position': 1, } state = unittest.mock.MagicMock() guild = unittest.mock.MagicMock() -category_channel_instance = discord.CategoryChannel( +category_channel_instance = disnake.CategoryChannel( state=state, guild=guild, data=category_channel_data ) @@ -419,7 +419,7 @@ class MockCategoryChannel(CustomMockMixin, unittest.mock.Mock, HashableMixin): super().__init__(**collections.ChainMap(default_kwargs, kwargs)) -# Create a Message instance to get a realistic MagicMock of `discord.Message` +# Create a Message instance to get a realistic MagicMock of `disnake.Message` message_data = { 'id': 1, 'webhook_id': 431341013479718912, @@ -438,10 +438,10 @@ message_data = { } state = unittest.mock.MagicMock() channel = unittest.mock.MagicMock() -message_instance = discord.Message(state=state, channel=channel, data=message_data) +message_instance = disnake.Message(state=state, channel=channel, data=message_data) -# Create a Context instance to get a realistic MagicMock of `discord.ext.commands.Context` +# Create a Context instance to get a realistic MagicMock of `disnake.ext.commands.Context` context_instance = Context( message=unittest.mock.MagicMock(), prefix="$", @@ -455,7 +455,7 @@ class MockContext(CustomMockMixin, unittest.mock.MagicMock): """ A MagicMock subclass to mock Context objects. - Instances of this class will follow the specifications of `discord.ext.commands.Context` + Instances of this class will follow the specifications of `disnake.ext.commands.Context` instances. For more information, see the `MockGuild` docstring. """ spec_set = context_instance @@ -471,14 +471,14 @@ class MockContext(CustomMockMixin, unittest.mock.MagicMock): self.invoked_from_error_handler = kwargs.get('invoked_from_error_handler', False) -attachment_instance = discord.Attachment(data=unittest.mock.MagicMock(id=1), state=unittest.mock.MagicMock()) +attachment_instance = disnake.Attachment(data=unittest.mock.MagicMock(id=1), state=unittest.mock.MagicMock()) class MockAttachment(CustomMockMixin, unittest.mock.MagicMock): """ A MagicMock subclass to mock Attachment objects. - Instances of this class will follow the specifications of `discord.Attachment` instances. For + Instances of this class will follow the specifications of `disnake.Attachment` instances. For more information, see the `MockGuild` docstring. """ spec_set = attachment_instance @@ -488,7 +488,7 @@ class MockMessage(CustomMockMixin, unittest.mock.MagicMock): """ A MagicMock subclass to mock Message objects. - Instances of this class will follow the specifications of `discord.Message` instances. For more + Instances of this class will follow the specifications of `disnake.Message` instances. For more information, see the `MockGuild` docstring. """ spec_set = message_instance @@ -501,14 +501,14 @@ class MockMessage(CustomMockMixin, unittest.mock.MagicMock): emoji_data = {'require_colons': True, 'managed': True, 'id': 1, 'name': 'hyperlemon'} -emoji_instance = discord.Emoji(guild=MockGuild(), state=unittest.mock.MagicMock(), data=emoji_data) +emoji_instance = disnake.Emoji(guild=MockGuild(), state=unittest.mock.MagicMock(), data=emoji_data) class MockEmoji(CustomMockMixin, unittest.mock.MagicMock): """ A MagicMock subclass to mock Emoji objects. - Instances of this class will follow the specifications of `discord.Emoji` instances. For more + Instances of this class will follow the specifications of `disnake.Emoji` instances. For more information, see the `MockGuild` docstring. """ spec_set = emoji_instance @@ -518,27 +518,27 @@ class MockEmoji(CustomMockMixin, unittest.mock.MagicMock): self.guild = kwargs.get('guild', MockGuild()) -partial_emoji_instance = discord.PartialEmoji(animated=False, name='guido') +partial_emoji_instance = disnake.PartialEmoji(animated=False, name='guido') class MockPartialEmoji(CustomMockMixin, unittest.mock.MagicMock): """ A MagicMock subclass to mock PartialEmoji objects. - Instances of this class will follow the specifications of `discord.PartialEmoji` instances. For + Instances of this class will follow the specifications of `disnake.PartialEmoji` instances. For more information, see the `MockGuild` docstring. """ spec_set = partial_emoji_instance -reaction_instance = discord.Reaction(message=MockMessage(), data={'me': True}, emoji=MockEmoji()) +reaction_instance = disnake.Reaction(message=MockMessage(), data={'me': True}, emoji=MockEmoji()) class MockReaction(CustomMockMixin, unittest.mock.MagicMock): """ A MagicMock subclass to mock Reaction objects. - Instances of this class will follow the specifications of `discord.Reaction` instances. For + Instances of this class will follow the specifications of `disnake.Reaction` instances. For more information, see the `MockGuild` docstring. """ spec_set = reaction_instance @@ -556,14 +556,14 @@ class MockReaction(CustomMockMixin, unittest.mock.MagicMock): self.__str__.return_value = str(self.emoji) -webhook_instance = discord.Webhook(data=unittest.mock.MagicMock(), session=unittest.mock.MagicMock()) +webhook_instance = disnake.Webhook(data=unittest.mock.MagicMock(), session=unittest.mock.MagicMock()) class MockAsyncWebhook(CustomMockMixin, unittest.mock.MagicMock): """ A MagicMock subclass to mock Webhook objects using an AsyncWebhookAdapter. - Instances of this class will follow the specifications of `discord.Webhook` instances. For + Instances of this class will follow the specifications of `disnake.Webhook` instances. For more information, see the `MockGuild` docstring. """ spec_set = webhook_instance diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 81285e009..c5e799a85 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -2,20 +2,20 @@ import asyncio import unittest import unittest.mock -import discord +import disnake from tests import helpers class DiscordMocksTests(unittest.TestCase): - """Tests for our specialized discord.py mocks.""" + """Tests for our specialized disnake mocks.""" def test_mock_role_default_initialization(self): """Test if the default initialization of MockRole results in the correct object.""" role = helpers.MockRole() - # The `spec` argument makes sure `isistance` checks with `discord.Role` pass - self.assertIsInstance(role, discord.Role) + # The `spec` argument makes sure `isistance` checks with `disnake.Role` pass + self.assertIsInstance(role, disnake.Role) self.assertEqual(role.name, "role") self.assertEqual(role.position, 1) @@ -61,8 +61,8 @@ class DiscordMocksTests(unittest.TestCase): """Test if the default initialization of Mockmember results in the correct object.""" member = helpers.MockMember() - # The `spec` argument makes sure `isistance` checks with `discord.Member` pass - self.assertIsInstance(member, discord.Member) + # The `spec` argument makes sure `isistance` checks with `disnake.Member` pass + self.assertIsInstance(member, disnake.Member) self.assertEqual(member.name, "member") self.assertListEqual(member.roles, [helpers.MockRole(name="@everyone", position=1, id=0)]) @@ -86,18 +86,18 @@ class DiscordMocksTests(unittest.TestCase): """Test if MockMember accepts and sets abitrary keyword arguments.""" member = helpers.MockMember( nick="Dino Man", - colour=discord.Colour.default(), + colour=disnake.Colour.default(), ) self.assertEqual(member.nick, "Dino Man") - self.assertEqual(member.colour, discord.Colour.default()) + self.assertEqual(member.colour, disnake.Colour.default()) def test_mock_guild_default_initialization(self): """Test if the default initialization of Mockguild results in the correct object.""" guild = helpers.MockGuild() - # The `spec` argument makes sure `isistance` checks with `discord.Guild` pass - self.assertIsInstance(guild, discord.Guild) + # The `spec` argument makes sure `isistance` checks with `disnake.Guild` pass + self.assertIsInstance(guild, disnake.Guild) self.assertListEqual(guild.roles, [helpers.MockRole(name="@everyone", position=1, id=0)]) self.assertListEqual(guild.members, []) @@ -127,15 +127,15 @@ class DiscordMocksTests(unittest.TestCase): """Tests if MockBot initializes with the correct values.""" bot = helpers.MockBot() - # The `spec` argument makes sure `isistance` checks with `discord.ext.commands.Bot` pass - self.assertIsInstance(bot, discord.ext.commands.Bot) + # The `spec` argument makes sure `isistance` checks with `disnake.ext.commands.Bot` pass + self.assertIsInstance(bot, disnake.ext.commands.Bot) def test_mock_context_default_initialization(self): """Tests if MockContext initializes with the correct values.""" context = helpers.MockContext() - # The `spec` argument makes sure `isistance` checks with `discord.ext.commands.Context` pass - self.assertIsInstance(context, discord.ext.commands.Context) + # The `spec` argument makes sure `isistance` checks with `disnake.ext.commands.Context` pass + self.assertIsInstance(context, disnake.ext.commands.Context) self.assertIsInstance(context.bot, helpers.MockBot) self.assertIsInstance(context.guild, helpers.MockGuild) -- cgit v1.2.3 From ab073c89ad37c26eb4103ef0ca58e16421bca875 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Mon, 21 Feb 2022 02:16:59 +0000 Subject: Migrate to use monkey patches from botcore --- bot/__init__.py | 18 +-- bot/converters.py | 2 +- bot/exts/filters/filtering.py | 2 +- bot/exts/utils/snekbox.py | 2 +- bot/monkey_patches.py | 76 ----------- poetry.lock | 291 ++++++++++++++++++++++++++---------------- pyproject.toml | 2 +- 7 files changed, 191 insertions(+), 202 deletions(-) delete mode 100644 bot/monkey_patches.py diff --git a/bot/__init__.py b/bot/__init__.py index b28513bff..f087792e9 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -1,11 +1,10 @@ import asyncio import os -from functools import partial, partialmethod from typing import TYPE_CHECKING -from disnake.ext import commands +from botcore.utils import apply_monkey_patches -from bot import log, monkey_patches +from bot import log if TYPE_CHECKING: from bot.bot import Bot @@ -16,16 +15,7 @@ log.setup() if os.name == "nt": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) -monkey_patches.patch_typing() - -# This patches any convertors that use PartialMessage, but not the PartialMessageConverter itself -# as library objects are made by this mapping. -# https://github.com/Rapptz/discord.py/blob/1a4e73d59932cdbe7bf2c281f25e32529fc7ae1f/discord/ext/commands/converter.py#L984-L1004 -commands.converter.PartialMessageConverter = monkey_patches.FixedPartialMessageConverter - -# Monkey-patch discord.py decorators to use the Command subclass which supports root aliases. -# Must be patched before any cogs are added. -commands.command = partial(commands.command, cls=monkey_patches.Command) -commands.GroupMixin.command = partialmethod(commands.GroupMixin.command, cls=monkey_patches.Command) +# Apply all monkey patches from bot core. +apply_monkey_patches() instance: "Bot" = None # Global Bot instance. diff --git a/bot/converters.py b/bot/converters.py index 9d93428ca..6f35d2fe4 100644 --- a/bot/converters.py +++ b/bot/converters.py @@ -8,7 +8,7 @@ from ssl import CertificateError import dateutil.parser import disnake from aiohttp import ClientConnectorError -from botcore.regex import DISCORD_INVITE +from botcore.utils.regex import DISCORD_INVITE from dateutil.relativedelta import relativedelta from disnake.ext.commands import BadArgument, Bot, Context, Converter, IDConverter, MemberConverter, UserConverter from disnake.utils import escape_markdown, snowflake_time diff --git a/bot/exts/filters/filtering.py b/bot/exts/filters/filtering.py index e8c9bab62..828e8b262 100644 --- a/bot/exts/filters/filtering.py +++ b/bot/exts/filters/filtering.py @@ -10,7 +10,7 @@ import disnake.errors import regex import tldextract from async_rediscache import RedisCache -from botcore.regex import DISCORD_INVITE +from botcore.utils.regex import DISCORD_INVITE from dateutil.relativedelta import relativedelta from disnake import Colour, HTTPException, Member, Message, NotFound, TextChannel from disnake.ext.commands import Cog diff --git a/bot/exts/utils/snekbox.py b/bot/exts/utils/snekbox.py index 07d824f87..5f82c1cc8 100644 --- a/bot/exts/utils/snekbox.py +++ b/bot/exts/utils/snekbox.py @@ -7,7 +7,7 @@ from functools import partial from signal import Signals from typing import Optional, Tuple -from botcore.regex import FORMATTED_CODE_REGEX, RAW_CODE_REGEX +from botcore.utils.regex import FORMATTED_CODE_REGEX, RAW_CODE_REGEX from disnake import AllowedMentions, HTTPException, Message, NotFound, Reaction, User from disnake.ext.commands import Cog, Context, command, guild_only diff --git a/bot/monkey_patches.py b/bot/monkey_patches.py deleted file mode 100644 index 590be22a2..000000000 --- a/bot/monkey_patches.py +++ /dev/null @@ -1,76 +0,0 @@ -import re -from datetime import timedelta - -import arrow -from disnake import Forbidden, http -from disnake.ext import commands - -from bot.log import get_logger - -log = get_logger(__name__) -MESSAGE_ID_RE = re.compile(r'(?P[0-9]{15,20})$') - - -class Command(commands.Command): - """ - A `disnake.ext.commands.Command` subclass which supports root aliases. - - A `root_aliases` keyword argument is added, which is a sequence of alias names that will act as - top-level commands rather than being aliases of the command's group. It's stored as an attribute - also named `root_aliases`. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.root_aliases = kwargs.get("root_aliases", []) - - if not isinstance(self.root_aliases, (list, tuple)): - raise TypeError("Root aliases of a command must be a list or a tuple of strings.") - - -def patch_typing() -> None: - """ - Sometimes discord turns off typing events by throwing 403's. - - Handle those issues by patching the trigger_typing method so it ignores 403's in general. - """ - log.debug("Patching send_typing, which should fix things breaking when discord disables typing events. Stay safe!") - - original = http.HTTPClient.send_typing - last_403 = None - - async def honeybadger_type(self, channel_id: int) -> None: # noqa: ANN001 - nonlocal last_403 - if last_403 and (arrow.utcnow() - last_403) < timedelta(minutes=5): - log.warning("Not sending typing event, we got a 403 less than 5 minutes ago.") - return - try: - await original(self, channel_id) - except Forbidden: - last_403 = arrow.utcnow() - log.warning("Got a 403 from typing event!") - pass - - http.HTTPClient.send_typing = honeybadger_type - - -class FixedPartialMessageConverter(commands.PartialMessageConverter): - """ - Make the Message converter infer channelID from the given context if only a messageID is given. - - Discord.py's Message converter is supposed to infer channelID based - on ctx.channel if only a messageID is given. A refactor commit, linked below, - a few weeks before d.py's archival broke this defined behaviour of the converter. - Currently, if only a messageID is given to the converter, it will only find that message - if it's in the bot's cache. - - https://github.com/Rapptz/discord.py/commit/1a4e73d59932cdbe7bf2c281f25e32529fc7ae1f - """ - - @staticmethod - def _get_id_matches(ctx: commands.Context, argument: str) -> tuple[int, int, int]: - """Inserts ctx.channel.id before calling super method if argument is just a messageID.""" - match = MESSAGE_ID_RE.match(argument) - if match: - argument = f"{ctx.channel.id}-{match.group('message_id')}" - return commands.PartialMessageConverter._get_id_matches(ctx, argument) diff --git a/poetry.lock b/poetry.lock index 087fd739c..3074f3745 100644 --- a/poetry.lock +++ b/poetry.lock @@ -26,22 +26,23 @@ pycares = ">=3.0.0" [[package]] name = "aiohttp" -version = "3.7.4.post0" +version = "3.8.1" description = "Async http client/server framework (asyncio)" category = "main" optional = false python-versions = ">=3.6" [package.dependencies] -async-timeout = ">=3.0,<4.0" +aiosignal = ">=1.1.2" +async-timeout = ">=4.0.0a3,<5.0" attrs = ">=17.3.0" -chardet = ">=2.0,<5.0" +charset-normalizer = ">=2.0,<3.0" +frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" -typing-extensions = ">=3.6.5" yarl = ">=1.0,<2.0" [package.extras] -speedups = ["aiodns", "brotlipy", "cchardet"] +speedups = ["aiodns", "brotli", "cchardet"] [[package]] name = "aioredis" @@ -70,6 +71,17 @@ yarl = "*" [package.extras] develop = ["aiomisc (>=11.0,<12.0)", "async-generator", "coverage (!=4.3)", "coveralls", "pylava", "pytest", "pytest-cov", "tox (>=2.4)"] +[[package]] +name = "aiosignal" +version = "1.2.0" +description = "aiosignal: a list of registered asynchronous callbacks" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +frozenlist = ">=1.1.0" + [[package]] name = "arrow" version = "1.0.3" @@ -98,11 +110,11 @@ fakeredis = ["fakeredis (>=1.3.1)"] [[package]] name = "async-timeout" -version = "3.0.1" +version = "4.0.2" description = "Timeout context manager for asyncio programs" category = "main" optional = false -python-versions = ">=3.5.3" +python-versions = ">=3.6" [[package]] name = "atomicwrites" @@ -143,18 +155,18 @@ lxml = ["lxml"] [[package]] name = "bot-core" -version = "2.1.0" +version = "3.0.0" description = "Bot-Core provides the core functionality and utilities for the bots of the Python Discord community." category = "main" optional = false python-versions = "3.9.*" [package.dependencies] -"discord.py" = {url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip"} +disnake = ">=2,<3" [package.source] type = "url" -url = "https://github.com/python-discord/bot-core/archive/refs/tags/v2.1.0.zip" +url = "https://github.com/python-discord/bot-core/archive/refs/tags/v3.0.0.zip" [[package]] name = "certifi" version = "2021.10.8" @@ -182,14 +194,6 @@ category = "dev" optional = false python-versions = ">=3.6.1" -[[package]] -name = "chardet" -version = "4.0.0" -description = "Universal encoding detector for Python 2 and 3" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - [[package]] name = "charset-normalizer" version = "2.0.12" @@ -262,26 +266,6 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["tox", "bump2version (<1)", "sphinx (<2)", "importlib-metadata (<3)", "importlib-resources (<4)", "configparser (<5)", "sphinxcontrib-websupport (<2)", "zipp (<2)", "PyTest (<5)", "PyTest-Cov (<2.6)", "pytest", "pytest-cov"] -[[package]] -name = "discord.py" -version = "2.0.0a0" -description = "A Python wrapper for the Discord API" -category = "main" -optional = false -python-versions = ">=3.8.0" - -[package.dependencies] -aiohttp = ">=3.6.0,<3.8.0" - -[package.extras] -docs = ["sphinx (==4.0.2)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport"] -speed = ["orjson (>=3.5.4)"] -voice = ["PyNaCl (>=1.3.0,<1.5)"] - -[package.source] -type = "url" -url = "https://github.com/Rapptz/discord.py/archive/45d498c1b76deaf3b394d17ccf56112fa691d160.zip" - [[package]] name = "disnake" version = "2.4.0" @@ -481,6 +465,14 @@ python-versions = "*" [package.dependencies] pycodestyle = ">=2.0.0,<3.0.0" +[[package]] +name = "frozenlist" +version = "1.3.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "hiredis" version = "2.0.0" @@ -502,7 +494,7 @@ pyreadline3 = {version = "*", markers = "sys_platform == \"win32\" and python_ve [[package]] name = "identify" -version = "2.4.10" +version = "2.4.11" description = "File identification library for Python" category = "dev" optional = false @@ -983,7 +975,7 @@ six = "*" [[package]] name = "sentry-sdk" -version = "1.5.5" +version = "1.5.6" description = "Python client for Sentry (https://sentry.io)" category = "main" optional = false @@ -1087,11 +1079,11 @@ test = ["pytest (>=3.6)", "pytest-cov", "pytest-django", "zope.component", "sybi [[package]] name = "tldextract" -version = "3.1.2" +version = "3.2.0" description = "Accurately separate the TLD from the registered domain and subdomains of a URL, using the Public Suffix List. By default, this includes the public ICANN TLDs and their exceptions. You can optionally support the Public Suffix List's private domains as well." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] filelock = ">=3.0.8" @@ -1107,14 +1099,6 @@ category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -[[package]] -name = "typing-extensions" -version = "4.1.1" -description = "Backported and Experimental Type Hints for Python 3.6+" -category = "main" -optional = false -python-versions = ">=3.6" - [[package]] name = "urllib3" version = "1.26.8" @@ -1130,7 +1114,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.13.1" +version = "20.13.2" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1169,7 +1153,7 @@ multidict = ">=4.0" [metadata] lock-version = "1.1" python-versions = "3.9.*" -content-hash = "b8b28311c13f7a66f028041bae889131d3916ca7f667c9a7539871d21bbcd077" +content-hash = "e4828d46fc4ce002fed010986558a26a2edecf410ba7884f42f96e77d91a3844" [metadata.files] aio-pika = [ @@ -1181,43 +1165,78 @@ aiodns = [ {file = "aiodns-2.0.0.tar.gz", hash = "sha256:815fdef4607474295d68da46978a54481dd1e7be153c7d60f9e72773cd38d77d"}, ] aiohttp = [ - {file = "aiohttp-3.7.4.post0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_ppc64le.whl", hash = "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_s390x.whl", hash = "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-win32.whl", hash = "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287"}, - {file = "aiohttp-3.7.4.post0-cp36-cp36m-win_amd64.whl", hash = "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-win32.whl", hash = "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f"}, - {file = "aiohttp-3.7.4.post0-cp37-cp37m-win_amd64.whl", hash = "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-win32.whl", hash = "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16"}, - {file = "aiohttp-3.7.4.post0-cp38-cp38-win_amd64.whl", hash = "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-win32.whl", hash = "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9"}, - {file = "aiohttp-3.7.4.post0-cp39-cp39-win_amd64.whl", hash = "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe"}, - {file = "aiohttp-3.7.4.post0.tar.gz", hash = "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, + {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, + {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, + {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, + {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, + {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, + {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, + {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, + {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, + {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, + {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, + {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, + {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, + {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, + {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, + {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, + {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, + {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, + {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, + {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, + {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, + {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, + {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, + {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, + {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, ] aioredis = [ {file = "aioredis-1.3.1-py3-none-any.whl", hash = "sha256:b61808d7e97b7cd5a92ed574937a079c9387fdadd22bfbfa7ad2fd319ecc26e3"}, @@ -1227,6 +1246,10 @@ aiormq = [ {file = "aiormq-3.3.1-py3-none-any.whl", hash = "sha256:e584dac13a242589aaf42470fd3006cb0dc5aed6506cbd20357c7ec8bbe4a89e"}, {file = "aiormq-3.3.1.tar.gz", hash = "sha256:8218dd9f7198d6e7935855468326bbacf0089f926c70baa8dd92944cb2496573"}, ] +aiosignal = [ + {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, + {file = "aiosignal-1.2.0.tar.gz", hash = "sha256:78ed67db6c7b7ced4f98e495e572106d5c432a93e1ddd1bf475e1dc05f5b7df2"}, +] arrow = [ {file = "arrow-1.0.3-py3-none-any.whl", hash = "sha256:3515630f11a15c61dcb4cdd245883270dd334c83f3e639824e65a4b79cc48543"}, {file = "arrow-1.0.3.tar.gz", hash = "sha256:399c9c8ae732270e1aa58ead835a79a40d7be8aa109c579898eb41029b5a231d"}, @@ -1236,8 +1259,8 @@ async-rediscache = [ {file = "async_rediscache-0.1.4-py3-none-any.whl", hash = "sha256:c25e4fff73f64d20645254783c3224a4c49e083e3fab67c44f17af944c5e26af"}, ] async-timeout = [ - {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, - {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, @@ -1312,10 +1335,6 @@ cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] -chardet = [ - {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, - {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, -] charset-normalizer = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, @@ -1390,7 +1409,6 @@ deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, {file = "Deprecated-1.2.13.tar.gz", hash = "sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d"}, ] -"discord.py" = [] disnake = [ {file = "disnake-2.4.0-py3-none-any.whl", hash = "sha256:390250a55ed8bbcc8c5753a72fb8fff2376a30295476edfebd0d2301855fb919"}, {file = "disnake-2.4.0.tar.gz", hash = "sha256:d7a9c83d5cbfcec42441dae1d96744f82c2a22403934db5d8862a8279ca4989c"}, @@ -1453,6 +1471,67 @@ flake8-tidy-imports = [ flake8-todo = [ {file = "flake8-todo-0.7.tar.gz", hash = "sha256:6e4c5491ff838c06fe5a771b0e95ee15fc005ca57196011011280fc834a85915"}, ] +frozenlist = [ + {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, + {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, + {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, + {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, + {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, + {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, + {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, + {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, + {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, + {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, + {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, + {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, + {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, + {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, + {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, + {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, + {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, + {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, + {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, + {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, + {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, + {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, + {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, +] hiredis = [ {file = "hiredis-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b4c8b0bc5841e578d5fb32a16e0c305359b987b850a06964bd5a62739d688048"}, {file = "hiredis-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0adea425b764a08270820531ec2218d0508f8ae15a448568109ffcae050fee26"}, @@ -1501,8 +1580,8 @@ humanfriendly = [ {file = "humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc"}, ] identify = [ - {file = "identify-2.4.10-py2.py3-none-any.whl", hash = "sha256:7d10baf6ba6f1912a0a49f4c1c2c49fa1718765c3a37d72d13b07779567c5b85"}, - {file = "identify-2.4.10.tar.gz", hash = "sha256:e12b2aea3cf108de73ae055c2260783bde6601de09718f6768cf8e9f6f6322a6"}, + {file = "identify-2.4.11-py2.py3-none-any.whl", hash = "sha256:fd906823ed1db23c7a48f9b176a1d71cb8abede1e21ebe614bac7bdd688d9213"}, + {file = "identify-2.4.11.tar.gz", hash = "sha256:2986942d3974c8f2e5019a190523b0b0e2a07cb8e89bf236727fb4b26f27f8fd"}, ] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, @@ -1959,8 +2038,8 @@ requests-file = [ {file = "requests_file-1.5.1-py2.py3-none-any.whl", hash = "sha256:dfe5dae75c12481f68ba353183c53a65e6044c923e64c24b2209f6c7570ca953"}, ] sentry-sdk = [ - {file = "sentry-sdk-1.5.5.tar.gz", hash = "sha256:98fd155fa5d5fec1dbabed32a1a4ae2705f1edaa5dae4e7f7b62a384ba30e759"}, - {file = "sentry_sdk-1.5.5-py2.py3-none-any.whl", hash = "sha256:3817274fba2498c8ebf6b896ee98ac916c5598706340573268c07bf2bb30d831"}, + {file = "sentry-sdk-1.5.6.tar.gz", hash = "sha256:ac2a50128409d57655279817aedcb7800cace1f76b266f3dd62055d5afd6e098"}, + {file = "sentry_sdk-1.5.6-py2.py3-none-any.whl", hash = "sha256:1ab34e3851a34aeb3d1af1a0f77cec73978c4e9698e5210d050e4932953cb241"}, ] sgmllib3k = [ {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, @@ -1994,24 +2073,20 @@ testfixtures = [ {file = "testfixtures-6.18.5.tar.gz", hash = "sha256:02dae883f567f5b70fd3ad3c9eefb95912e78ac90be6c7444b5e2f46bf572c84"}, ] tldextract = [ - {file = "tldextract-3.1.2-py2.py3-none-any.whl", hash = "sha256:f55e05f6bf4cc952a87d13594386d32ad2dd265630a8bdfc3df03bd60425c6b0"}, - {file = "tldextract-3.1.2.tar.gz", hash = "sha256:d2034c3558651f7d8fdadea83fb681050b2d662dc67a00d950326dc902029444"}, + {file = "tldextract-3.2.0-py3-none-any.whl", hash = "sha256:427703b65db54644f7b81d3dcb79bf355c1a7c28a12944e5cc6787531ccc828a"}, + {file = "tldextract-3.2.0.tar.gz", hash = "sha256:3d4b6a2105600b7d0290ea237bf30b6b0dc763e50fcbe40e849a019bd6dbcbff"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] -typing-extensions = [ - {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, - {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, -] urllib3 = [ {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, ] virtualenv = [ - {file = "virtualenv-20.13.1-py2.py3-none-any.whl", hash = "sha256:45e1d053cad4cd453181ae877c4ffc053546ae99e7dd049b9ff1d9be7491abf7"}, - {file = "virtualenv-20.13.1.tar.gz", hash = "sha256:e0621bcbf4160e4e1030f05065c8834b4e93f4fcc223255db2a823440aca9c14"}, + {file = "virtualenv-20.13.2-py2.py3-none-any.whl", hash = "sha256:e7b34c9474e6476ee208c43a4d9ac1510b041c68347eabfe9a9ea0c86aa0a46b"}, + {file = "virtualenv-20.13.2.tar.gz", hash = "sha256:01f5f80744d24a3743ce61858123488e91cb2dd1d3bdf92adaf1bba39ffdedf0"}, ] wrapt = [ {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, diff --git a/pyproject.toml b/pyproject.toml index 1f02818ee..06795fd0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ license = "MIT" python = "3.9.*" disnake = "~=2.4" # See https://bot-core.pythondiscord.com/ for docs. -bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v2.1.0.zip"} +bot-core = {url = "https://github.com/python-discord/bot-core/archive/refs/tags/v3.0.0.zip"} aio-pika = "~=6.1" aiodns = "~=2.0" aiohttp = "~=3.7" -- cgit v1.2.3 From a1c73b5eca88d1b92cd42d3c41183387209461b9 Mon Sep 17 00:00:00 2001 From: Chris Lovering Date: Sat, 5 Mar 2022 23:11:17 +0000 Subject: No longer use Interaction.message, as it was removed from disnake interaction.response is what should be used now instead. --- bot/exts/info/help.py | 16 +++++----------- bot/exts/info/subscribe.py | 10 ++++++---- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/bot/exts/info/help.py b/bot/exts/info/help.py index 29d73c564..597534083 100644 --- a/bot/exts/info/help.py +++ b/bot/exts/info/help.py @@ -6,7 +6,7 @@ from collections import namedtuple from contextlib import suppress from typing import List, Optional, Union -from disnake import ButtonStyle, Colour, Embed, Emoji, Interaction, PartialEmoji, ui +from disnake import ButtonStyle, Colour, Embed, Emoji, HTTPException, Interaction, PartialEmoji, ui from disnake.ext.commands import Bot, Cog, Command, CommandError, Context, DisabledCommand, Group, HelpCommand from rapidfuzz import fuzz, process from rapidfuzz.utils import default_process @@ -57,16 +57,13 @@ class SubcommandButton(ui.Button): async def callback(self, interaction: Interaction) -> None: """Edits the help embed to that of the subcommand.""" - message = interaction.message - if not message: - return - subcommand = self.command if isinstance(subcommand, Group): embed, subcommand_view = await self.help_command.format_group_help(subcommand) else: embed, subcommand_view = await self.help_command.command_formatting(subcommand) - await message.edit(embed=embed, view=subcommand_view) + with suppress(HTTPException): + await interaction.response.edit_message(embed=embed, view=subcommand_view) class GroupButton(ui.Button): @@ -98,12 +95,9 @@ class GroupButton(ui.Button): async def callback(self, interaction: Interaction) -> None: """Edits the help embed to that of the parent.""" - message = interaction.message - if not message: - return - embed, group_view = await self.help_command.format_group_help(self.command.parent) - await message.edit(embed=embed, view=group_view) + with suppress(HTTPException): + await interaction.response.edit_message(embed=embed, view=group_view) class CommandView(ui.View): diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index 0f285e0cb..ddfb238b8 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -1,4 +1,5 @@ import calendar +import contextlib import operator import typing as t from dataclasses import dataclass @@ -82,7 +83,7 @@ class SingleRoleButton(disnake.ui.Button): ADD_STYLE = disnake.ButtonStyle.success REMOVE_STYLE = disnake.ButtonStyle.red UNAVAILABLE_STYLE = disnake.ButtonStyle.secondary - LABEL_FORMAT = "{action} role {role_name}." + LABEL_FORMAT = "{action} role {role_name}" CUSTOM_ID_FORMAT = "subscribe-{role_id}" def __init__(self, role: AssignableRole, assigned: bool, row: int): @@ -106,7 +107,8 @@ class SingleRoleButton(disnake.ui.Button): """Update the member's role and change button text to reflect current text.""" if isinstance(interaction.user, disnake.User): log.trace("User %s is not a member", interaction.user) - await interaction.message.delete() + with contextlib.suppress(disnake.HTTPException): + await interaction.delete_original_message() self.view.stop() return @@ -132,8 +134,8 @@ class SingleRoleButton(disnake.ui.Button): self.style = self.REMOVE_STYLE if self.assigned else self.ADD_STYLE self.label = self.LABEL_FORMAT.format(action="Remove" if self.assigned else "Add", role_name=self.role.name) try: - await interaction.message.edit(view=self.view) - except disnake.NotFound: + await interaction.response.edit_message(view=self.view) + except disnake.HTTPException: log.debug("Subscribe message for %s removed before buttons could be updated", interaction.user) self.view.stop() -- cgit v1.2.3 From 56f91ef04267bb0fe2e48650ced5b786c50f8499 Mon Sep 17 00:00:00 2001 From: MarkKoz <1515135+MarkKoz@users.noreply.github.com> Date: Sat, 5 Mar 2022 16:28:57 -0800 Subject: Add more expiration details to infraction DMs Separate the expiration timestamp and the duration. Explicitly indicate if an infraction is permanent or expired. Include the time remaining as a humanised delta. --- bot/exts/moderation/infraction/_utils.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index eec539fee..d5c6cd817 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -1,6 +1,7 @@ import typing as t from datetime import datetime +import arrow import discord from discord.ext.commands import Context @@ -44,6 +45,7 @@ LONGEST_EXTRAS = max(len(INFRACTION_APPEAL_SERVER_FOOTER), len(INFRACTION_APPEAL INFRACTION_DESCRIPTION_TEMPLATE = ( "**Type:** {type}\n" "**Expires:** {expires}\n" + "**Duration:** {duration}\n" "**Reason:** {reason}\n" ) @@ -173,7 +175,23 @@ async def notify_infraction( infr_id = infraction["id"] infr_type = infraction["type"].replace("_", " ").title() icon_url = INFRACTION_ICONS[infraction["type"]][0] - expires_at = time.format_with_duration(infraction["expires_at"]) + + if infraction["expires_at"] is None: + expires_at = "Never" + duration = "Permanent" + else: + expiry = arrow.get(infraction["expires_at"]) + now = arrow.utcnow() + + expires_at = time.format_relative(expiry) + duration = time.humanize_delta(infraction["inserted_at"], expiry, max_units=2) + + if expiry < now: + expires_at += " (Expired)" + else: + remaining = time.humanize_delta(expiry, now, max_units=2) + if duration != remaining: + duration += f" ({remaining} remaining)" log.trace(f"Sending {user} a DM about their {infr_type} infraction.") @@ -182,7 +200,8 @@ async def notify_infraction( text = INFRACTION_DESCRIPTION_TEMPLATE.format( type=infr_type.title(), - expires=expires_at or "N/A", + expires=expires_at, + duration=duration, reason=reason or "No reason provided." ) -- cgit v1.2.3 From 60c5762436dcacef5c67dfc266d3bbecf7349cfb Mon Sep 17 00:00:00 2001 From: MarkKoz <1515135+MarkKoz@users.noreply.github.com> Date: Sat, 5 Mar 2022 16:52:06 -0800 Subject: Fix detection of expired infractions in DMs --- bot/exts/moderation/infraction/_utils.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py index e319f9d71..36e818ec6 100644 --- a/bot/exts/moderation/infraction/_utils.py +++ b/bot/exts/moderation/infraction/_utils.py @@ -181,17 +181,15 @@ async def notify_infraction( duration = "Permanent" else: expiry = arrow.get(infraction["expires_at"]) - now = arrow.utcnow() - expires_at = time.format_relative(expiry) duration = time.humanize_delta(infraction["inserted_at"], expiry, max_units=2) - if expiry < now: - expires_at += " (Expired)" - else: - remaining = time.humanize_delta(expiry, now, max_units=2) + if infraction["active"]: + remaining = time.humanize_delta(expiry, arrow.utcnow(), max_units=2) if duration != remaining: duration += f" ({remaining} remaining)" + else: + expires_at += " (Inactive)" log.trace(f"Sending {user} a DM about their {infr_type} infraction.") -- cgit v1.2.3 From 37ff9d2b9f4e131d5d84afac5e5aa529020f0b53 Mon Sep 17 00:00:00 2001 From: camcaswell <38672443+camcaswell@users.noreply.github.com> Date: Sat, 12 Mar 2022 23:55:27 -0500 Subject: Add regex tag (#2109) Co-authored-by: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/resources/tags/regex.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 bot/resources/tags/regex.md diff --git a/bot/resources/tags/regex.md b/bot/resources/tags/regex.md new file mode 100644 index 000000000..35fee45a9 --- /dev/null +++ b/bot/resources/tags/regex.md @@ -0,0 +1,15 @@ +**Regular expressions** +Regular expressions (regex) are a tool for finding patterns in strings. The standard library's `re` module defines functions for using regex patterns. + +**Example** +We can use regex to pull out all the numbers in a sentence: +```py +>>> import re +>>> x = "On Oct 18 1963 a cat was launched aboard rocket #47" +>>> regex_pattern = r"[0-9]{1,3}" # Matches 1-3 digits +>>> re.findall(regex_pattern, foo) +['18', '196', '3', '47'] # Notice the year is cut off +``` +**See Also** +• [The re docs](https://docs.python.org/3/library/re.html) - for functions that use regex +• [regex101.com](https://regex101.com) - an interactive site for testing your regular expression -- cgit v1.2.3