aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Gareth Coles <[email protected]>2018-11-17 00:18:34 +0000
committerGravatar Gareth Coles <[email protected]>2018-11-17 00:18:34 +0000
commit7e799a9d50bed230a3b04600692c4a5d4cf2b398 (patch)
treedfbead858984ea4568bd9a2473f7c11c86d8c31b
parentReitz sez: Try --sequential (diff)
parentSend 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.py191
-rw-r--r--bot/cogs/superstarify.py22
-rw-r--r--bot/constants.py1
-rw-r--r--config-default.yml1
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"