aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar mbaruh <[email protected]>2021-04-14 20:05:04 +0300
committerGravatar mbaruh <[email protected]>2021-04-14 20:05:04 +0300
commite06f496a6e3f9a9d6cfaeb3902547aa9da1dd7c1 (patch)
tree40ab5afcfe0244649b4ac1929c5dfe94c766f5e3
parentMerge pull request #1521 from ToxicKidz/dont-use-startswith (diff)
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.
-rw-r--r--bot/constants.py1
-rw-r--r--bot/exts/moderation/duty.py135
-rw-r--r--bot/exts/moderation/modlog.py6
-rw-r--r--config-default.yml5
4 files changed, 143 insertions, 4 deletions
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: