We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Upon receiving a non-ban infraction, the DM from the bot should tell them about ModMail
Currently, users get a message like this one when they get an infraction:

![image](https://user-images.githubusercontent.com/32915757/104851448-0c1c9980-58c3-11eb-98cc-b80f25f9dbbb.png)

For infractions other than bans (regardless of duration), the embed should tell them to use the ModMail bot if they want to discuss or ask questions about the infraction. We might also except Superstarify infractions.

I'm not sure if this should await the redesign of the infraction system or if we can implement it without sabotaging Scragly's efforts.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `bot/exts/moderation/infraction/_utils.py`
Content:
```
1 import logging
2 import typing as t
3 from datetime import datetime
4 
5 import discord
6 from discord.ext.commands import Context
7 
8 from bot.api import ResponseCodeError
9 from bot.constants import Colours, Icons
10 from bot.errors import InvalidInfractedUser
11 
12 log = logging.getLogger(__name__)
13 
14 # apply icon, pardon icon
15 INFRACTION_ICONS = {
16     "ban": (Icons.user_ban, Icons.user_unban),
17     "kick": (Icons.sign_out, None),
18     "mute": (Icons.user_mute, Icons.user_unmute),
19     "note": (Icons.user_warn, None),
20     "superstar": (Icons.superstarify, Icons.unsuperstarify),
21     "warning": (Icons.user_warn, None),
22     "voice_ban": (Icons.voice_state_red, Icons.voice_state_green),
23 }
24 RULES_URL = "https://pythondiscord.com/pages/rules"
25 APPEALABLE_INFRACTIONS = ("ban", "mute", "voice_ban")
26 
27 # Type aliases
28 UserObject = t.Union[discord.Member, discord.User]
29 UserSnowflake = t.Union[UserObject, discord.Object]
30 Infraction = t.Dict[str, t.Union[str, int, bool]]
31 
32 APPEAL_EMAIL = "appeals@pythondiscord.com"
33 
34 INFRACTION_TITLE = f"Please review our rules over at {RULES_URL}"
35 INFRACTION_APPEAL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}"
36 INFRACTION_AUTHOR_NAME = "Infraction information"
37 
38 INFRACTION_DESCRIPTION_TEMPLATE = (
39     "**Type:** {type}\n"
40     "**Expires:** {expires}\n"
41     "**Reason:** {reason}\n"
42 )
43 
44 
45 async def post_user(ctx: Context, user: UserSnowflake) -> t.Optional[dict]:
46     """
47     Create a new user in the database.
48 
49     Used when an infraction needs to be applied on a user absent in the guild.
50     """
51     log.trace(f"Attempting to add user {user.id} to the database.")
52 
53     if not isinstance(user, (discord.Member, discord.User)):
54         log.debug("The user being added to the DB is not a Member or User object.")
55 
56     payload = {
57         'discriminator': int(getattr(user, 'discriminator', 0)),
58         'id': user.id,
59         'in_guild': False,
60         'name': getattr(user, 'name', 'Name unknown'),
61         'roles': []
62     }
63 
64     try:
65         response = await ctx.bot.api_client.post('bot/users', json=payload)
66         log.info(f"User {user.id} added to the DB.")
67         return response
68     except ResponseCodeError as e:
69         log.error(f"Failed to add user {user.id} to the DB. {e}")
70         await ctx.send(f":x: The attempt to add the user to the DB failed: status {e.status}")
71 
72 
73 async def post_infraction(
74     ctx: Context,
75     user: UserSnowflake,
76     infr_type: str,
77     reason: str,
78     expires_at: datetime = None,
79     hidden: bool = False,
80     active: bool = True
81 ) -> t.Optional[dict]:
82     """Posts an infraction to the API."""
83     if isinstance(user, (discord.Member, discord.User)) and user.bot:
84         log.trace(f"Posting of {infr_type} infraction for {user} to the API aborted. User is a bot.")
85         raise InvalidInfractedUser(user)
86 
87     log.trace(f"Posting {infr_type} infraction for {user} to the API.")
88 
89     payload = {
90         "actor": ctx.author.id,  # Don't use ctx.message.author; antispam only patches ctx.author.
91         "hidden": hidden,
92         "reason": reason,
93         "type": infr_type,
94         "user": user.id,
95         "active": active
96     }
97     if expires_at:
98         payload['expires_at'] = expires_at.isoformat()
99 
100     # Try to apply the infraction. If it fails because the user doesn't exist, try to add it.
101     for should_post_user in (True, False):
102         try:
103             response = await ctx.bot.api_client.post('bot/infractions', json=payload)
104             return response
105         except ResponseCodeError as e:
106             if e.status == 400 and 'user' in e.response_json:
107                 # Only one attempt to add the user to the database, not two:
108                 if not should_post_user or await post_user(ctx, user) is None:
109                     return
110             else:
111                 log.exception(f"Unexpected error while adding an infraction for {user}:")
112                 await ctx.send(f":x: There was an error adding the infraction: status {e.status}.")
113                 return
114 
115 
116 async def get_active_infraction(
117         ctx: Context,
118         user: UserSnowflake,
119         infr_type: str,
120         send_msg: bool = True
121 ) -> t.Optional[dict]:
122     """
123     Retrieves an active infraction of the given type for the user.
124 
125     If `send_msg` is True and the user has an active infraction matching the `infr_type` parameter,
126     then a message for the moderator will be sent to the context channel letting them know.
127     Otherwise, no message will be sent.
128     """
129     log.trace(f"Checking if {user} has active infractions of type {infr_type}.")
130 
131     active_infractions = await ctx.bot.api_client.get(
132         'bot/infractions',
133         params={
134             'active': 'true',
135             'type': infr_type,
136             'user__id': str(user.id)
137         }
138     )
139     if active_infractions:
140         # Checks to see if the moderator should be told there is an active infraction
141         if send_msg:
142             log.trace(f"{user} has active infractions of type {infr_type}.")
143             await ctx.send(
144                 f":x: According to my records, this user already has a {infr_type} infraction. "
145                 f"See infraction **#{active_infractions[0]['id']}**."
146             )
147         return active_infractions[0]
148     else:
149         log.trace(f"{user} does not have active infractions of type {infr_type}.")
150 
151 
152 async def notify_infraction(
153     user: UserObject,
154     infr_type: str,
155     expires_at: t.Optional[str] = None,
156     reason: t.Optional[str] = None,
157     icon_url: str = Icons.token_removed
158 ) -> bool:
159     """DM a user about their new infraction and return True if the DM is successful."""
160     log.trace(f"Sending {user} a DM about their {infr_type} infraction.")
161 
162     text = INFRACTION_DESCRIPTION_TEMPLATE.format(
163         type=infr_type.title(),
164         expires=expires_at or "N/A",
165         reason=reason or "No reason provided."
166     )
167 
168     # For case when other fields than reason is too long and this reach limit, then force-shorten string
169     if len(text) > 2048:
170         text = f"{text[:2045]}..."
171 
172     embed = discord.Embed(
173         description=text,
174         colour=Colours.soft_red
175     )
176 
177     embed.set_author(name=INFRACTION_AUTHOR_NAME, icon_url=icon_url, url=RULES_URL)
178     embed.title = INFRACTION_TITLE
179     embed.url = RULES_URL
180 
181     if infr_type in APPEALABLE_INFRACTIONS:
182         embed.set_footer(text=INFRACTION_APPEAL_FOOTER)
183 
184     return await send_private_embed(user, embed)
185 
186 
187 async def notify_pardon(
188     user: UserObject,
189     title: str,
190     content: str,
191     icon_url: str = Icons.user_verified
192 ) -> bool:
193     """DM a user about their pardoned infraction and return True if the DM is successful."""
194     log.trace(f"Sending {user} a DM about their pardoned infraction.")
195 
196     embed = discord.Embed(
197         description=content,
198         colour=Colours.soft_green
199     )
200 
201     embed.set_author(name=title, icon_url=icon_url)
202 
203     return await send_private_embed(user, embed)
204 
205 
206 async def send_private_embed(user: UserObject, embed: discord.Embed) -> bool:
207     """
208     A helper method for sending an embed to a user's DMs.
209 
210     Returns a boolean indicator of DM success.
211     """
212     try:
213         await user.send(embed=embed)
214         return True
215     except (discord.HTTPException, discord.Forbidden, discord.NotFound):
216         log.debug(
217             f"Infraction-related information could not be sent to user {user} ({user.id}). "
218             "The user either could not be retrieved or probably disabled their DMs."
219         )
220         return False
221
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py
--- a/bot/exts/moderation/infraction/_utils.py
+++ b/bot/exts/moderation/infraction/_utils.py
@@ -22,7 +22,6 @@
     "voice_ban": (Icons.voice_state_red, Icons.voice_state_green),
 }
 RULES_URL = "https://pythondiscord.com/pages/rules"
-APPEALABLE_INFRACTIONS = ("ban", "mute", "voice_ban")
 
 # Type aliases
 UserObject = t.Union[discord.Member, discord.User]
@@ -31,8 +30,12 @@
 
 APPEAL_EMAIL = "appeals@pythondiscord.com"
 
-INFRACTION_TITLE = f"Please review our rules over at {RULES_URL}"
-INFRACTION_APPEAL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}"
+INFRACTION_TITLE = "Please review our rules"
+INFRACTION_APPEAL_EMAIL_FOOTER = f"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}"
+INFRACTION_APPEAL_MODMAIL_FOOTER = (
+    'If you would like to discuss or appeal this infraction, '
+    'send a message to the ModMail bot'
+)
 INFRACTION_AUTHOR_NAME = "Infraction information"
 
 INFRACTION_DESCRIPTION_TEMPLATE = (
@@ -71,13 +74,13 @@
 
 
 async def post_infraction(
-    ctx: Context,
-    user: UserSnowflake,
-    infr_type: str,
-    reason: str,
-    expires_at: datetime = None,
-    hidden: bool = False,
-    active: bool = True
+        ctx: Context,
+        user: UserSnowflake,
+        infr_type: str,
+        reason: str,
+        expires_at: datetime = None,
+        hidden: bool = False,
+        active: bool = True
 ) -> t.Optional[dict]:
     """Posts an infraction to the API."""
     if isinstance(user, (discord.Member, discord.User)) and user.bot:
@@ -150,11 +153,11 @@
 
 
 async def notify_infraction(
-    user: UserObject,
-    infr_type: str,
-    expires_at: t.Optional[str] = None,
-    reason: t.Optional[str] = None,
-    icon_url: str = Icons.token_removed
+        user: UserObject,
+        infr_type: str,
+        expires_at: t.Optional[str] = None,
+        reason: t.Optional[str] = None,
+        icon_url: str = Icons.token_removed
 ) -> bool:
     """DM a user about their new infraction and return True if the DM is successful."""
     log.trace(f"Sending {user} a DM about their {infr_type} infraction.")
@@ -178,17 +181,18 @@
     embed.title = INFRACTION_TITLE
     embed.url = RULES_URL
 
-    if infr_type in APPEALABLE_INFRACTIONS:
-        embed.set_footer(text=INFRACTION_APPEAL_FOOTER)
+    embed.set_footer(
+        text=INFRACTION_APPEAL_EMAIL_FOOTER if infr_type == 'Ban' else INFRACTION_APPEAL_MODMAIL_FOOTER
+    )
 
     return await send_private_embed(user, embed)
 
 
 async def notify_pardon(
-    user: UserObject,
-    title: str,
-    content: str,
-    icon_url: str = Icons.user_verified
+        user: UserObject,
+        title: str,
+        content: str,
+        icon_url: str = Icons.user_verified
 ) -> bool:
     """DM a user about their pardoned infraction and return True if the DM is successful."""
     log.trace(f"Sending {user} a DM about their pardoned infraction.")

{"golden_diff": "diff --git a/bot/exts/moderation/infraction/_utils.py b/bot/exts/moderation/infraction/_utils.py\n--- a/bot/exts/moderation/infraction/_utils.py\n+++ b/bot/exts/moderation/infraction/_utils.py\n@@ -22,7 +22,6 @@\n     \"voice_ban\": (Icons.voice_state_red, Icons.voice_state_green),\n }\n RULES_URL = \"https://pythondiscord.com/pages/rules\"\n-APPEALABLE_INFRACTIONS = (\"ban\", \"mute\", \"voice_ban\")\n \n # Type aliases\n UserObject = t.Union[discord.Member, discord.User]\n@@ -31,8 +30,12 @@\n \n APPEAL_EMAIL = \"appeals@pythondiscord.com\"\n \n-INFRACTION_TITLE = f\"Please review our rules over at {RULES_URL}\"\n-INFRACTION_APPEAL_FOOTER = f\"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}\"\n+INFRACTION_TITLE = \"Please review our rules\"\n+INFRACTION_APPEAL_EMAIL_FOOTER = f\"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}\"\n+INFRACTION_APPEAL_MODMAIL_FOOTER = (\n+    'If you would like to discuss or appeal this infraction, '\n+    'send a message to the ModMail bot'\n+)\n INFRACTION_AUTHOR_NAME = \"Infraction information\"\n \n INFRACTION_DESCRIPTION_TEMPLATE = (\n@@ -71,13 +74,13 @@\n \n \n async def post_infraction(\n-    ctx: Context,\n-    user: UserSnowflake,\n-    infr_type: str,\n-    reason: str,\n-    expires_at: datetime = None,\n-    hidden: bool = False,\n-    active: bool = True\n+        ctx: Context,\n+        user: UserSnowflake,\n+        infr_type: str,\n+        reason: str,\n+        expires_at: datetime = None,\n+        hidden: bool = False,\n+        active: bool = True\n ) -> t.Optional[dict]:\n     \"\"\"Posts an infraction to the API.\"\"\"\n     if isinstance(user, (discord.Member, discord.User)) and user.bot:\n@@ -150,11 +153,11 @@\n \n \n async def notify_infraction(\n-    user: UserObject,\n-    infr_type: str,\n-    expires_at: t.Optional[str] = None,\n-    reason: t.Optional[str] = None,\n-    icon_url: str = Icons.token_removed\n+        user: UserObject,\n+        infr_type: str,\n+        expires_at: t.Optional[str] = None,\n+        reason: t.Optional[str] = None,\n+        icon_url: str = Icons.token_removed\n ) -> bool:\n     \"\"\"DM a user about their new infraction and return True if the DM is successful.\"\"\"\n     log.trace(f\"Sending {user} a DM about their {infr_type} infraction.\")\n@@ -178,17 +181,18 @@\n     embed.title = INFRACTION_TITLE\n     embed.url = RULES_URL\n \n-    if infr_type in APPEALABLE_INFRACTIONS:\n-        embed.set_footer(text=INFRACTION_APPEAL_FOOTER)\n+    embed.set_footer(\n+        text=INFRACTION_APPEAL_EMAIL_FOOTER if infr_type == 'Ban' else INFRACTION_APPEAL_MODMAIL_FOOTER\n+    )\n \n     return await send_private_embed(user, embed)\n \n \n async def notify_pardon(\n-    user: UserObject,\n-    title: str,\n-    content: str,\n-    icon_url: str = Icons.user_verified\n+        user: UserObject,\n+        title: str,\n+        content: str,\n+        icon_url: str = Icons.user_verified\n ) -> bool:\n     \"\"\"DM a user about their pardoned infraction and return True if the DM is successful.\"\"\"\n     log.trace(f\"Sending {user} a DM about their pardoned infraction.\")\n", "issue": "Upon receiving a non-ban infraction, the DM from the bot should tell them about ModMail\nCurrently, users get a message like this one when they get an infraction:\r\n\r\n![image](https://user-images.githubusercontent.com/32915757/104851448-0c1c9980-58c3-11eb-98cc-b80f25f9dbbb.png)\r\n\r\nFor infractions other than bans (regardless of duration), the embed should tell them to use the ModMail bot if they want to discuss or ask questions about the infraction. We might also except Superstarify infractions.\r\n\r\nI'm not sure if this should await the redesign of the infraction system or if we can implement it without sabotaging Scragly's efforts.\n", "before_files": [{"content": "import logging\nimport typing as t\nfrom datetime import datetime\n\nimport discord\nfrom discord.ext.commands import Context\n\nfrom bot.api import ResponseCodeError\nfrom bot.constants import Colours, Icons\nfrom bot.errors import InvalidInfractedUser\n\nlog = logging.getLogger(__name__)\n\n# apply icon, pardon icon\nINFRACTION_ICONS = {\n    \"ban\": (Icons.user_ban, Icons.user_unban),\n    \"kick\": (Icons.sign_out, None),\n    \"mute\": (Icons.user_mute, Icons.user_unmute),\n    \"note\": (Icons.user_warn, None),\n    \"superstar\": (Icons.superstarify, Icons.unsuperstarify),\n    \"warning\": (Icons.user_warn, None),\n    \"voice_ban\": (Icons.voice_state_red, Icons.voice_state_green),\n}\nRULES_URL = \"https://pythondiscord.com/pages/rules\"\nAPPEALABLE_INFRACTIONS = (\"ban\", \"mute\", \"voice_ban\")\n\n# Type aliases\nUserObject = t.Union[discord.Member, discord.User]\nUserSnowflake = t.Union[UserObject, discord.Object]\nInfraction = t.Dict[str, t.Union[str, int, bool]]\n\nAPPEAL_EMAIL = \"appeals@pythondiscord.com\"\n\nINFRACTION_TITLE = f\"Please review our rules over at {RULES_URL}\"\nINFRACTION_APPEAL_FOOTER = f\"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}\"\nINFRACTION_AUTHOR_NAME = \"Infraction information\"\n\nINFRACTION_DESCRIPTION_TEMPLATE = (\n    \"**Type:** {type}\\n\"\n    \"**Expires:** {expires}\\n\"\n    \"**Reason:** {reason}\\n\"\n)\n\n\nasync def post_user(ctx: Context, user: UserSnowflake) -> t.Optional[dict]:\n    \"\"\"\n    Create a new user in the database.\n\n    Used when an infraction needs to be applied on a user absent in the guild.\n    \"\"\"\n    log.trace(f\"Attempting to add user {user.id} to the database.\")\n\n    if not isinstance(user, (discord.Member, discord.User)):\n        log.debug(\"The user being added to the DB is not a Member or User object.\")\n\n    payload = {\n        'discriminator': int(getattr(user, 'discriminator', 0)),\n        'id': user.id,\n        'in_guild': False,\n        'name': getattr(user, 'name', 'Name unknown'),\n        'roles': []\n    }\n\n    try:\n        response = await ctx.bot.api_client.post('bot/users', json=payload)\n        log.info(f\"User {user.id} added to the DB.\")\n        return response\n    except ResponseCodeError as e:\n        log.error(f\"Failed to add user {user.id} to the DB. {e}\")\n        await ctx.send(f\":x: The attempt to add the user to the DB failed: status {e.status}\")\n\n\nasync def post_infraction(\n    ctx: Context,\n    user: UserSnowflake,\n    infr_type: str,\n    reason: str,\n    expires_at: datetime = None,\n    hidden: bool = False,\n    active: bool = True\n) -> t.Optional[dict]:\n    \"\"\"Posts an infraction to the API.\"\"\"\n    if isinstance(user, (discord.Member, discord.User)) and user.bot:\n        log.trace(f\"Posting of {infr_type} infraction for {user} to the API aborted. User is a bot.\")\n        raise InvalidInfractedUser(user)\n\n    log.trace(f\"Posting {infr_type} infraction for {user} to the API.\")\n\n    payload = {\n        \"actor\": ctx.author.id,  # Don't use ctx.message.author; antispam only patches ctx.author.\n        \"hidden\": hidden,\n        \"reason\": reason,\n        \"type\": infr_type,\n        \"user\": user.id,\n        \"active\": active\n    }\n    if expires_at:\n        payload['expires_at'] = expires_at.isoformat()\n\n    # Try to apply the infraction. If it fails because the user doesn't exist, try to add it.\n    for should_post_user in (True, False):\n        try:\n            response = await ctx.bot.api_client.post('bot/infractions', json=payload)\n            return response\n        except ResponseCodeError as e:\n            if e.status == 400 and 'user' in e.response_json:\n                # Only one attempt to add the user to the database, not two:\n                if not should_post_user or await post_user(ctx, user) is None:\n                    return\n            else:\n                log.exception(f\"Unexpected error while adding an infraction for {user}:\")\n                await ctx.send(f\":x: There was an error adding the infraction: status {e.status}.\")\n                return\n\n\nasync def get_active_infraction(\n        ctx: Context,\n        user: UserSnowflake,\n        infr_type: str,\n        send_msg: bool = True\n) -> t.Optional[dict]:\n    \"\"\"\n    Retrieves an active infraction of the given type for the user.\n\n    If `send_msg` is True and the user has an active infraction matching the `infr_type` parameter,\n    then a message for the moderator will be sent to the context channel letting them know.\n    Otherwise, no message will be sent.\n    \"\"\"\n    log.trace(f\"Checking if {user} has active infractions of type {infr_type}.\")\n\n    active_infractions = await ctx.bot.api_client.get(\n        'bot/infractions',\n        params={\n            'active': 'true',\n            'type': infr_type,\n            'user__id': str(user.id)\n        }\n    )\n    if active_infractions:\n        # Checks to see if the moderator should be told there is an active infraction\n        if send_msg:\n            log.trace(f\"{user} has active infractions of type {infr_type}.\")\n            await ctx.send(\n                f\":x: According to my records, this user already has a {infr_type} infraction. \"\n                f\"See infraction **#{active_infractions[0]['id']}**.\"\n            )\n        return active_infractions[0]\n    else:\n        log.trace(f\"{user} does not have active infractions of type {infr_type}.\")\n\n\nasync def notify_infraction(\n    user: UserObject,\n    infr_type: str,\n    expires_at: t.Optional[str] = None,\n    reason: t.Optional[str] = None,\n    icon_url: str = Icons.token_removed\n) -> bool:\n    \"\"\"DM a user about their new infraction and return True if the DM is successful.\"\"\"\n    log.trace(f\"Sending {user} a DM about their {infr_type} infraction.\")\n\n    text = INFRACTION_DESCRIPTION_TEMPLATE.format(\n        type=infr_type.title(),\n        expires=expires_at or \"N/A\",\n        reason=reason or \"No reason provided.\"\n    )\n\n    # For case when other fields than reason is too long and this reach limit, then force-shorten string\n    if len(text) > 2048:\n        text = f\"{text[:2045]}...\"\n\n    embed = discord.Embed(\n        description=text,\n        colour=Colours.soft_red\n    )\n\n    embed.set_author(name=INFRACTION_AUTHOR_NAME, icon_url=icon_url, url=RULES_URL)\n    embed.title = INFRACTION_TITLE\n    embed.url = RULES_URL\n\n    if infr_type in APPEALABLE_INFRACTIONS:\n        embed.set_footer(text=INFRACTION_APPEAL_FOOTER)\n\n    return await send_private_embed(user, embed)\n\n\nasync def notify_pardon(\n    user: UserObject,\n    title: str,\n    content: str,\n    icon_url: str = Icons.user_verified\n) -> bool:\n    \"\"\"DM a user about their pardoned infraction and return True if the DM is successful.\"\"\"\n    log.trace(f\"Sending {user} a DM about their pardoned infraction.\")\n\n    embed = discord.Embed(\n        description=content,\n        colour=Colours.soft_green\n    )\n\n    embed.set_author(name=title, icon_url=icon_url)\n\n    return await send_private_embed(user, embed)\n\n\nasync def send_private_embed(user: UserObject, embed: discord.Embed) -> bool:\n    \"\"\"\n    A helper method for sending an embed to a user's DMs.\n\n    Returns a boolean indicator of DM success.\n    \"\"\"\n    try:\n        await user.send(embed=embed)\n        return True\n    except (discord.HTTPException, discord.Forbidden, discord.NotFound):\n        log.debug(\n            f\"Infraction-related information could not be sent to user {user} ({user.id}). \"\n            \"The user either could not be retrieved or probably disabled their DMs.\"\n        )\n        return False\n", "path": "bot/exts/moderation/infraction/_utils.py"}], "after_files": [{"content": "import logging\nimport typing as t\nfrom datetime import datetime\n\nimport discord\nfrom discord.ext.commands import Context\n\nfrom bot.api import ResponseCodeError\nfrom bot.constants import Colours, Icons\nfrom bot.errors import InvalidInfractedUser\n\nlog = logging.getLogger(__name__)\n\n# apply icon, pardon icon\nINFRACTION_ICONS = {\n    \"ban\": (Icons.user_ban, Icons.user_unban),\n    \"kick\": (Icons.sign_out, None),\n    \"mute\": (Icons.user_mute, Icons.user_unmute),\n    \"note\": (Icons.user_warn, None),\n    \"superstar\": (Icons.superstarify, Icons.unsuperstarify),\n    \"warning\": (Icons.user_warn, None),\n    \"voice_ban\": (Icons.voice_state_red, Icons.voice_state_green),\n}\nRULES_URL = \"https://pythondiscord.com/pages/rules\"\n\n# Type aliases\nUserObject = t.Union[discord.Member, discord.User]\nUserSnowflake = t.Union[UserObject, discord.Object]\nInfraction = t.Dict[str, t.Union[str, int, bool]]\n\nAPPEAL_EMAIL = \"appeals@pythondiscord.com\"\n\nINFRACTION_TITLE = \"Please review our rules\"\nINFRACTION_APPEAL_EMAIL_FOOTER = f\"To appeal this infraction, send an e-mail to {APPEAL_EMAIL}\"\nINFRACTION_APPEAL_MODMAIL_FOOTER = (\n    'If you would like to discuss or appeal this infraction, '\n    'send a message to the ModMail bot'\n)\nINFRACTION_AUTHOR_NAME = \"Infraction information\"\n\nINFRACTION_DESCRIPTION_TEMPLATE = (\n    \"**Type:** {type}\\n\"\n    \"**Expires:** {expires}\\n\"\n    \"**Reason:** {reason}\\n\"\n)\n\n\nasync def post_user(ctx: Context, user: UserSnowflake) -> t.Optional[dict]:\n    \"\"\"\n    Create a new user in the database.\n\n    Used when an infraction needs to be applied on a user absent in the guild.\n    \"\"\"\n    log.trace(f\"Attempting to add user {user.id} to the database.\")\n\n    if not isinstance(user, (discord.Member, discord.User)):\n        log.debug(\"The user being added to the DB is not a Member or User object.\")\n\n    payload = {\n        'discriminator': int(getattr(user, 'discriminator', 0)),\n        'id': user.id,\n        'in_guild': False,\n        'name': getattr(user, 'name', 'Name unknown'),\n        'roles': []\n    }\n\n    try:\n        response = await ctx.bot.api_client.post('bot/users', json=payload)\n        log.info(f\"User {user.id} added to the DB.\")\n        return response\n    except ResponseCodeError as e:\n        log.error(f\"Failed to add user {user.id} to the DB. {e}\")\n        await ctx.send(f\":x: The attempt to add the user to the DB failed: status {e.status}\")\n\n\nasync def post_infraction(\n        ctx: Context,\n        user: UserSnowflake,\n        infr_type: str,\n        reason: str,\n        expires_at: datetime = None,\n        hidden: bool = False,\n        active: bool = True\n) -> t.Optional[dict]:\n    \"\"\"Posts an infraction to the API.\"\"\"\n    if isinstance(user, (discord.Member, discord.User)) and user.bot:\n        log.trace(f\"Posting of {infr_type} infraction for {user} to the API aborted. User is a bot.\")\n        raise InvalidInfractedUser(user)\n\n    log.trace(f\"Posting {infr_type} infraction for {user} to the API.\")\n\n    payload = {\n        \"actor\": ctx.author.id,  # Don't use ctx.message.author; antispam only patches ctx.author.\n        \"hidden\": hidden,\n        \"reason\": reason,\n        \"type\": infr_type,\n        \"user\": user.id,\n        \"active\": active\n    }\n    if expires_at:\n        payload['expires_at'] = expires_at.isoformat()\n\n    # Try to apply the infraction. If it fails because the user doesn't exist, try to add it.\n    for should_post_user in (True, False):\n        try:\n            response = await ctx.bot.api_client.post('bot/infractions', json=payload)\n            return response\n        except ResponseCodeError as e:\n            if e.status == 400 and 'user' in e.response_json:\n                # Only one attempt to add the user to the database, not two:\n                if not should_post_user or await post_user(ctx, user) is None:\n                    return\n            else:\n                log.exception(f\"Unexpected error while adding an infraction for {user}:\")\n                await ctx.send(f\":x: There was an error adding the infraction: status {e.status}.\")\n                return\n\n\nasync def get_active_infraction(\n        ctx: Context,\n        user: UserSnowflake,\n        infr_type: str,\n        send_msg: bool = True\n) -> t.Optional[dict]:\n    \"\"\"\n    Retrieves an active infraction of the given type for the user.\n\n    If `send_msg` is True and the user has an active infraction matching the `infr_type` parameter,\n    then a message for the moderator will be sent to the context channel letting them know.\n    Otherwise, no message will be sent.\n    \"\"\"\n    log.trace(f\"Checking if {user} has active infractions of type {infr_type}.\")\n\n    active_infractions = await ctx.bot.api_client.get(\n        'bot/infractions',\n        params={\n            'active': 'true',\n            'type': infr_type,\n            'user__id': str(user.id)\n        }\n    )\n    if active_infractions:\n        # Checks to see if the moderator should be told there is an active infraction\n        if send_msg:\n            log.trace(f\"{user} has active infractions of type {infr_type}.\")\n            await ctx.send(\n                f\":x: According to my records, this user already has a {infr_type} infraction. \"\n                f\"See infraction **#{active_infractions[0]['id']}**.\"\n            )\n        return active_infractions[0]\n    else:\n        log.trace(f\"{user} does not have active infractions of type {infr_type}.\")\n\n\nasync def notify_infraction(\n        user: UserObject,\n        infr_type: str,\n        expires_at: t.Optional[str] = None,\n        reason: t.Optional[str] = None,\n        icon_url: str = Icons.token_removed\n) -> bool:\n    \"\"\"DM a user about their new infraction and return True if the DM is successful.\"\"\"\n    log.trace(f\"Sending {user} a DM about their {infr_type} infraction.\")\n\n    text = INFRACTION_DESCRIPTION_TEMPLATE.format(\n        type=infr_type.title(),\n        expires=expires_at or \"N/A\",\n        reason=reason or \"No reason provided.\"\n    )\n\n    # For case when other fields than reason is too long and this reach limit, then force-shorten string\n    if len(text) > 2048:\n        text = f\"{text[:2045]}...\"\n\n    embed = discord.Embed(\n        description=text,\n        colour=Colours.soft_red\n    )\n\n    embed.set_author(name=INFRACTION_AUTHOR_NAME, icon_url=icon_url, url=RULES_URL)\n    embed.title = INFRACTION_TITLE\n    embed.url = RULES_URL\n\n    embed.set_footer(\n        text=INFRACTION_APPEAL_EMAIL_FOOTER if infr_type == 'Ban' else INFRACTION_APPEAL_MODMAIL_FOOTER\n    )\n\n    return await send_private_embed(user, embed)\n\n\nasync def notify_pardon(\n        user: UserObject,\n        title: str,\n        content: str,\n        icon_url: str = Icons.user_verified\n) -> bool:\n    \"\"\"DM a user about their pardoned infraction and return True if the DM is successful.\"\"\"\n    log.trace(f\"Sending {user} a DM about their pardoned infraction.\")\n\n    embed = discord.Embed(\n        description=content,\n        colour=Colours.soft_green\n    )\n\n    embed.set_author(name=title, icon_url=icon_url)\n\n    return await send_private_embed(user, embed)\n\n\nasync def send_private_embed(user: UserObject, embed: discord.Embed) -> bool:\n    \"\"\"\n    A helper method for sending an embed to a user's DMs.\n\n    Returns a boolean indicator of DM success.\n    \"\"\"\n    try:\n        await user.send(embed=embed)\n        return True\n    except (discord.HTTPException, discord.Forbidden, discord.NotFound):\n        log.debug(\n            f\"Infraction-related information could not be sent to user {user} ({user.id}). \"\n            \"The user either could not be retrieved or probably disabled their DMs.\"\n        )\n        return False\n", "path": "bot/exts/moderation/infraction/_utils.py"}]}