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 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 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 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 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 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 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 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 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 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