Repository: python-discord/bot

Issue:
Add created date and duration when outputting expired infraction.
# Abstract
In addition to displaying `Member`, `Actor` and `Reason` of an expired infraction, display created date as well as duration since applied.
Also as a bonus, show the member via <@id>

# Specification
Right now when an infraction expired, the bot will output this:
![image](https://user-images.githubusercontent.com/19921765/70550854-ffe6e880-1ba8-11ea-9627-32f833c55e3f.png)

Proposal:
```
Member: <@248100771634085889> `248100771634085889`
Actor: Python#4329
Created: 2019-03-02 14:20 ( 14 months and 15 days )
Reason: Being disruptive and nonconstructive in other's help sessions, crossposting questions across help channels, disregarding TOS after people explicitly point it out, "I dont wanna spent hours reading While u can just help me" in response to being shown relevant resources. It seems you're not wanting to put the effort in when receiving help from others, and fail to learn anything from previous warnings and mutes. Don't bother returning if you can't adjust the total disregard you have.
Watching: Yes
```

# Rationale
I believe it will be helpful to know how long an user has been tempbanned / tempmuted, otherwise mods will have to either search their memory or do another extra step of `infr search`

# Motivation
I don't wanna do an extra `infr search`, I'm lazy!


Discussion:

Resolving patch:
diff --git a/bot/cogs/moderation/scheduler.py b/bot/cogs/moderation/scheduler.py
--- a/bot/cogs/moderation/scheduler.py
+++ b/bot/cogs/moderation/scheduler.py
@@ -309,16 +309,23 @@ async def deactivate_infraction(
         guild = self.bot.get_guild(constants.Guild.id)
         mod_role = guild.get_role(constants.Roles.moderator)
         user_id = infraction["user"]
+        actor = infraction["actor"]
         type_ = infraction["type"]
         id_ = infraction["id"]
+        inserted_at = infraction["inserted_at"]
+        expiry = infraction["expires_at"]
 
         log.info(f"Marking infraction #{id_} as inactive (expired).")
 
+        expiry = dateutil.parser.isoparse(expiry).replace(tzinfo=None) if expiry else None
+        created = time.format_infraction_with_duration(inserted_at, expiry)
+
         log_content = None
         log_text = {
-            "Member": str(user_id),
-            "Actor": str(self.bot.user),
-            "Reason": infraction["reason"]
+            "Member": f"<@{user_id}>",
+            "Actor": str(self.bot.get_user(actor) or actor),
+            "Reason": infraction["reason"],
+            "Created": created,
         }
 
         try:
@@ -384,14 +391,19 @@ async def deactivate_infraction(
         if send_log:
             log_title = f"expiration failed" if "Failure" in log_text else "expired"
 
+            user = self.bot.get_user(user_id)
+            avatar = user.avatar_url_as(static_format="png") if user else None
+
             log.trace(f"Sending deactivation mod log for infraction #{id_}.")
             await self.mod_log.send_log_message(
                 icon_url=utils.INFRACTION_ICONS[type_][1],
                 colour=Colours.soft_green,
                 title=f"Infraction {log_title}: {type_}",
+                thumbnail=avatar,
                 text="\n".join(f"{k}: {v}" for k, v in log_text.items()),
                 footer=f"ID: {id_}",
                 content=log_content,
+
             )
 
         return log_text
diff --git a/bot/utils/time.py b/bot/utils/time.py
--- a/bot/utils/time.py
+++ b/bot/utils/time.py
@@ -114,30 +114,40 @@ def format_infraction(timestamp: str) -> str:
 
 
 def format_infraction_with_duration(
-    expiry: Optional[str],
+    date_to: Optional[str],
     date_from: Optional[datetime.datetime] = None,
-    max_units: int = 2
+    max_units: int = 2,
+    absolute: bool = True
 ) -> Optional[str]:
     """
-    Format an infraction timestamp to a more readable ISO 8601 format WITH the duration.
+    Return `date_to` formatted as a readable ISO-8601 with the humanized duration since `date_from`.
 
-    Returns a human-readable version of the duration between datetime.utcnow() and an expiry.
-    Unlike `humanize_delta`, this function will force the `precision` to be `seconds` by not passing it.
-    `max_units` specifies the maximum number of units of time to include (e.g. 1 may include days but not hours).
-    By default, max_units is 2.
+    `date_from` must be an ISO-8601 formatted timestamp. The duration is calculated as from
+    `date_from` until `date_to` with a precision of seconds. If `date_from` is unspecified, the
+    current time is used.
+
+    `max_units` specifies the maximum number of units of time to include in the duration. For
+    example, a value of 1 may include days but not hours.
+
+    If `absolute` is True, the absolute value of the duration delta is used. This prevents negative
+    values in the case that `date_to` is in the past relative to `date_from`.
     """
-    if not expiry:
+    if not date_to:
         return None
 
+    date_to_formatted = format_infraction(date_to)
+
     date_from = date_from or datetime.datetime.utcnow()
-    date_to = dateutil.parser.isoparse(expiry).replace(tzinfo=None, microsecond=0)
+    date_to = dateutil.parser.isoparse(date_to).replace(tzinfo=None, microsecond=0)
 
-    expiry_formatted = format_infraction(expiry)
+    delta = relativedelta(date_to, date_from)
+    if absolute:
+        delta = abs(delta)
 
-    duration = humanize_delta(relativedelta(date_to, date_from), max_units=max_units)
-    duration_formatted = f" ({duration})" if duration else ''
+    duration = humanize_delta(delta, max_units=max_units)
+    duration_formatted = f" ({duration})" if duration else ""
 
-    return f"{expiry_formatted}{duration_formatted}"
+    return f"{date_to_formatted}{duration_formatted}"
 
 
 def until_expiration(


Test patch: