We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Write unit tests for `bot/cogs/moderation/utils.py`
Write unit tests for [`bot/cogs/moderation/utils.py`](../blob/master/bot/cogs/moderation/utils.py).

## Implementation details
Please make sure to read the general information in the [meta issue](553) and the [testing README](../blob/master/tests/README.md). We are aiming for a 100% [branch coverage](https://coverage.readthedocs.io/en/stable/branch.html) for this file, but if you think that is not possible, please discuss that in this issue.

## Additional information
If you want to work on this issue, **please make sure that you get assigned to it** by one of the core devs before starting to work on it. We would like to prevent the situation that multiple people are working on the same issue. To get assigned, leave a comment showing your interesting in tackling this issue.


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

{"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@@ -1,5 +1,4 @@\n import logging\n-import textwrap\n import typing as t\n from datetime import datetime\n \n@@ -28,6 +27,18 @@\n UserSnowflake = t.Union[UserObject, discord.Object]\n Infraction = t.Dict[str, t.Union[str, int, bool]]\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_AUTHOR_NAME = \"Infraction information\"\n+\n+INFRACTION_DESCRIPTION_TEMPLATE = (\n+    \"**Type:** {type}\\n\"\n+    \"**Expires:** {expires}\\n\"\n+    \"**Reason:** {reason}\\n\"\n+)\n+\n \n async def post_user(ctx: Context, user: UserSnowflake) -> t.Optional[dict]:\n     \"\"\"\n@@ -142,25 +153,27 @@\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 = textwrap.dedent(f\"\"\"\n-        **Type:** {infr_type.capitalize()}\n-        **Expires:** {expires_at or \"N/A\"}\n-        **Reason:** {reason or \"No reason provided.\"}\n-    \"\"\")\n+    text = INFRACTION_DESCRIPTION_TEMPLATE.format(\n+        type=infr_type.capitalize(),\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=textwrap.shorten(text, width=2048, placeholder=\"...\"),\n+        description=text,\n         colour=Colours.soft_red\n     )\n \n-    embed.set_author(name=\"Infraction information\", icon_url=icon_url, url=RULES_URL)\n-    embed.title = f\"Please review our rules over at {RULES_URL}\"\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(\n-            text=\"To appeal this infraction, send an e-mail to appeals@pythondiscord.com\"\n-        )\n+        embed.set_footer(text=INFRACTION_APPEAL_FOOTER)\n \n     return await send_private_embed(user, embed)\n", "issue": "Write unit tests for `bot/cogs/moderation/utils.py`\nWrite unit tests for [`bot/cogs/moderation/utils.py`](../blob/master/bot/cogs/moderation/utils.py).\n\n## Implementation details\nPlease make sure to read the general information in the [meta issue](553) and the [testing README](../blob/master/tests/README.md). We are aiming for a 100% [branch coverage](https://coverage.readthedocs.io/en/stable/branch.html) for this file, but if you think that is not possible, please discuss that in this issue.\n\n## Additional information\nIf you want to work on this issue, **please make sure that you get assigned to it** by one of the core devs before starting to work on it. We would like to prevent the situation that multiple people are working on the same issue. To get assigned, leave a comment showing your interesting in tackling this issue.\n\n", "before_files": [{"content": "import logging\nimport textwrap\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\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}\nRULES_URL = \"https://pythondiscord.com/pages/rules\"\nAPPEALABLE_INFRACTIONS = (\"ban\", \"mute\")\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\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    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 = textwrap.dedent(f\"\"\"\n        **Type:** {infr_type.capitalize()}\n        **Expires:** {expires_at or \"N/A\"}\n        **Reason:** {reason or \"No reason provided.\"}\n    \"\"\")\n\n    embed = discord.Embed(\n        description=textwrap.shorten(text, width=2048, placeholder=\"...\"),\n        colour=Colours.soft_red\n    )\n\n    embed.set_author(name=\"Infraction information\", icon_url=icon_url, url=RULES_URL)\n    embed.title = f\"Please review our rules over at {RULES_URL}\"\n    embed.url = RULES_URL\n\n    if infr_type in APPEALABLE_INFRACTIONS:\n        embed.set_footer(\n            text=\"To appeal this infraction, send an e-mail to appeals@pythondiscord.com\"\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"}], "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\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}\nRULES_URL = \"https://pythondiscord.com/pages/rules\"\nAPPEALABLE_INFRACTIONS = (\"ban\", \"mute\")\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    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.capitalize(),\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"}]}