diff options
author | 2018-11-17 00:18:34 +0000 | |
---|---|---|
committer | 2018-11-17 00:18:34 +0000 | |
commit | 7e799a9d50bed230a3b04600692c4a5d4cf2b398 (patch) | |
tree | dfbead858984ea4568bd9a2473f7c11c86d8c31b | |
parent | Reitz sez: Try --sequential (diff) | |
parent | Send users a message when they're given an infraction. (diff) |
Merge branch 'infraction-notifs' into 'master'
Send users a message when they're given an infraction.
See merge request python-discord/projects/bot!49
-rw-r--r-- | bot/cogs/moderation.py | 191 | ||||
-rw-r--r-- | bot/cogs/superstarify.py | 22 | ||||
-rw-r--r-- | bot/constants.py | 1 | ||||
-rw-r--r-- | config-default.yml | 1 |
4 files changed, 203 insertions, 12 deletions
diff --git a/bot/cogs/moderation.py b/bot/cogs/moderation.py index 2ab59f12d..4afeeb768 100644 --- a/bot/cogs/moderation.py +++ b/bot/cogs/moderation.py @@ -1,10 +1,12 @@ import asyncio import logging import textwrap -import typing +from typing import Union from aiohttp import ClientError -from discord import Colour, Embed, Guild, Member, Object, User +from discord import ( + Colour, Embed, Forbidden, Guild, HTTPException, Member, Object, User +) from discord.ext.commands import ( BadArgument, BadUnionArgument, Bot, Context, command, group ) @@ -15,12 +17,17 @@ from bot.constants import Colours, Event, Icons, Keys, Roles, URLs from bot.converters import InfractionSearchQuery from bot.decorators import with_role from bot.pagination import LinePaginator -from bot.utils.scheduling import Scheduler +from bot.utils.scheduling import Scheduler, create_task from bot.utils.time import parse_rfc1123, wait_until log = logging.getLogger(__name__) MODERATION_ROLES = Roles.owner, Roles.admin, Roles.moderator +INFRACTION_ICONS = { + "Mute": Icons.user_mute, + "Kick": Icons.sign_out, + "Ban": Icons.user_ban +} def proxy_user(user_id: str) -> Object: @@ -66,13 +73,19 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command(name="warn") - async def warn(self, ctx: Context, user: typing.Union[User, proxy_user], *, reason: str = None): + async def warn(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None): """ Create a warning infraction in the database for a user. :param user: accepts user mention, ID, etc. :param reason: The reason for the warning. """ + await self.notify_infraction( + user=user, + infr_type="Warning", + reason=reason + ) + try: response = await self.bot.http_session.post( URLs.site_infractions, @@ -110,6 +123,12 @@ class Moderation(Scheduler): :param reason: The reason for the kick. """ + await self.notify_infraction( + user=user, + infr_type="Kick", + reason=reason + ) + try: response = await self.bot.http_session.post( URLs.site_infractions, @@ -156,13 +175,20 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command(name="ban") - async def ban(self, ctx: Context, user: typing.Union[User, proxy_user], *, reason: str = None): + async def ban(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None): """ Create a permanent ban infraction in the database for a user. :param user: Accepts user mention, ID, etc. :param reason: The reason for the ban. """ + await self.notify_infraction( + user=user, + infr_type="Ban", + duration="Permanent", + reason=reason + ) + try: response = await self.bot.http_session.post( URLs.site_infractions, @@ -217,6 +243,13 @@ class Moderation(Scheduler): :param reason: The reason for the mute. """ + await self.notify_infraction( + user=user, + infr_type="Mute", + duration="Permanent", + reason=reason + ) + try: response = await self.bot.http_session.post( URLs.site_infractions, @@ -275,6 +308,13 @@ class Moderation(Scheduler): :param reason: The reason for the temporary mute. """ + await self.notify_infraction( + user=user, + infr_type="Mute", + duration=duration, + reason=reason + ) + try: response = await self.bot.http_session.post( URLs.site_infractions, @@ -330,7 +370,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command(name="tempban") - async def tempban(self, ctx: Context, user: typing.Union[User, proxy_user], duration: str, *, reason: str = None): + async def tempban(self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None): """ Create a temporary ban infraction in the database for a user. :param user: Accepts user mention, ID, etc. @@ -338,6 +378,13 @@ class Moderation(Scheduler): :param reason: The reason for the temporary ban. """ + await self.notify_infraction( + user=user, + infr_type="Ban", + duration=duration, + reason=reason + ) + try: response = await self.bot.http_session.post( URLs.site_infractions, @@ -398,7 +445,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command(name="shadow_warn", hidden=True, aliases=['shadowwarn', 'swarn', 'note']) - async def shadow_warn(self, ctx: Context, user: typing.Union[User, proxy_user], *, reason: str = None): + async def shadow_warn(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None): """ Create a warning infraction in the database for a user. :param user: accepts user mention, ID, etc. @@ -490,7 +537,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command(name="shadow_ban", hidden=True, aliases=['shadowban', 'sban']) - async def shadow_ban(self, ctx: Context, user: typing.Union[User, proxy_user], *, reason: str = None): + async def shadow_ban(self, ctx: Context, user: Union[User, proxy_user], *, reason: str = None): """ Create a permanent ban infraction in the database for a user. :param user: Accepts user mention, ID, etc. @@ -668,7 +715,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command(name="shadow_tempban", hidden=True, aliases=["shadowtempban, stempban"]) async def shadow_tempban( - self, ctx: Context, user: typing.Union[User, proxy_user], duration: str, *, reason: str = None + self, ctx: Context, user: Union[User, proxy_user], duration: str, *, reason: str = None ): """ Create a temporary ban infraction in the database for a user. @@ -782,6 +829,13 @@ class Moderation(Scheduler): Intended expiry: {infraction_object['expires_at']} """) ) + + await self.notify_pardon( + user=user, + title="You have been unmuted.", + content="You may now send messages in the server.", + icon_url=Icons.user_unmute + ) except Exception: log.exception("There was an error removing an infraction.") await ctx.send(":x: There was an error removing the infraction.") @@ -789,7 +843,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @command(name="unban") - async def unban(self, ctx: Context, user: typing.Union[User, proxy_user]): + async def unban(self, ctx: Context, user: Union[User, proxy_user]): """ Deactivates the active ban infraction for a user. :param user: Accepts user mention, ID, etc. @@ -1026,7 +1080,7 @@ class Moderation(Scheduler): @with_role(*MODERATION_ROLES) @infraction_search_group.command(name="user", aliases=("member", "id")) - async def search_user(self, ctx: Context, user: typing.Union[User, proxy_user]): + async def search_user(self, ctx: Context, user: Union[User, proxy_user]): """ Search for infractions by member. """ @@ -1102,6 +1156,38 @@ class Moderation(Scheduler): max_size=1000 ) + # endregion + # region: Utility functions + + def schedule_expiration(self, loop: asyncio.AbstractEventLoop, infraction_object: dict): + """ + Schedules a task to expire a temporary infraction. + :param loop: the asyncio event loop + :param infraction_object: the infraction object to expire at the end of the task + """ + + infraction_id = infraction_object["id"] + if infraction_id in self.scheduled_tasks: + return + + task: asyncio.Task = create_task(loop, self._scheduled_expiration(infraction_object)) + + self.scheduled_tasks[infraction_id] = task + + def cancel_expiration(self, infraction_id: str): + """ + Un-schedules a task set to expire a temporary infraction. + :param infraction_id: the ID of the infraction in question + """ + + task = self.scheduled_tasks.get(infraction_id) + if task is None: + log.warning(f"Failed to unschedule {infraction_id}: no task found.") + return + task.cancel() + log.debug(f"Unscheduled {infraction_id}.") + del self.scheduled_tasks[infraction_id] + async def _scheduled_task(self, infraction_object: dict): """ A co-routine which marks an infraction as expired after the delay from the time of scheduling @@ -1121,6 +1207,16 @@ class Moderation(Scheduler): self.cancel_task(infraction_object["id"]) + # Notify the user that they've been unmuted. + user_id = int(infraction_object["user"]["user_id"]) + guild = self.bot.get_guild(constants.Guild.id) + await self.notify_pardon( + user=guild.get_member(user_id), + title="You have been unmuted.", + content="You may now send messages in the server.", + icon_url=Icons.user_unmute + ) + async def _deactivate_infraction(self, infraction_object): """ A co-routine which marks an infraction as inactive on the website. This co-routine does not cancel or @@ -1177,6 +1273,79 @@ class Moderation(Scheduler): return lines.strip() + async def notify_infraction( + self, user: Union[User, Member], infr_type: str, duration: str = None, reason: str = None + ): + """ + Notify a user of their fresh infraction :) + + :param user: The user to send the message to. + :param infr_type: The type of infraction, as a string. + :param duration: The duration of the infraction. + :param reason: The reason for the infraction. + """ + + if duration is None: + duration = "N/A" + + if reason is None: + reason = "No reason provided." + + embed = Embed( + description=textwrap.dedent(f""" + **Type:** {infr_type} + **Duration:** {duration} + **Reason:** {reason} + """), + colour=Colour(Colours.soft_red) + ) + + icon_url = INFRACTION_ICONS.get(infr_type, Icons.token_removed) + embed.set_author(name="Infraction Information", icon_url=icon_url) + embed.set_footer(text=f"Please review our rules over at https://pythondiscord.com/about/rules") + + await self.send_private_embed(user, embed) + + async def notify_pardon( + self, user: Union[User, Member], title: str, content: str, icon_url: str = Icons.user_verified + ): + """ + Notify a user that an infraction has been lifted. + + :param user: The user to send the message to. + :param title: The title of the embed. + :param content: The content of the embed. + :param icon_url: URL for the title icon. + """ + + embed = Embed( + description=content, + colour=Colour(Colours.soft_green) + ) + + embed.set_author(name=title, icon_url=icon_url) + + await self.send_private_embed(user, embed) + + async def send_private_embed(self, user: Union[User, Member], embed: Embed): + """ + A helper method for sending an embed to a user's DMs. + + :param user: The user to send the embed to. + :param embed: The embed to send. + """ + + # sometimes `user` is a `discord.Object`, so let's make it a proper user. + user = await self.bot.get_user_info(user.id) + + try: + await user.send(embed=embed) + except (HTTPException, Forbidden): + log.debug( + f"Infraction-related information could not be sent to user {user} ({user.id}). " + "They've probably just disabled private messages." + ) + # endregion async def __error(self, ctx, error): diff --git a/bot/cogs/superstarify.py b/bot/cogs/superstarify.py index e1cfcc184..75d42c76b 100644 --- a/bot/cogs/superstarify.py +++ b/bot/cogs/superstarify.py @@ -5,6 +5,7 @@ from discord import Colour, Embed, Member from discord.errors import Forbidden from discord.ext.commands import Bot, Context, command +from bot.cogs.moderation import Moderation from bot.constants import ( Channels, Keys, NEGATIVE_REPLIES, POSITIVE_REPLIES, @@ -14,6 +15,7 @@ from bot.decorators import with_role log = logging.getLogger(__name__) +NICKNAME_POLICY_URL = "https://pythondiscord.com/about/rules#nickname-policy" class Superstarify: @@ -25,6 +27,10 @@ class Superstarify: self.bot = bot self.headers = {"X-API-KEY": Keys.site_api} + @property + def moderation(self) -> Moderation: + return self.bot.get_cog("Moderation") + async def on_member_update(self, before, after): """ This event will trigger when someone changes their name. @@ -133,7 +139,7 @@ class Superstarify: f"Your new nickname will be **{forced_nick}**.\n\n" f"You will be unable to change your nickname until \n**{end_time}**.\n\n" "If you're confused by this, please read our " - "[official nickname policy](https://pythondiscord.com/about/rules#nickname-policy)." + f"[official nickname policy]({NICKNAME_POLICY_URL})." ) embed.set_image(url=image_url) @@ -146,6 +152,13 @@ class Superstarify: f"They will not be able to change their nickname again until **{end_time}**" ) + await self.moderation.notify_infraction( + user=member, + infr_type="Superstarify", + duration=duration, + reason=f"Your nickname didn't comply with our [nickname policy]({NICKNAME_POLICY_URL})." + ) + # Change the nick and return the embed log.debug("Changing the users nickname and sending the embed.") await member.edit(nick=forced_nick) @@ -186,6 +199,13 @@ class Superstarify: f"{response}" ) + else: + await self.moderation.notify_pardon( + user=member, + title="You are no longer superstarified.", + content="You may now change your nickname on the server." + ) + log.debug(f"{member.display_name} was successfully released from superstar-prison.") await ctx.send(embed=embed) diff --git a/bot/constants.py b/bot/constants.py index 57ce87ea1..5e7927ed9 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -290,6 +290,7 @@ class Icons(metaclass=YAMLGetter): user_mute: str user_unmute: str + user_verified: str pencil: str diff --git a/config-default.yml b/config-default.yml index e0f5fb235..41383a6ae 100644 --- a/config-default.yml +++ b/config-default.yml @@ -70,6 +70,7 @@ style: user_mute: "https://cdn.discordapp.com/emojis/472472640100106250.png" user_unmute: "https://cdn.discordapp.com/emojis/472472639206719508.png" + user_verified: "https://cdn.discordapp.com/emojis/470326274519334936.png" pencil: "https://cdn.discordapp.com/emojis/470326272401211415.png" |