From e06f496a6e3f9a9d6cfaeb3902547aa9da1dd7c1 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Wed, 14 Apr 2021 20:05:04 +0300 Subject: Add Duty cog and new Moderators role Added a cog to allow moderators to go off and on duty. The off-duty state is cached via a redis cache, and its expiry is scheduled via the Scheduler. Additionally changes which roles are pinged on mod alerts. --- bot/constants.py | 1 + bot/exts/moderation/duty.py | 135 ++++++++++++++++++++++++++++++++++++++++++ bot/exts/moderation/modlog.py | 6 +- config-default.yml | 5 +- 4 files changed, 143 insertions(+), 4 deletions(-) create mode 100644 bot/exts/moderation/duty.py diff --git a/bot/constants.py b/bot/constants.py index 6d14bbb3a..cc3aa41a5 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -491,6 +491,7 @@ class Roles(metaclass=YAMLGetter): domain_leads: int helpers: int moderators: int + mod_team: int owners: int project_leads: int diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py new file mode 100644 index 000000000..13be016f2 --- /dev/null +++ b/bot/exts/moderation/duty.py @@ -0,0 +1,135 @@ +import datetime +import logging + +from async_rediscache import RedisCache +from dateutil.parser import isoparse +from discord import Member +from discord.ext.commands import Cog, Context, group, has_any_role + +from bot.bot import Bot +from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles +from bot.converters import Expiry +from bot.utils.scheduling import Scheduler + + +log = logging.getLogger(__name__) + + +class Duty(Cog): + """Commands for a moderator to go on and off duty.""" + + # RedisCache[str, str] + # The cache's keys are mods who are off-duty. + # The cache's values are the times when the role should be re-applied to them, stored in ISO format. + off_duty_mods = RedisCache() + + def __init__(self, bot: Bot): + self.bot = bot + self._role_scheduler = Scheduler(self.__class__.__name__) + + self.guild = None + self.moderators_role = None + + self.bot.loop.create_task(self.reschedule_roles()) + + async def reschedule_roles(self) -> None: + """Reschedule moderators role re-apply times.""" + await self.bot.wait_until_guild_available() + self.guild = self.bot.get_guild(Guild.id) + self.moderators_role = self.guild.get_role(Roles.moderators) + + mod_team = self.guild.get_role(Roles.mod_team) + on_duty = self.moderators_role.members + off_duty = await self.off_duty_mods.to_dict() + + log.trace("Applying the moderators role to the mod team where necessary.") + for mod in mod_team.members: + if mod in on_duty: # Make sure that on-duty mods aren't in the cache. + if mod in off_duty: + await self.off_duty_mods.delete(mod.id) + continue + + # Keep the role off only for those in the cache. + if mod.id not in off_duty: + await self.reapply_role(mod) + else: + expiry = isoparse(off_duty[mod.id]).replace(tzinfo=None) + self._role_scheduler.schedule_at(expiry, mod.id, self.reapply_role(mod)) + + 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}.") + await mod.add_roles(self.moderators_role, reason="Off-duty period expired.") + + @group(name='duty', invoke_without_command=True) + @has_any_role(*MODERATION_ROLES) + async def duty_group(self, ctx: Context) -> None: + """Allow the removal and re-addition of the pingable moderators role.""" + await ctx.send_help(ctx.command) + + @duty_group.command(name='off') + @has_any_role(*MODERATION_ROLES) + async def off_command(self, ctx: Context, duration: Expiry) -> None: + """ + Temporarily removes the pingable moderators role for a set amount of time. + + A unit of time should be appended to the duration. + Units (∗case-sensitive): + \u2003`y` - years + \u2003`m` - months∗ + \u2003`w` - weeks + \u2003`d` - days + \u2003`h` - hours + \u2003`M` - minutes∗ + \u2003`s` - seconds + + Alternatively, an ISO 8601 timestamp can be provided for the duration. + + The duration cannot be longer than 30 days. + """ + duration: datetime.datetime + delta = duration - datetime.datetime.utcnow() + if delta > datetime.timedelta(days=30): + await ctx.send(":x: Cannot remove the role for longer than 30 days.") + return + + mod = ctx.author + + await mod.remove_roles(self.moderators_role, reason="Entered off-duty period.") + + await self.off_duty_mods.update({mod.id: duration.isoformat()}) + + if mod.id in self._role_scheduler: + self._role_scheduler.cancel(mod.id) + self._role_scheduler.schedule_at(duration, mod.id, self.reapply_role(mod)) + + until_date = duration.replace(microsecond=0).isoformat() + await ctx.send(f"{Emojis.check_mark} Moderators role has been removed until {until_date}.") + + @duty_group.command(name='on') + @has_any_role(*MODERATION_ROLES) + async def on_command(self, ctx: Context) -> None: + """Re-apply the pingable moderators role.""" + mod = ctx.author + if mod in self.moderators_role.members: + await ctx.send(":question: You already have the role.") + return + + await mod.add_roles(self.moderators_role, reason="Off-duty period canceled.") + + await self.off_duty_mods.delete(mod.id) + + if mod.id in self._role_scheduler: + self._role_scheduler.cancel(mod.id) + + await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") + + def cog_unload(self) -> None: + """Cancel role tasks when the cog unloads.""" + log.trace("Cog unload: canceling role tasks.") + self._role_scheduler.cancel_all() + + +def setup(bot: Bot) -> None: + """Load the Slowmode cog.""" + bot.add_cog(Duty(bot)) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 2dae9d268..f68a1880e 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -14,7 +14,7 @@ from discord.abc import GuildChannel from discord.ext.commands import Cog, Context from bot.bot import Bot -from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, URLs +from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, Roles, URLs from bot.utils.messages import format_user from bot.utils.time import humanize_delta @@ -115,9 +115,9 @@ class ModLog(Cog, name="ModLog"): if ping_everyone: if content: - content = f"@everyone\n{content}" + content = f"<@&{Roles.moderators}> @here\n{content}" else: - content = "@everyone" + content = f"<@&{Roles.moderators}> @here" # Truncate content to 2000 characters and append an ellipsis. if content and len(content) > 2000: diff --git a/config-default.yml b/config-default.yml index 8c6e18470..6eb954cd5 100644 --- a/config-default.yml +++ b/config-default.yml @@ -260,7 +260,8 @@ guild: devops: 409416496733880320 domain_leads: 807415650778742785 helpers: &HELPERS_ROLE 267630620367257601 - moderators: &MODS_ROLE 267629731250176001 + moderators: &MODS_ROLE 831776746206265384 + mod_team: &MOD_TEAM_ROLE 267629731250176001 owners: &OWNERS_ROLE 267627879762755584 project_leads: 815701647526330398 @@ -274,12 +275,14 @@ guild: moderation_roles: - *ADMINS_ROLE - *MODS_ROLE + - *MOD_TEAM_ROLE - *OWNERS_ROLE staff_roles: - *ADMINS_ROLE - *HELPERS_ROLE - *MODS_ROLE + - *MOD_TEAM_ROLE - *OWNERS_ROLE webhooks: -- cgit v1.2.3 From 65df8e24874cda7b9525acde346199f66e59650f Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Thu, 15 Apr 2021 00:55:29 +0300 Subject: Remove extra newline Co-authored-by: ks129 <45097959+ks129@users.noreply.github.com> --- bot/exts/moderation/duty.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 13be016f2..94eed9331 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -11,7 +11,6 @@ from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles from bot.converters import Expiry from bot.utils.scheduling import Scheduler - log = logging.getLogger(__name__) -- cgit v1.2.3 From 38714aef8c5b71c5e8313a82bef18947f1f1395a Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 15 Apr 2021 01:00:31 +0300 Subject: Fix setup docstring to specify correct cog --- bot/exts/moderation/duty.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 94eed9331..3f34e366c 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -130,5 +130,5 @@ class Duty(Cog): def setup(bot: Bot) -> None: - """Load the Slowmode cog.""" + """Load the Duty cog.""" bot.add_cog(Duty(bot)) -- cgit v1.2.3 From 6c00f74c8dcd2f3f1aaa4eff89e72cc135b75357 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 15 Apr 2021 01:04:16 +0300 Subject: Add off-duty expiration date to audit log --- bot/exts/moderation/duty.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 3f34e366c..265261be8 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -94,7 +94,8 @@ class Duty(Cog): mod = ctx.author - await mod.remove_roles(self.moderators_role, reason="Entered off-duty period.") + until_date = duration.replace(microsecond=0).isoformat() + await mod.remove_roles(self.moderators_role, reason=f"Entered off-duty period until {until_date}.") await self.off_duty_mods.update({mod.id: duration.isoformat()}) @@ -102,7 +103,6 @@ class Duty(Cog): self._role_scheduler.cancel(mod.id) self._role_scheduler.schedule_at(duration, mod.id, self.reapply_role(mod)) - until_date = duration.replace(microsecond=0).isoformat() await ctx.send(f"{Emojis.check_mark} Moderators role has been removed until {until_date}.") @duty_group.command(name='on') -- cgit v1.2.3 From b5fbca6f32c437aa45e28916451de39fb1485a75 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Thu, 15 Apr 2021 01:10:37 +0300 Subject: Use set instead of update in duty off --- bot/exts/moderation/duty.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 265261be8..0b07510db 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -97,7 +97,7 @@ class Duty(Cog): until_date = duration.replace(microsecond=0).isoformat() await mod.remove_roles(self.moderators_role, reason=f"Entered off-duty period until {until_date}.") - await self.off_duty_mods.update({mod.id: duration.isoformat()}) + await self.off_duty_mods.set(mod.id, duration.isoformat()) if mod.id in self._role_scheduler: self._role_scheduler.cancel(mod.id) -- cgit v1.2.3 From f11ebfde17634eed7fa242f72b309c4a75c885cd Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 17 Apr 2021 01:15:38 +0300 Subject: Keep config succint A moderator is expected to have the mod-team role and therefore it's enough to specify the latter in the mod and staff roles. --- config-default.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/config-default.yml b/config-default.yml index 6eb954cd5..b19164d3f 100644 --- a/config-default.yml +++ b/config-default.yml @@ -274,14 +274,12 @@ guild: moderation_roles: - *ADMINS_ROLE - - *MODS_ROLE - *MOD_TEAM_ROLE - *OWNERS_ROLE staff_roles: - *ADMINS_ROLE - *HELPERS_ROLE - - *MODS_ROLE - *MOD_TEAM_ROLE - *OWNERS_ROLE -- cgit v1.2.3 From 2053b2e36ece02680ed85b970c4fbf687fe07e0f Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 17 Apr 2021 01:19:54 +0300 Subject: Assume a scheduled task exists for `duty on` The lack of such a task may be indicative of a bug. --- bot/exts/moderation/duty.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 0b07510db..8d0c96363 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -118,8 +118,7 @@ class Duty(Cog): await self.off_duty_mods.delete(mod.id) - if mod.id in self._role_scheduler: - self._role_scheduler.cancel(mod.id) + self._role_scheduler.cancel(mod.id) await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") -- cgit v1.2.3 From 5506fb74f90831e686f4636595f62e4bcc72a703 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 17 Apr 2021 01:24:17 +0300 Subject: Improve documentation --- bot/exts/moderation/duty.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index 8d0c96363..eab0fd99f 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -94,11 +94,12 @@ class Duty(Cog): mod = ctx.author - until_date = duration.replace(microsecond=0).isoformat() + until_date = duration.replace(microsecond=0).isoformat() # Looks noisy with microseconds. await mod.remove_roles(self.moderators_role, reason=f"Entered off-duty period until {until_date}.") await self.off_duty_mods.set(mod.id, duration.isoformat()) + # Allow rescheduling the task without cancelling it separately via the `on` command. if mod.id in self._role_scheduler: self._role_scheduler.cancel(mod.id) self._role_scheduler.schedule_at(duration, mod.id, self.reapply_role(mod)) @@ -118,6 +119,7 @@ class Duty(Cog): await self.off_duty_mods.delete(mod.id) + # We assume the task exists. Lack of it may indicate a bug. self._role_scheduler.cancel(mod.id) await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") -- cgit v1.2.3 From 4a051cdb016748daca724e95957bd011cc3f6c3f Mon Sep 17 00:00:00 2001 From: mbaruh Date: Sat, 17 Apr 2021 01:43:17 +0300 Subject: Name the rescheduling task, and cancel it on cog unload --- bot/exts/moderation/duty.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index eab0fd99f..e05472448 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -29,7 +29,7 @@ class Duty(Cog): self.guild = None self.moderators_role = None - self.bot.loop.create_task(self.reschedule_roles()) + self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="duty-reschedule") async def reschedule_roles(self) -> None: """Reschedule moderators role re-apply times.""" @@ -127,6 +127,7 @@ class Duty(Cog): def cog_unload(self) -> None: """Cancel role tasks when the cog unloads.""" log.trace("Cog unload: canceling role tasks.") + self.reschedule_task.cancel() self._role_scheduler.cancel_all() -- cgit v1.2.3 From d2d939c96de22ae174072dd8cc2bad2fe4f2174a Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Sat, 17 Apr 2021 13:19:08 +0300 Subject: Remove here ping Kinda defeats the purpose of being off-duty. --- bot/exts/moderation/modlog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index f68a1880e..5e8ea595b 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -115,9 +115,9 @@ class ModLog(Cog, name="ModLog"): if ping_everyone: if content: - content = f"<@&{Roles.moderators}> @here\n{content}" + content = f"<@&{Roles.moderators}>\n{content}" else: - content = f"<@&{Roles.moderators}> @here" + content = f"<@&{Roles.moderators}>" # Truncate content to 2000 characters and append an ellipsis. if content and len(content) > 2000: -- cgit v1.2.3 From bd54449e8994c38b2fd073056f82e6c52785d4c6 Mon Sep 17 00:00:00 2001 From: mbaruh Date: Mon, 19 Apr 2021 15:43:33 +0300 Subject: Renamed Duty cog to Modpings The renaming includes the commands inside it. --- bot/exts/moderation/duty.py | 46 ++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py index e05472448..c351db615 100644 --- a/bot/exts/moderation/duty.py +++ b/bot/exts/moderation/duty.py @@ -14,13 +14,13 @@ from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) -class Duty(Cog): - """Commands for a moderator to go on and off duty.""" +class Modpings(Cog): + """Commands for a moderator to turn moderator pings on and off.""" # RedisCache[str, str] - # The cache's keys are mods who are off-duty. + # The cache's keys are mods who have pings off. # The cache's values are the times when the role should be re-applied to them, stored in ISO format. - off_duty_mods = RedisCache() + pings_off_mods = RedisCache() def __init__(self, bot: Bot): self.bot = bot @@ -29,7 +29,7 @@ class Duty(Cog): self.guild = None self.moderators_role = None - self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="duty-reschedule") + self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="mod-pings-reschedule") async def reschedule_roles(self) -> None: """Reschedule moderators role re-apply times.""" @@ -38,35 +38,35 @@ class Duty(Cog): self.moderators_role = self.guild.get_role(Roles.moderators) mod_team = self.guild.get_role(Roles.mod_team) - on_duty = self.moderators_role.members - off_duty = await self.off_duty_mods.to_dict() + pings_on = self.moderators_role.members + pings_off = await self.pings_off_mods.to_dict() log.trace("Applying the moderators role to the mod team where necessary.") for mod in mod_team.members: - if mod in on_duty: # Make sure that on-duty mods aren't in the cache. - if mod in off_duty: - await self.off_duty_mods.delete(mod.id) + if mod in pings_on: # Make sure that on-duty mods aren't in the cache. + if mod in pings_off: + await self.pings_off_mods.delete(mod.id) continue # Keep the role off only for those in the cache. - if mod.id not in off_duty: + if mod.id not in pings_off: await self.reapply_role(mod) else: - expiry = isoparse(off_duty[mod.id]).replace(tzinfo=None) + expiry = isoparse(pings_off[mod.id]).replace(tzinfo=None) self._role_scheduler.schedule_at(expiry, mod.id, self.reapply_role(mod)) 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}.") - await mod.add_roles(self.moderators_role, reason="Off-duty period expired.") + await mod.add_roles(self.moderators_role, reason="Pings off period expired.") - @group(name='duty', invoke_without_command=True) + @group(name='modpings', aliases=('modping',), invoke_without_command=True) @has_any_role(*MODERATION_ROLES) - async def duty_group(self, ctx: Context) -> None: + async def modpings_group(self, ctx: Context) -> None: """Allow the removal and re-addition of the pingable moderators role.""" await ctx.send_help(ctx.command) - @duty_group.command(name='off') + @modpings_group.command(name='off') @has_any_role(*MODERATION_ROLES) async def off_command(self, ctx: Context, duration: Expiry) -> None: """ @@ -95,9 +95,9 @@ class Duty(Cog): mod = ctx.author until_date = duration.replace(microsecond=0).isoformat() # Looks noisy with microseconds. - await mod.remove_roles(self.moderators_role, reason=f"Entered off-duty period until {until_date}.") + await mod.remove_roles(self.moderators_role, reason=f"Turned pings off until {until_date}.") - await self.off_duty_mods.set(mod.id, duration.isoformat()) + await self.pings_off_mods.set(mod.id, duration.isoformat()) # Allow rescheduling the task without cancelling it separately via the `on` command. if mod.id in self._role_scheduler: @@ -106,7 +106,7 @@ class Duty(Cog): await ctx.send(f"{Emojis.check_mark} Moderators role has been removed until {until_date}.") - @duty_group.command(name='on') + @modpings_group.command(name='on') @has_any_role(*MODERATION_ROLES) async def on_command(self, ctx: Context) -> None: """Re-apply the pingable moderators role.""" @@ -115,9 +115,9 @@ class Duty(Cog): await ctx.send(":question: You already have the role.") return - await mod.add_roles(self.moderators_role, reason="Off-duty period canceled.") + await mod.add_roles(self.moderators_role, reason="Pings off period canceled.") - await self.off_duty_mods.delete(mod.id) + await self.pings_off_mods.delete(mod.id) # We assume the task exists. Lack of it may indicate a bug. self._role_scheduler.cancel(mod.id) @@ -132,5 +132,5 @@ class Duty(Cog): def setup(bot: Bot) -> None: - """Load the Duty cog.""" - bot.add_cog(Duty(bot)) + """Load the Modpings cog.""" + bot.add_cog(Modpings(bot)) -- cgit v1.2.3 From e30667fb4e23648c3f308bfc06cf643852d0c29c Mon Sep 17 00:00:00 2001 From: mbaruh Date: Mon, 19 Apr 2021 15:44:58 +0300 Subject: Renamed duty.py to modpings.py --- bot/exts/moderation/duty.py | 136 ---------------------------------------- bot/exts/moderation/modpings.py | 136 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 136 deletions(-) delete mode 100644 bot/exts/moderation/duty.py create mode 100644 bot/exts/moderation/modpings.py diff --git a/bot/exts/moderation/duty.py b/bot/exts/moderation/duty.py deleted file mode 100644 index c351db615..000000000 --- a/bot/exts/moderation/duty.py +++ /dev/null @@ -1,136 +0,0 @@ -import datetime -import logging - -from async_rediscache import RedisCache -from dateutil.parser import isoparse -from discord import Member -from discord.ext.commands import Cog, Context, group, has_any_role - -from bot.bot import Bot -from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles -from bot.converters import Expiry -from bot.utils.scheduling import Scheduler - -log = logging.getLogger(__name__) - - -class Modpings(Cog): - """Commands for a moderator to turn moderator pings on and off.""" - - # RedisCache[str, str] - # The cache's keys are mods who have pings off. - # The cache's values are the times when the role should be re-applied to them, stored in ISO format. - pings_off_mods = RedisCache() - - def __init__(self, bot: Bot): - self.bot = bot - self._role_scheduler = Scheduler(self.__class__.__name__) - - self.guild = None - self.moderators_role = None - - self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="mod-pings-reschedule") - - async def reschedule_roles(self) -> None: - """Reschedule moderators role re-apply times.""" - await self.bot.wait_until_guild_available() - self.guild = self.bot.get_guild(Guild.id) - self.moderators_role = self.guild.get_role(Roles.moderators) - - mod_team = self.guild.get_role(Roles.mod_team) - pings_on = self.moderators_role.members - pings_off = await self.pings_off_mods.to_dict() - - log.trace("Applying the moderators role to the mod team where necessary.") - for mod in mod_team.members: - if mod in pings_on: # Make sure that on-duty mods aren't in the cache. - if mod in pings_off: - await self.pings_off_mods.delete(mod.id) - continue - - # Keep the role off only for those in the cache. - if mod.id not in pings_off: - await self.reapply_role(mod) - else: - expiry = isoparse(pings_off[mod.id]).replace(tzinfo=None) - self._role_scheduler.schedule_at(expiry, mod.id, self.reapply_role(mod)) - - 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}.") - await mod.add_roles(self.moderators_role, reason="Pings off period expired.") - - @group(name='modpings', aliases=('modping',), invoke_without_command=True) - @has_any_role(*MODERATION_ROLES) - async def modpings_group(self, ctx: Context) -> None: - """Allow the removal and re-addition of the pingable moderators role.""" - await ctx.send_help(ctx.command) - - @modpings_group.command(name='off') - @has_any_role(*MODERATION_ROLES) - async def off_command(self, ctx: Context, duration: Expiry) -> None: - """ - Temporarily removes the pingable moderators role for a set amount of time. - - A unit of time should be appended to the duration. - Units (∗case-sensitive): - \u2003`y` - years - \u2003`m` - months∗ - \u2003`w` - weeks - \u2003`d` - days - \u2003`h` - hours - \u2003`M` - minutes∗ - \u2003`s` - seconds - - Alternatively, an ISO 8601 timestamp can be provided for the duration. - - The duration cannot be longer than 30 days. - """ - duration: datetime.datetime - delta = duration - datetime.datetime.utcnow() - if delta > datetime.timedelta(days=30): - await ctx.send(":x: Cannot remove the role for longer than 30 days.") - return - - mod = ctx.author - - until_date = duration.replace(microsecond=0).isoformat() # Looks noisy with microseconds. - await mod.remove_roles(self.moderators_role, reason=f"Turned pings off until {until_date}.") - - await self.pings_off_mods.set(mod.id, duration.isoformat()) - - # Allow rescheduling the task without cancelling it separately via the `on` command. - if mod.id in self._role_scheduler: - self._role_scheduler.cancel(mod.id) - self._role_scheduler.schedule_at(duration, mod.id, self.reapply_role(mod)) - - await ctx.send(f"{Emojis.check_mark} Moderators role has been removed until {until_date}.") - - @modpings_group.command(name='on') - @has_any_role(*MODERATION_ROLES) - async def on_command(self, ctx: Context) -> None: - """Re-apply the pingable moderators role.""" - mod = ctx.author - if mod in self.moderators_role.members: - await ctx.send(":question: You already have the role.") - return - - await mod.add_roles(self.moderators_role, reason="Pings off period canceled.") - - await self.pings_off_mods.delete(mod.id) - - # We assume the task exists. Lack of it may indicate a bug. - self._role_scheduler.cancel(mod.id) - - await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") - - def cog_unload(self) -> None: - """Cancel role tasks when the cog unloads.""" - log.trace("Cog unload: canceling role tasks.") - self.reschedule_task.cancel() - self._role_scheduler.cancel_all() - - -def setup(bot: Bot) -> None: - """Load the Modpings cog.""" - bot.add_cog(Modpings(bot)) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py new file mode 100644 index 000000000..c351db615 --- /dev/null +++ b/bot/exts/moderation/modpings.py @@ -0,0 +1,136 @@ +import datetime +import logging + +from async_rediscache import RedisCache +from dateutil.parser import isoparse +from discord import Member +from discord.ext.commands import Cog, Context, group, has_any_role + +from bot.bot import Bot +from bot.constants import Emojis, Guild, MODERATION_ROLES, Roles +from bot.converters import Expiry +from bot.utils.scheduling import Scheduler + +log = logging.getLogger(__name__) + + +class Modpings(Cog): + """Commands for a moderator to turn moderator pings on and off.""" + + # RedisCache[str, str] + # The cache's keys are mods who have pings off. + # The cache's values are the times when the role should be re-applied to them, stored in ISO format. + pings_off_mods = RedisCache() + + def __init__(self, bot: Bot): + self.bot = bot + self._role_scheduler = Scheduler(self.__class__.__name__) + + self.guild = None + self.moderators_role = None + + self.reschedule_task = self.bot.loop.create_task(self.reschedule_roles(), name="mod-pings-reschedule") + + async def reschedule_roles(self) -> None: + """Reschedule moderators role re-apply times.""" + await self.bot.wait_until_guild_available() + self.guild = self.bot.get_guild(Guild.id) + self.moderators_role = self.guild.get_role(Roles.moderators) + + mod_team = self.guild.get_role(Roles.mod_team) + pings_on = self.moderators_role.members + pings_off = await self.pings_off_mods.to_dict() + + log.trace("Applying the moderators role to the mod team where necessary.") + for mod in mod_team.members: + if mod in pings_on: # Make sure that on-duty mods aren't in the cache. + if mod in pings_off: + await self.pings_off_mods.delete(mod.id) + continue + + # Keep the role off only for those in the cache. + if mod.id not in pings_off: + await self.reapply_role(mod) + else: + expiry = isoparse(pings_off[mod.id]).replace(tzinfo=None) + self._role_scheduler.schedule_at(expiry, mod.id, self.reapply_role(mod)) + + 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}.") + await mod.add_roles(self.moderators_role, reason="Pings off period expired.") + + @group(name='modpings', aliases=('modping',), invoke_without_command=True) + @has_any_role(*MODERATION_ROLES) + async def modpings_group(self, ctx: Context) -> None: + """Allow the removal and re-addition of the pingable moderators role.""" + await ctx.send_help(ctx.command) + + @modpings_group.command(name='off') + @has_any_role(*MODERATION_ROLES) + async def off_command(self, ctx: Context, duration: Expiry) -> None: + """ + Temporarily removes the pingable moderators role for a set amount of time. + + A unit of time should be appended to the duration. + Units (∗case-sensitive): + \u2003`y` - years + \u2003`m` - months∗ + \u2003`w` - weeks + \u2003`d` - days + \u2003`h` - hours + \u2003`M` - minutes∗ + \u2003`s` - seconds + + Alternatively, an ISO 8601 timestamp can be provided for the duration. + + The duration cannot be longer than 30 days. + """ + duration: datetime.datetime + delta = duration - datetime.datetime.utcnow() + if delta > datetime.timedelta(days=30): + await ctx.send(":x: Cannot remove the role for longer than 30 days.") + return + + mod = ctx.author + + until_date = duration.replace(microsecond=0).isoformat() # Looks noisy with microseconds. + await mod.remove_roles(self.moderators_role, reason=f"Turned pings off until {until_date}.") + + await self.pings_off_mods.set(mod.id, duration.isoformat()) + + # Allow rescheduling the task without cancelling it separately via the `on` command. + if mod.id in self._role_scheduler: + self._role_scheduler.cancel(mod.id) + self._role_scheduler.schedule_at(duration, mod.id, self.reapply_role(mod)) + + await ctx.send(f"{Emojis.check_mark} Moderators role has been removed until {until_date}.") + + @modpings_group.command(name='on') + @has_any_role(*MODERATION_ROLES) + async def on_command(self, ctx: Context) -> None: + """Re-apply the pingable moderators role.""" + mod = ctx.author + if mod in self.moderators_role.members: + await ctx.send(":question: You already have the role.") + return + + await mod.add_roles(self.moderators_role, reason="Pings off period canceled.") + + await self.pings_off_mods.delete(mod.id) + + # We assume the task exists. Lack of it may indicate a bug. + self._role_scheduler.cancel(mod.id) + + await ctx.send(f"{Emojis.check_mark} Moderators role has been re-applied.") + + def cog_unload(self) -> None: + """Cancel role tasks when the cog unloads.""" + log.trace("Cog unload: canceling role tasks.") + self.reschedule_task.cancel() + self._role_scheduler.cancel_all() + + +def setup(bot: Bot) -> None: + """Load the Modpings cog.""" + bot.add_cog(Modpings(bot)) -- cgit v1.2.3 From a7581a4f9f2724672eebfdf541a922973c018c23 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Mon, 19 Apr 2021 20:48:26 +0300 Subject: CamelCase the cog name --- bot/exts/moderation/modpings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py index c351db615..690aa7c68 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -14,7 +14,7 @@ from bot.utils.scheduling import Scheduler log = logging.getLogger(__name__) -class Modpings(Cog): +class ModPings(Cog): """Commands for a moderator to turn moderator pings on and off.""" # RedisCache[str, str] @@ -132,5 +132,5 @@ class Modpings(Cog): def setup(bot: Bot) -> None: - """Load the Modpings cog.""" - bot.add_cog(Modpings(bot)) + """Load the ModPings cog.""" + bot.add_cog(ModPings(bot)) -- cgit v1.2.3 From b12666dc4b75146b150c0812c5cb56f4317773ae Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Tue, 20 Apr 2021 18:48:12 +0300 Subject: Improve rediscache doc Co-authored-by: ChrisJL --- 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 690aa7c68..2f180e594 100644 --- a/bot/exts/moderation/modpings.py +++ b/bot/exts/moderation/modpings.py @@ -17,7 +17,7 @@ log = logging.getLogger(__name__) class ModPings(Cog): """Commands for a moderator to turn moderator pings on and off.""" - # RedisCache[str, str] + # RedisCache[discord.Member.id, 'Naïve ISO 8601 string'] # The cache's keys are mods who have pings off. # The cache's values are the times when the role should be re-applied to them, stored in ISO format. pings_off_mods = RedisCache() -- cgit v1.2.3 From 8a73d2b5e71444595b72155d7106c0fc48eeb027 Mon Sep 17 00:00:00 2001 From: Boris Muratov <8bee278@gmail.com> Date: Tue, 20 Apr 2021 19:14:10 +0300 Subject: Remove allowed mentions in modlog alert The modlog alert embed no longer pings everyone. --- bot/exts/moderation/modlog.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py index 5e8ea595b..e92f76c9a 100644 --- a/bot/exts/moderation/modlog.py +++ b/bot/exts/moderation/modlog.py @@ -127,8 +127,7 @@ class ModLog(Cog, name="ModLog"): log_message = await channel.send( content=content, embed=embed, - files=files, - allowed_mentions=discord.AllowedMentions(everyone=True) + files=files ) if additional_embeds: -- cgit v1.2.3 From c20f84ff95671527e6fbacb04f07bcee3baaafcd Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Tue, 20 Apr 2021 17:54:44 +0100 Subject: Add the Moderators role to moderation_roles in config This allows mod alert pings to go through in #mod-alerts, the allowed mentions only included the mods team role which is not pinged on mod alerts. --- config-default.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config-default.yml b/config-default.yml index b19164d3f..b7c446889 100644 --- a/config-default.yml +++ b/config-default.yml @@ -275,6 +275,7 @@ guild: moderation_roles: - *ADMINS_ROLE - *MOD_TEAM_ROLE + - *MODS_ROLE - *OWNERS_ROLE staff_roles: -- cgit v1.2.3