Repository: python-discord/bot
Issue: Infraction listing for permanent infractions lists "expired" time remaining

A permanent infraction will have its "Remaining" attribute displayed as "Expired" due to the following line of code:

https://github.com/python-discord/bot/blob/5df2fa2f948852826322409103c91565390110fe/bot/exts/moderation/infraction/management.py#L264

Remaining is "None" not because it has expired, but because there is no defined time remaining for an infraction that is not scheduled to expire. Previously, there was a suggestion in issue #668:

> For fun, we could put (∞) next to permanent infractions.

I think this could be a good choice here. It's also unlikely to introduce much complexity, since all that's needed is an additional case to the "time remaining" if statement to handle unexpired permanent infractions.


Changed files:
bot/exts/moderation/infraction/management.py, bot/exts/moderation/infraction/_scheduler.py, bot/converters.py, bot/exts/moderation/infraction/superstarify.py, bot/exts/utils/utils.py, bot/utils/time.py, bot/exts/recruitment/talentpool/_cog.py, bot/exts/moderation/watchchannels/_watchchannel.py, tests/bot/exts/info/test_information.py, bot/exts/recruitment/talentpool/_review.py, bot/exts/info/information.py, bot/exts/utils/reminders.py, bot/utils/scheduling.py, bot/exts/moderation/modlog.py, bot/exts/moderation/stream.py, tests/bot/utils/test_time.py, bot/exts/moderation/slowmode.py, bot/exts/moderation/modpings.py, bot/exts/moderation/defcon.py

diff --git a/bot/converters.py b/bot/converters.py
index cd33f5ed..3522a32a 100644
--- a/bot/converters.py
+++ b/bot/converters.py
@@ -6,7 +6,6 @@ from datetime import datetime, timezone
 from ssl import CertificateError
 
 import dateutil.parser
-import dateutil.tz
 import discord
 from aiohttp import ClientConnectorError
 from botcore.regex import DISCORD_INVITE
@@ -21,8 +20,8 @@ from bot.errors import InvalidInfraction
 from bot.exts.info.doc import _inventory_parser
 from bot.exts.info.tags import TagIdentifier
 from bot.log import get_logger
+from bot.utils import time
 from bot.utils.extensions import EXTENSIONS, unqualify
-from bot.utils.time import parse_duration_string
 
 if t.TYPE_CHECKING:
     from bot.exts.info.source import SourceType
@@ -338,7 +337,7 @@ class DurationDelta(Converter):
 
         The units need to be provided in descending order of magnitude.
         """
-        if not (delta := parse_duration_string(duration)):
+        if not (delta := time.parse_duration_string(duration)):
             raise BadArgument(f"`{duration}` is not a valid duration string.")
 
         return delta
@@ -454,9 +453,9 @@ class ISODateTime(Converter):
             raise BadArgument(f"`{datetime_string}` is not a valid ISO-8601 datetime string")
 
         if dt.tzinfo:
-            dt = dt.astimezone(dateutil.tz.UTC)
+            dt = dt.astimezone(timezone.utc)
         else:  # Without a timezone, assume it represents UTC.
-            dt = dt.replace(tzinfo=dateutil.tz.UTC)
+            dt = dt.replace(tzinfo=timezone.utc)
 
         return dt
 
diff --git a/bot/exts/info/information.py b/bot/exts/info/information.py
index 1f95c460..29a00ec5 100644
--- a/bot/exts/info/information.py
+++ b/bot/exts/info/information.py
@@ -18,10 +18,10 @@ from bot.decorators import in_whitelist
 from bot.errors import NonExistentRoleError
 from bot.log import get_logger
 from bot.pagination import LinePaginator
+from bot.utils import time
 from bot.utils.channel import is_mod_channel, is_staff_channel
 from bot.utils.checks import cooldown_with_role_bypass, has_no_roles_check, in_whitelist_check
 from bot.utils.members import get_or_fetch_member
-from bot.utils.time import TimestampFormats, discord_timestamp, humanize_delta
 
 log = get_logger(__name__)
 
@@ -83,7 +83,7 @@ class Information(Cog):
 
         defcon_info = ""
         if cog := self.bot.get_cog("Defcon"):
-            threshold = humanize_delta(cog.threshold) if cog.threshold else "-"
+            threshold = time.humanize_delta(cog.threshold) if cog.threshold else "-"
             defcon_info = f"Defcon threshold: {threshold}\\n"
 
         verification = f"Verification level: {ctx.guild.verification_level.name}\\n"
@@ -173,7 +173,7 @@ class Information(Cog):
         """Returns an embed full of server information."""
         embed = Embed(colour=Colour.og_blurple(), title="Server Information")
 
-        created = discord_timestamp(ctx.guild.created_at, TimestampFormats.RELATIVE)
+        created = time.format_relative(ctx.guild.created_at)
         num_roles = len(ctx.guild.roles) - 1  # Exclude @everyone
 
         # Server Features are only useful in certain channels
@@ -249,7 +249,7 @@ class Information(Cog):
         """Creates an embed containing information on the `user`."""
         on_server = bool(await get_or_fetch_member(ctx.guild, user.id))
 
-        created = discord_timestamp(user.created_at, TimestampFormats.RELATIVE)
+        created = time.format_relative(user.created_at)
 
         name = str(user)
         if on_server and user.nick:
@@ -272,7 +272,7 @@ class Information(Cog):
 
         if on_server:
             if user.joined_at:
-                joined = discord_timestamp(user.joined_at, TimestampFormats.RELATIVE)
+                joined = time.format_relative(user.joined_at)
             else:
                 joined = "Unable to get join date"
 
diff --git a/bot/exts/moderation/defcon.py b/bot/exts/moderation/defcon.py
index 14db3736..178be734 100644
--- a/bot/exts/moderation/defcon.py
+++ b/bot/exts/moderation/defcon.py
@@ -17,12 +17,9 @@ from bot.constants import Channels, Colours, Emojis, Event, Icons, MODERATION_RO
 from bot.converters import DurationDelta, Expiry
 from bot.exts.moderation.modlog import ModLog
 from bot.log import get_logger
-from bot.utils import scheduling
+from bot.utils import scheduling, time
 from bot.utils.messages import format_user
 from bot.utils.scheduling import Scheduler
-from bot.utils.time import (
-    TimestampFormats, discord_timestamp, humanize_delta, parse_duration_string, relativedelta_to_timedelta
-)
 
 log = get_logger(__name__)
 
@@ -88,7 +85,7 @@ class Defcon(Cog):
 
         try:
             settings = await self.defcon_settings.to_dict()
-            self.threshold = parse_duration_string(settings["threshold"]) if settings.get("threshold") else None
+            self.threshold = time.parse_duration_string(settings["threshold"]) if settings.get("threshold") else None
             self.expiry = datetime.fromisoformat(settings["expiry"]) if settings.get("expiry") else None
         except RedisError:
             log.exception("Unable to get DEFCON settings!")
@@ -102,7 +99,7 @@ class Defcon(Cog):
                 self.scheduler.schedule_at(self.expiry, 0, self._remove_threshold())
 
             self._update_notifier()
-            log.info(f"DEFCON synchronized: {humanize_delta(self.threshold) if self.threshold else '-'}")
+            log.info(f"DEFCON synchronized: {time.humanize_delta(self.threshold) if self.threshold else '-'}")
 
         self._update_channel_topic()
 
@@ -112,7 +109,7 @@ class Defcon(Cog):
         if self.threshold:
             now = arrow.utcnow()
 
-            if now - member.created_at < relativedelta_to_timedelta(self.threshold):
+            if now - member.created_at < time.relativedelta_to_timedelta(self.threshold):
                 log.info(f"Rejecting user {member}: Account is too new")
 
                 message_sent = False
@@ -151,11 +148,12 @@ class Defcon(Cog):
     @has_any_role(*MODERATION_ROLES)
     async def status(self, ctx: Context) -> None:
         """Check the current status of DEFCON mode."""
+        expiry = time.format_relative(self.expiry) if self.expiry else "-"
         embed = Embed(
             colour=Colour.og_blurple(), title="DEFCON Status",
             description=f"""
-                **Threshold:** {humanize_delta(self.threshold) if self.threshold else "-"}
-                **Expires:** {discord_timestamp(self.expiry, TimestampFormats.RELATIVE) if self.expiry else "-"}
+                **Threshold:** {time.humanize_delta(self.threshold) if self.threshold else "-"}
+                **Expires:** {expiry}
                 **Verification level:** {ctx.guild.verification_level.name}
                 """
         )
@@ -213,7 +211,8 @@ class Defcon(Cog):
 
     def _update_channel_topic(self) -> None:
         """Update the #defcon channel topic with the current DEFCON status."""
-        new_topic = f"{BASE_CHANNEL_TOPIC}\\n(Threshold: {humanize_delta(self.threshold) if self.threshold else '-'})"
+        threshold = time.humanize_delta(self.threshold) if self.threshold else '-'
+        new_topic = f"{BASE_CHANNEL_TOPIC}\\n(Threshold: {threshold})"
 
         self.mod_log.ignore(Event.guild_channel_update, Channels.defcon)
         scheduling.create_task(self.channel.edit(topic=new_topic))
@@ -255,12 +254,12 @@ class Defcon(Cog):
 
         expiry_message = ""
         if expiry:
-            activity_duration = relativedelta(expiry, arrow.utcnow().datetime)
-            expiry_message = f" for the next {humanize_delta(activity_duration, max_units=2)}"
+            formatted_expiry = time.humanize_delta(expiry, max_units=2)
+            expiry_message = f" for the next {formatted_expiry}"
 
         if self.threshold:
             channel_message = (
-                f"updated; accounts must be {humanize_delta(self.threshold)} "
+                f"updated; accounts must be {time.humanize_delta(self.threshold)} "
                 f"old to join the server{expiry_message}"
             )
         else:
@@ -290,7 +289,7 @@ class Defcon(Cog):
 
     def _log_threshold_stat(self, threshold: relativedelta) -> None:
         """Adds the threshold to the bot stats in days."""
-        threshold_days = relativedelta_to_timedelta(threshold).total_seconds() / SECONDS_IN_DAY
+        threshold_days = time.relativedelta_to_timedelta(threshold).total_seconds() / SECONDS_IN_DAY
         self.bot.stats.gauge("defcon.threshold", threshold_days)
 
     async def _send_defcon_log(self, action: Action, actor: User) -> None:
@@ -298,7 +297,7 @@ class Defcon(Cog):
         info = action.value
         log_msg: str = (
             f"**Staffer:** {actor.mention} {actor} (`{actor.id}`)\\n"
-            f"{info.template.format(threshold=(humanize_delta(self.threshold) if self.threshold else '-'))}"
+            f"{info.template.format(threshold=(time.humanize_delta(self.threshold) if self.threshold else '-'))}"
         )
         status_msg = f"DEFCON {action.name.lower()}"
 
@@ -317,7 +316,7 @@ class Defcon(Cog):
     @tasks.loop(hours=1)
     async def defcon_notifier(self) -> None:
         """Routinely notify moderators that DEFCON is active."""
-        await self.channel.send(f"Defcon is on and is set to {humanize_delta(self.threshold)}.")
+        await self.channel.send(f"Defcon is on and is set to {time.humanize_delta(self.threshold)}.")
 
     def cog_unload(self) -> None:
         """Cancel the notifer and threshold removal tasks when the cog unloads."""
diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py
index 57aa2d9b..47b63942 100644
--- a/bot/exts/moderation/infraction/_scheduler.py
+++ b/bot/exts/moderation/infraction/_scheduler.py
@@ -136,7 +136,7 @@ class InfractionScheduler:
         infr_type = infraction["type"]
         icon = _utils.INFRACTION_ICONS[infr_type][0]
         reason = infraction["reason"]
-        expiry = time.format_infraction_with_duration(infraction["expires_at"])
+        expiry = time.format_with_duration(infraction["expires_at"])
         id_ = infraction['id']
 
         if user_reason is None:
@@ -381,20 +381,15 @@ class InfractionScheduler:
         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) if expiry else None
-        created = time.format_infraction_with_duration(inserted_at, expiry)
-
         log_content = None
         log_text = {
             "Member": f"<@{user_id}>",
             "Actor": f"<@{actor}>",
             "Reason": infraction["reason"],
-            "Created": created,
+            "Created": time.format_with_duration(infraction["inserted_at"], infraction["expires_at"]),
         }
 
         try:
diff --git a/bot/exts/moderation/infraction/management.py b/bot/exts/moderation/infraction/management.py
index 9649ff85..dda3fada 100644
--- a/bot/exts/moderation/infraction/management.py
+++ b/bot/exts/moderation/infraction/management.py
@@ -1,10 +1,7 @@
 import textwrap
 import typing as t
-from datetime import datetime, timezone
 
-import dateutil.parser
 import discord
-from dateutil.relativedelta import relativedelta
 from discord.ext import commands
 from discord.ext.commands import Context
 from discord.utils import escape_markdown
@@ -20,7 +17,6 @@ from bot.pagination import LinePaginator
 from bot.utils import messages, time
 from bot.utils.channel import is_mod_channel
 from bot.utils.members import get_or_fetch_member
-from bot.utils.time import humanize_delta, until_expiration
 
 log = get_logger(__name__)
 
@@ -151,7 +147,7 @@ class ModManagement(commands.Cog):
             confirm_messages.append("marked as permanent")
         elif duration is not None:
             request_data['expires_at'] = duration.isoformat()
-            expiry = time.format_infraction_with_duration(request_data['expires_at'])
+            expiry = time.format_with_duration(duration)
             confirm_messages.append(f"set to expire on {expiry}")
         else:
             confirm_messages.append("expiry unchanged")
@@ -183,8 +179,8 @@ class ModManagement(commands.Cog):
                 self.infractions_cog.schedule_expiration(new_infraction)
 
             log_text += f"""
-                Previous expiry: {until_expiration(infraction['expires_at']) or "Permanent"}
-                New expiry: {until_expiration(new_infraction['expires_at']) or "Permanent"}
+                Previous expiry: {time.until_expiration(infraction['expires_at'])}
+                New expiry: {time.until_expiration(new_infraction['expires_at'])}
             """.rstrip()
 
         changes = ' & '.join(confirm_messages)
@@ -352,7 +348,8 @@ class ModManagement(commands.Cog):
         active = infraction["active"]
         user = infraction["user"]
         expires_at = infraction["expires_at"]
-        created = time.format_infraction(infraction["inserted_at"])
+        inserted_at = infraction["inserted_at"]
+        created = time.discord_timestamp(inserted_at)
         dm_sent = infraction["dm_sent"]
 
         # Format the user string.
@@ -365,19 +362,14 @@ class ModManagement(commands.Cog):
             user_str = f"<@{user['id']}> ({name}#{user['discriminator']:04})"
 
         if active:
-            remaining = time.until_expiration(expires_at) or "Expired"
+            remaining = time.until_expiration(expires_at)
         else:
             remaining = "Inactive"
 
         if expires_at is None:
             duration = "*Permanent*"
         else:
-            date_from = datetime.fromtimestamp(
-                float(time.DISCORD_TIMESTAMP_REGEX.match(created).group(1)),
-                timezone.utc
-            )
-            date_to = dateutil.parser.isoparse(expires_at)
-            duration = humanize_delta(relativedelta(date_to, date_from))
+            duration = time.humanize_delta(inserted_at, expires_at)
 
         # Format `dm_sent`
         if dm_sent is None:
diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py
index 08c92b8f..a037ca1b 100644
--- a/bot/exts/moderation/infraction/superstarify.py
+++ b/bot/exts/moderation/infraction/superstarify.py
@@ -14,9 +14,9 @@ from bot.converters import Duration, Expiry
 from bot.exts.moderation.infraction import _utils
 from bot.exts.moderation.infraction._scheduler import InfractionScheduler
 from bot.log import get_logger
+from bot.utils import time
 from bot.utils.members import get_or_fetch_member
 from bot.utils.messages import format_user
-from bot.utils.time import format_infraction
 
 log = get_logger(__name__)
 NICKNAME_POLICY_URL = "https://pythondiscord.com/pages/rules/#nickname-policy"
@@ -73,7 +73,7 @@ class Superstarify(InfractionScheduler, Cog):
         notified = await _utils.notify_infraction(
             user=after,
             infr_type="Superstarify",
-            expires_at=format_infraction(infraction["expires_at"]),
+            expires_at=time.discord_timestamp(infraction["expires_at"]),
             reason=(
                 "You have tried to change your nickname on the **Python Discord** server "
                 f"from **{before.display_name}** to **{after.display_name}**, but as you "
@@ -150,7 +150,7 @@ class Superstarify(InfractionScheduler, Cog):
         id_ = infraction["id"]
 
         forced_nick = self.get_nick(id_, member.id)
-        expiry_str = format_infraction(infraction["expires_at"])
+        expiry_str = time.discord_timestamp(infraction["expires_at"])
 
         # Apply the infraction
         async def action() -> None:
diff --git a/bot/exts/moderation/modlog.py b/bot/exts/moderation/modlog.py
index fc920499..2c01a4a2 100644
--- a/bot/exts/moderation/modlog.py
+++ b/bot/exts/moderation/modlog.py
@@ -16,8 +16,8 @@ from discord.utils import escape_markdown
 from bot.bot import Bot
 from bot.constants import Categories, Channels, Colours, Emojis, Event, Guild as GuildConstant, Icons, Roles, URLs
 from bot.log import get_logger
+from bot.utils import time
 from bot.utils.messages import format_user
-from bot.utils.time import humanize_delta
 
 log = get_logger(__name__)
 
@@ -407,7 +407,7 @@ class ModLog(Cog, name="ModLog"):
         now = datetime.now(timezone.utc)
         difference = abs(relativedelta(now, member.created_at))
 
-        message = format_user(member) + "\\n\\n**Account age:** " + humanize_delta(difference)
+        message = format_user(member) + "\\n\\n**Account age:** " + time.humanize_delta(difference)
 
         if difference.days < 1 and difference.months < 1 and difference.years < 1:  # New user account!
             message = f"{Emojis.new} {message}"
@@ -713,7 +713,7 @@ class ModLog(Cog, name="ModLog"):
             # datetime as the baseline and create a human-readable delta between this edit event
             # and the last time the message was edited
             timestamp = msg_before.edited_at
-            delta = humanize_delta(relativedelta(msg_after.edited_at, msg_before.edited_at))
+            delta = time.humanize_delta(msg_after.edited_at, msg_before.edited_at)
             footer = f"Last edited {delta} ago"
         else:
             # Message was not previously edited, use the created_at datetime as the baseline, no
diff --git a/bot/exts/moderation/modpings.py b/bot/exts/moderation/modpings.py
index 20a8c39d..b5cd29b1 100644
--- a/bot/exts/moderation/modpings.py
+++ b/bot/exts/moderation/modpings.py
@@ -11,9 +11,8 @@ from bot.bot import Bot
 from bot.constants import Colours, Emojis, Guild, Icons, MODERATION_ROLES, Roles
 from bot.converters import Expiry
 from bot.log import get_logger
-from bot.utils import scheduling
+from bot.utils import scheduling, time
 from bot.utils.scheduling import Scheduler
-from bot.utils.time import TimestampFormats, discord_timestamp
 
 log = get_logger(__name__)
 
@@ -233,8 +232,8 @@ class ModPings(Cog):
 
         await ctx.send(
             f"{Emojis.ok_hand} {ctx.author.mention} Scheduled mod pings from "
-            f"{discord_timestamp(start, TimestampFormats.TIME)} to "
-            f"{discord_timestamp(end, TimestampFormats.TIME)}!"
+            f"{time.discord_timestamp(start, time.TimestampFormats.TIME)} to "
+            f"{time.discord_timestamp(end, time.TimestampFormats.TIME)}!"
         )
 
     @schedule_modpings.command(name='delete', aliases=('del', 'd'))
diff --git a/bot/exts/moderation/slowmode.py b/bot/exts/moderation/slowmode.py
index da04d1e9..b6a77144 100644
--- a/bot/exts/moderation/slowmode.py
+++ b/bot/exts/moderation/slowmode.py
@@ -39,8 +39,7 @@ class Slowmode(Cog):
         if channel is None:
             channel = ctx.channel
 
-        delay = relativedelta(seconds=channel.slowmode_delay)
-        humanized_delay = time.humanize_delta(delay)
+        humanized_delay = time.humanize_delta(seconds=channel.slowmode_delay)
 
         await ctx.send(f'The slowmode delay for {channel.mention} is {humanized_delay}.')
 
diff --git a/bot/exts/moderation/stream.py b/bot/exts/moderation/stream.py
index 99bbd872..4dccc8a7 100644
--- a/bot/exts/moderation/stream.py
+++ b/bot/exts/moderation/stream.py
@@ -14,9 +14,8 @@ from bot.constants import (
 from bot.converters import Expiry
 from bot.log import get_logger
 from bot.pagination import LinePaginator
-from bot.utils import scheduling
+from bot.utils import scheduling, time
 from bot.utils.members import get_or_fetch_member
-from bot.utils.time import discord_timestamp, format_infraction_with_duration
 
 log = get_logger(__name__)
 
@@ -131,10 +130,10 @@ class Stream(commands.Cog):
 
         await member.add_roles(discord.Object(Roles.video), reason="Temporary streaming access granted")
 
-        await ctx.send(f"{Emojis.check_mark} {member.mention} can now stream until {discord_timestamp(duration)}.")
+        await ctx.send(f"{Emojis.check_mark} {member.mention} can now stream until {time.discord_timestamp(duration)}.")
 
         # Convert here for nicer logging
-        revoke_time = format_infraction_with_duration(str(duration))
+        revoke_time = time.format_with_duration(duration)
         log.debug(f"Successfully gave {member} ({member.id}) permission to stream until {revoke_time}.")
 
     @commands.command(aliases=("pstream",))
diff --git a/bot/exts/moderation/watchchannels/_watchchannel.py b/bot/exts/moderation/watchchannels/_watchchannel.py
index 34d44591..ee9b6ba4 100644
--- a/bot/exts/moderation/watchchannels/_watchchannel.py
+++ b/bot/exts/moderation/watchchannels/_watchchannel.py
@@ -18,9 +18,8 @@ from bot.exts.filters.webhook_remover import WEBHOOK_URL_RE
 from bot.exts.moderation.modlog import ModLog
 from bot.log import CustomLogger, get_logger
 from bot.pagination import LinePaginator
-from bot.utils import CogABCMeta, messages, scheduling
+from bot.utils import CogABCMeta, messages, scheduling, time
 from bot.utils.members import get_or_fetch_member
-from bot.utils.time import get_time_delta
 
 log = get_logger(__name__)
 
@@ -286,7 +285,7 @@ class WatchChannel(metaclass=CogABCMeta):
         actor = actor.display_name if actor else self.watched_users[user_id]['actor']
 
         inserted_at = self.watched_users[user_id]['inserted_at']
-        time_delta = get_time_delta(inserted_at)
+        time_delta = time.format_relative(inserted_at)
 
         reason = self.watched_users[user_id]['reason']
 
@@ -360,7 +359,7 @@ class WatchChannel(metaclass=CogABCMeta):
             if member:
                 line += f" ({member.name}#{member.discriminator})"
             inserted_at = user_data['inserted_at']
-            line += f", added {get_time_delta(inserted_at)}"
+            line += f", added {time.format_relative(inserted_at)}"
             if not member:  # Cross off users who left the server.
                 line = f"~~{line}~~"
             list_data["info"][user_id] = line
diff --git a/bot/exts/recruitment/talentpool/_cog.py b/bot/exts/recruitment/talentpool/_cog.py
index 8fa0be5b..0554bf37 100644
--- a/bot/exts/recruitment/talentpool/_cog.py
+++ b/bot/exts/recruitment/talentpool/_cog.py
@@ -17,7 +17,6 @@ from bot.log import get_logger
 from bot.pagination import LinePaginator
 from bot.utils import scheduling, time
 from bot.utils.members import get_or_fetch_member
-from bot.utils.time import get_time_delta
 
 AUTOREVIEW_ENABLED_KEY = "autoreview_enabled"
 REASON_MAX_CHARS = 1000
@@ -181,7 +180,7 @@ class TalentPool(Cog, name="Talentpool"):
             if member:
                 line += f" ({member.name}#{member.discriminator})"
             inserted_at = user_data['inserted_at']
-            line += f", added {get_time_delta(inserted_at)}"
+            line += f", added {time.format_relative(inserted_at)}"
             if not member:  # Cross off users who left the server.
                 line = f"~~{line}~~"
             if user_data['reviewed']:
@@ -260,7 +259,7 @@ class TalentPool(Cog, name="Talentpool"):
             return
 
         if len(reason) > REASON_MAX_CHARS:
-            await ctx.send(f":x: Maxiumum allowed characters for the reason is {REASON_MAX_CHARS}.")
+            await ctx.send(f":x: Maximum allowed characters for the reason is {REASON_MAX_CHARS}.")
             return
 
         # Manual request with `raise_for_status` as False because we want the actual response
@@ -445,7 +444,7 @@ class TalentPool(Cog, name="Talentpool"):
     async def edit_end_reason_command(self, ctx: Context, nomination_id: int, *, reason: str) -> None:
         """Edits the unnominate reason for the nomination with the given `id`."""
         if len(reason) > REASON_MAX_CHARS:
-            await ctx.send(f":x: Maxiumum allowed characters for the end reason is {REASON_MAX_CHARS}.")
+            await ctx.send(f":x: Maximum allowed characters for the end reason is {REASON_MAX_CHARS}.")
             return
 
         try:
@@ -562,7 +561,7 @@ class TalentPool(Cog, name="Talentpool"):
             actor = await get_or_fetch_member(guild, actor_id)
 
             reason = site_entry["reason"] or "*None*"
-            created = time.format_infraction(site_entry["inserted_at"])
+            created = time.discord_timestamp(site_entry["inserted_at"])
             entries.append(
                 f"Actor: {actor.mention if actor else actor_id}\\nCreated: {created}\\nReason: {reason}"
             )
@@ -571,7 +570,7 @@ class TalentPool(Cog, name="Talentpool"):
 
         active = nomination_object["active"]
 
-        start_date = time.format_infraction(nomination_object["inserted_at"])
+        start_date = time.discord_timestamp(nomination_object["inserted_at"])
         if active:
             lines = textwrap.dedent(
                 f"""
@@ -585,7 +584,7 @@ class TalentPool(Cog, name="Talentpool"):
                 """
             )
         else:
-            end_date = time.format_infraction(nomination_object["ended_at"])
+            end_date = time.discord_timestamp(nomination_object["ended_at"])
             lines = textwrap.dedent(
                 f"""
                 ===============
diff --git a/bot/exts/recruitment/talentpool/_review.py b/bot/exts/recruitment/talentpool/_review.py
index 0e719489..b4d17762 100644
--- a/bot/exts/recruitment/talentpool/_review.py
+++ b/bot/exts/recruitment/talentpool/_review.py
@@ -17,10 +17,10 @@ from bot.api import ResponseCodeError
 from bot.bot import Bot
 from bot.constants import Channels, Colours, Emojis, Guild, Roles
 from bot.log import get_logger
+from bot.utils import time
 from bot.utils.members import get_or_fetch_member
 from bot.utils.messages import count_unique_users_reaction, pin_no_system_message
 from bot.utils.scheduling import Scheduler
-from bot.utils.time import get_time_delta, time_since
 
 if typing.TYPE_CHECKING:
     from bot.exts.recruitment.talentpool._cog import TalentPool
@@ -273,7 +273,7 @@ class Reviewer:
                 last_channel = user_activity["top_channel_activity"][-1]
                 channels += f", and {last_channel[1]} in {last_channel[0]}"
 
-        joined_at_formatted = time_since(member.joined_at)
+        joined_at_formatted = time.format_relative(member.joined_at)
         review = (
             f"{member.name} joined the server **{joined_at_formatted}**"
             f" and has **{messages} messages**{channels}."
@@ -321,7 +321,7 @@ class Reviewer:
             infractions += ", with the last infraction issued "
 
         # Infractions were ordered by time since insertion descending.
-        infractions += get_time_delta(infraction_list[0]['inserted_at'])
+        infractions += time.format_relative(infraction_list[0]['inserted_at'])
 
         return f"They have {infractions}."
 
@@ -365,7 +365,7 @@ class Reviewer:
 
         nomination_times = f"{num_entries} times" if num_entries > 1 else "once"
         rejection_times = f"{len(history)} times" if len(history) > 1 else "once"
-        end_time = time_since(isoparse(history[0]['ended_at']))
+        end_time = time.format_relative(history[0]['ended_at'])
 
         review = (
             f"They were nominated **{nomination_times}** before"
diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py
index 90677b2d..289d0035 100644
--- a/bot/exts/utils/reminders.py
+++ b/bot/exts/utils/reminders.py
@@ -13,13 +13,12 @@ from bot.constants import Guild, Icons, MODERATION_ROLES, POSITIVE_REPLIES, Role
 from bot.converters import Duration, UnambiguousUser
 from bot.log import get_logger
 from bot.pagination import LinePaginator
-from bot.utils import scheduling
+from bot.utils import scheduling, time
 from bot.utils.checks import has_any_role_check, has_no_roles_check
 from bot.utils.lock import lock_arg
 from bot.utils.members import get_or_fetch_member
 from bot.utils.messages import send_denial
 from bot.utils.scheduling import Scheduler
-from bot.utils.time import TimestampFormats, discord_timestamp
 
 log = get_logger(__name__)
 
@@ -169,7 +168,7 @@ class Reminders(Cog):
         self.schedule_reminder(reminder)
 
     @lock_arg(LOCK_NAMESPACE, "reminder", itemgetter("id"), raise_error=True)
-    async def send_reminder(self, reminder: dict, expected_time: datetime = None) -> None:
+    async def send_reminder(self, reminder: dict, expected_time: t.Optional[time.Timestamp] = None) -> None:
         """Send the reminder."""
         is_valid, user, channel = self.ensure_valid_reminder(reminder)
         if not is_valid:
@@ -310,7 +309,8 @@ class Reminders(Cog):
             }
         )
 
-        mention_string = f"Your reminder will arrive on {discord_timestamp(expiration, TimestampFormats.DAY_TIME)}"
+        formatted_time = time.discord_timestamp(expiration, time.TimestampFormats.DAY_TIME)
+        mention_string = f"Your reminder will arrive on {formatted_time}"
 
         if mentions:
             mention_string += f" and will mention {len(mentions)} other(s)"
@@ -347,8 +347,7 @@ class Reminders(Cog):
 
         for content, remind_at, id_, mentions in reminders:
             # Parse and humanize the time, make it pretty :D
-            remind_datetime = isoparse(remind_at)
-            time = discord_timestamp(remind_datetime, TimestampFormats.RELATIVE)
+            expiry = time.format_relative(remind_at)
 
             mentions = ", ".join([
                 # Both Role and User objects have the `name` attribute
@@ -357,7 +356,7 @@ class Reminders(Cog):
             mention_string = f"\\n**Mentions:** {mentions}" if mentions else ""
 
             text = textwrap.dedent(f"""
-            **Reminder #{id_}:** *expires {time}* (ID: {id_}){mention_string}
+            **Reminder #{id_}:** *expires {expiry}* (ID: {id_}){mention_string}
             {content}
             """).strip()
 
diff --git a/bot/exts/utils/utils.py b/bot/exts/utils/utils.py
index f76eea51..2a074788 100644
--- a/bot/exts/utils/utils.py
+++ b/bot/exts/utils/utils.py
@@ -13,8 +13,7 @@ from bot.converters import Snowflake
 from bot.decorators import in_whitelist
 from bot.log import get_logger
 from bot.pagination import LinePaginator
-from bot.utils import messages
-from bot.utils.time import time_since
+from bot.utils import messages, time
 
 log = get_logger(__name__)
 
@@ -173,7 +172,7 @@ class Utils(Cog):
         lines = []
         for snowflake in snowflakes:
             created_at = snowflake_time(snowflake)
-            lines.append(f"**{snowflake}**\\nCreated at {created_at} ({time_since(created_at)}).")
+            lines.append(f"**{snowflake}**\\nCreated at {created_at} ({time.format_relative(created_at)}).")
 
         await LinePaginator.paginate(
             lines,
diff --git a/bot/utils/scheduling.py b/bot/utils/scheduling.py
index 7b4c8e2d..23acacf7 100644
--- a/bot/utils/scheduling.py
+++ b/bot/utils/scheduling.py
@@ -5,6 +5,8 @@ import typing as t
 from datetime import datetime
 from functools import partial
 
+from arrow import Arrow
+
 from bot.log import get_logger
 
 
@@ -58,7 +60,7 @@ class Scheduler:
         self._scheduled_tasks[task_id] = task
         self._log.debug(f"Scheduled task #{task_id} {id(task)}.")
 
-    def schedule_at(self, time: datetime, task_id: t.Hashable, coroutine: t.Coroutine) -> None:
+    def schedule_at(self, time: t.Union[datetime, Arrow], task_id: t.Hashable, coroutine: t.Coroutine) -> None:
         """
         Schedule `coroutine` to be executed at the given `time`.
 
diff --git a/bot/utils/time.py b/bot/utils/time.py
index eaa9b72e..a0379c3e 100644
--- a/bot/utils/time.py
+++ b/bot/utils/time.py
@@ -1,15 +1,12 @@
 import datetime
 import re
 from enum import Enum
-from typing import Optional, Union
+from time import struct_time
+from typing import Literal, Optional, Union, overload
 
 import arrow
-import dateutil.parser
 from dateutil.relativedelta import relativedelta
 
-RFC1123_FORMAT = "%a, %d %b %Y %H:%M:%S GMT"
-DISCORD_TIMESTAMP_REGEX = re.compile(r"<t:(\\d+):f>")
-
 _DURATION_REGEX = re.compile(
     r"((?P<years>\\d+?) ?(years|year|Y|y) ?)?"
     r"((?P<months>\\d+?) ?(months|month|m) ?)?"
@@ -20,8 +17,19 @@ _DURATION_REGEX = re.compile(
     r"((?P<seconds>\\d+?) ?(seconds|second|S|s))?"
 )
 
-
-ValidTimestamp = Union[int, datetime.datetime, datetime.date, datetime.timedelta, relativedelta]
+# All supported types for the single-argument overload of arrow.get(). tzinfo is excluded because
+# it's too implicit of a way for the caller to specify that they want the current time.
+Timestamp = Union[
+    arrow.Arrow,
+    datetime.datetime,
+    datetime.date,
+    struct_time,
+    int,  # POSIX timestamp
+    float,  # POSIX timestamp
+    str,  # ISO 8601-formatted string
+    tuple[int, int, int],  # ISO calendar tuple
+]
+_Precision = Literal["years", "months", "days", "hours", "minutes", "seconds"]
 
 
 class TimestampFormats(Enum):
@@ -42,7 +50,7 @@ class TimestampFormats(Enum):
 
 def _stringify_time_unit(value: int, unit: str) -> str:
     """
-    Returns a string to represent a value and time unit, ensuring that it uses the right plural form of the unit.
+    Return a string to represent a value and time unit, ensuring the unit's correct plural form is used.
 
     >>> _stringify_time_unit(1, "seconds")
     "1 second"
@@ -61,33 +69,140 @@ def _stringify_time_unit(value: int, unit: str) -> str:
         return f"{value} {unit}"
 
 
-def discord_timestamp(timestamp: ValidTimestamp, format: TimestampFormats = TimestampFormats.DATE_TIME) -> str:
-    """Create and format a Discord flavored markdown timestamp."""
-    if format not in TimestampFormats:
-        raise ValueError(f"Format can only be one of {', '.join(TimestampFormats.args)}, not {format}.")
+def discord_timestamp(timestamp: Timestamp, format: TimestampFormats = TimestampFormats.DATE_TIME) -> str:
+    """
+    Format a timestamp as a Discord-flavored Markdown timestamp.
+
+    `timestamp` can be any type supported by the single-arg `arrow.get()`, except for a `tzinfo`.
+    """
+    timestamp = int(arrow.get(timestamp).timestamp())
+    return f"<t:{timestamp}:{format.value}>"
+
+
+# region humanize_delta overloads
+@overload
+def humanize_delta(
+    arg1: Union[relativedelta, Timestamp],
+    /,
+    *,
+    precision: _Precision = "seconds",
+    max_units: int = 6,
+    absolute: bool = True,
+) -> str:
+    ...
+
+
+@overload
+def humanize_delta(
+    end: Timestamp,
+    start: Timestamp,
+    /,
+    *,
+    precision: _Precision = "seconds",
+    max_units: int = 6,
+    absolute: bool = True,
+) -> str:
+    ...
+
+
+@overload
+def humanize_delta(
+    *,
+    years: int = 0,
+    months: int = 0,
+    weeks: float = 0,
+    days: float = 0,
+    hours: float = 0,
+    minutes: float = 0,
+    seconds: float = 0,
+    precision: _Precision = "seconds",
+    max_units: int = 6,
+    absolute: bool = True,
+) -> str:
+    ...
+# endregion
+
+
+def humanize_delta(
+    *args,
+    precision: _Precision = "seconds",
+    max_units: int = 6,
+    absolute: bool = True,
+    **kwargs,
+) -> str:
+    """
+    Return a human-readable version of a time duration.
+
+    `precision` is the smallest unit of time to include (e.g. "seconds", "minutes").
 
-    # Convert each possible timestamp class to an integer.
-    if isinstance(timestamp, datetime.datetime):
-        timestamp = (timestamp - arrow.get(0)).total_seconds()
-    elif isinstance(timestamp, datetime.date):
-        timestamp = (timestamp - arrow.get(0)).total_seconds()
-    elif isinstance(timestamp, datetime.timedelta):
-        timestamp = timestamp.total_seconds()
-    elif isinstance(timestamp, relativedelta):
-        timestamp = timestamp.seconds
+    `max_units` is the maximum number of units of time to include.
+    Count units from largest to smallest (e.g. count days before months).
 
-    return f"<t:{int(timestamp)}:{format.value}>"
+    Use the absolute value of the duration if `absolute` is True.
 
+    Usage:
 
-def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units: int = 6) -> str:
-    """
-    Returns a human-readable version of the relativedelta.
+    Keyword arguments specifying values for time units, to construct a `relativedelta` and humanize
+    the duration represented by it:
+
+    >>> humanize_delta(days=2, hours=16, seconds=23)
+    '2 days, 16 hours and 23 seconds'
+
+    **One** `relativedelta` object, to humanize the duration represented by it:
+
+    >>> humanize_delta(relativedelta(years=12, months=6))
+    '12 years and 6 months'
+
+    Note that `leapdays` and absolute info (singular names) will be ignored during humanization.
+
+    **One** timestamp of a type supported by the single-arg `arrow.get()`, except for `tzinfo`,
+    to humanize the duration between it and the current time:
+
+    >>> humanize_delta('2021-08-06T12:43:01Z', absolute=True)  # now = 2021-08-06T12:33:33Z
+    '9 minutes and 28 seconds'
+
+    >>> humanize_delta('2021-08-06T12:43:01Z', absolute=False)  # now = 2021-08-06T12:33:33Z
+    '-9 minutes and -28 seconds'
+
+    **Two** timestamps, each of a type supported by the single-arg `arrow.get()`, except for
+    `tzinfo`, to humanize the duration between them:
+
+    >>> humanize_delta(datetime.datetime(2020, 1, 1), '2021-01-01T12:00:00Z', absolute=False)
+    '1 year and 12 hours'
+
+    >>> humanize_delta('2021-01-01T12:00:00Z', datetime.datetime(2020, 1, 1), absolute=False)
+    '-1 years and -12 hours'
+
+    Note that order of the arguments can result in a different output even if `absolute` is True:
+
+    >>> x = datetime.datetime(3000, 11, 1)
+    >>> y = datetime.datetime(3000, 9, 2)
+    >>> humanize_delta(y, x, absolute=True), humanize_delta(x, y, absolute=True)
+    ('1 month and 30 days', '1 month and 29 days')
 
-    precision specifies the smallest unit of time to include (e.g. "seconds", "minutes").
-    max_units specifies the maximum number of units of time to include (e.g. 1 may include days but not hours).
+    This is due to the nature of `relativedelta`; it does not represent a fixed period of time.
+    Instead, it's relative to the `datetime` to which it's added to get the other `datetime`.
+    In the example, the difference arises because all months don't have the same number of days.
     """
+    if args and kwargs:
+        raise ValueError("Unsupported combination of positional and keyword arguments.")
+
+    if len(args) == 0:
+        delta = relativedelta(**kwargs)
+    elif len(args) == 1 and isinstance(args[0], relativedelta):
+        delta = args[0]
+    elif len(args) <= 2:
+        end = arrow.get(args[0])
+        start = arrow.get(args[1]) if len(args) == 2 else arrow.utcnow()
+
+        delta = relativedelta(end.datetime, start.datetime)
+        if absolute:
+            delta = abs(delta)
+    else:
+        raise ValueError(f"Received {len(args)} positional arguments, but expected 1 or 2.")
+
     if max_units <= 0:
-        raise ValueError("max_units must be positive")
+        raise ValueError("max_units must be positive.")
 
     units = (
         ("years", delta.years),
@@ -98,7 +213,7 @@ def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units:
         ("seconds", delta.seconds),
     )
 
-    # Add the time units that are >0, but stop at accuracy or max_units.
+    # Add the time units that are >0, but stop at precision or max_units.
     time_strings = []
     unit_count = 0
     for unit, value in units:
@@ -109,7 +224,7 @@ def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units:
         if unit == precision or unit_count >= max_units:
             break
 
-    # Add the 'and' between the last two units, if necessary
+    # Add the 'and' between the last two units, if necessary.
     if len(time_strings) > 1:
         time_strings[-1] = f"{time_strings[-2]} and {time_strings[-1]}"
         del time_strings[-2]
@@ -123,19 +238,12 @@ def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units:
     return humanized
 
 
-def get_time_delta(time_string: str) -> str:
-    """Returns the time in human-readable time delta format."""
-    date_time = dateutil.parser.isoparse(time_string)
-    time_delta = time_since(date_time)
-
-    return time_delta
-
-
 def parse_duration_string(duration: str) -> Optional[relativedelta]:
     """
-    Converts a `duration` string to a relativedelta object.
+    Convert a `duration` string to a relativedelta object.
+
+    The following symbols are supported for each unit of time:
 
-    The function supports the following symbols for each unit of time:
     - years: `Y`, `y`, `year`, `years`
     - months: `m`, `month`, `months`
     - weeks: `w`, `W`, `week`, `weeks`
@@ -143,8 +251,9 @@ def parse_duration_string(duration: str) -> Optional[relativedelta]:
     - hours: `H`, `h`, `hour`, `hours`
     - minutes: `M`, `minute`, `minutes`
     - seconds: `S`, `s`, `second`, `seconds`
+
     The units need to be provided in descending order of magnitude.
-    If the string does represent a durationdelta object, it will return None.
+    Return None if the `duration` string cannot be parsed according to the symbols above.
     """
     match = _DURATION_REGEX.fullmatch(duration)
     if not match:
@@ -157,76 +266,63 @@ def parse_duration_string(duration: str) -> Optional[relativedelta]:
 
 
 def relativedelta_to_timedelta(delta: relativedelta) -> datetime.timedelta:
-    """Converts a relativedelta object to a timedelta object."""
+    """Convert a relativedelta object to a timedelta object."""
     utcnow = arrow.utcnow()
     return utcnow + delta - utcnow
 
 
-def time_since(past_datetime: datetime.datetime) -> str:
-    """Takes a datetime and returns a discord timestamp that describes how long ago that datetime was."""
-    return discord_timestamp(past_datetime, TimestampFormats.RELATIVE)
-
-
-def parse_rfc1123(stamp: str) -> datetime.datetime:
-    """Parse RFC1123 time string into datetime."""
-    return datetime.datetime.strptime(stamp, RFC1123_FORMAT).replace(tzinfo=datetime.timezone.utc)
+def format_relative(timestamp: Timestamp) -> str:
+    """
+    Format `timestamp` as a relative Discord timestamp.
 
+    A relative timestamp describes how much time has elapsed since `timestamp` or how much time
+    remains until `timestamp` is reached.
 
-def format_infraction(timestamp: str) -> str:
-    """Format an infraction timestamp to a discord timestamp."""
-    return discord_timestamp(dateutil.parser.isoparse(timestamp))
+    `timestamp` can be any type supported by the single-arg `arrow.get()`, except for a `tzinfo`.
+    """
+    return discord_timestamp(timestamp, TimestampFormats.RELATIVE)
 
 
-def format_infraction_with_duration(
-    date_to: Optional[str],
-    date_from: Optional[datetime.datetime] = None,
+def format_with_duration(
+    timestamp: Optional[Timestamp],
+    other_timestamp: Optional[Timestamp] = None,
     max_units: int = 2,
-    absolute: bool = True
 ) -> Optional[str]:
     """
-    Return `date_to` formatted as a discord timestamp with the timestamp duration since `date_from`.
+    Return `timestamp` formatted as a discord timestamp with the timestamp duration since `other_timestamp`.
+
+    `timestamp` and `other_timestamp` can be any type supported by the single-arg `arrow.get()`,
+    except for a `tzinfo`. Use the current time if `other_timestamp` is None or unspecified.
 
-    `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.
+    `max_units` is forwarded to `time.humanize_delta`. See its documentation for more information.
 
-    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`.
+    Return None if `timestamp` is None.
     """
-    if not date_to:
+    if timestamp is None:
         return None
 
-    date_to_formatted = format_infraction(date_to)
-
-    date_from = date_from or datetime.datetime.now(datetime.timezone.utc)
-    date_to = dateutil.parser.isoparse(date_to).replace(microsecond=0)
+    if other_timestamp is None:
+        other_timestamp = arrow.utcnow()
 
-    delta = relativedelta(date_to, date_from)
-    if absolute:
-        delta = abs(delta)
+    formatted_timestamp = discord_timestamp(timestamp)
+    duration = humanize_delta(timestamp, other_timestamp, max_units=max_units)
 
-    duration = humanize_delta(delta, max_units=max_units)
-    duration_formatted = f" ({duration})" if duration else ""
+    return f"{formatted_timestamp} ({duration})"
 
-    return f"{date_to_formatted}{duration_formatted}"
 
-
-def until_expiration(
-    expiry: Optional[str]
-) -> Optional[str]:
+def until_expiration(expiry: Optional[Timestamp]) -> str:
     """
-    Get the remaining time until infraction's expiration, in a discord timestamp.
+    Get the remaining time until an infraction's expiration as a Discord timestamp.
 
-    Returns a human-readable version of the remaining duration between arrow.utcnow() and an expiry.
-    Similar to time_since, except that this function doesn't error on a null input
-    and return null if the expiry is in the paste
-    """
-    if not expiry:
-        return None
+    `expiry` can be any type supported by the single-arg `arrow.get()`, except for a `tzinfo`.
 
-    now = arrow.utcnow()
-    since = dateutil.parser.isoparse(expiry).replace(microsecond=0)
+    Return "Permanent" if `expiry` is None. Return "Expired" if `expiry` is in the past.
+    """
+    if expiry is None:
+        return "Permanent"
 
-    if since < now:
-        return None
+    expiry = arrow.get(expiry)
+    if expiry < arrow.utcnow():
+        return "Expired"
 
-    return discord_timestamp(since, TimestampFormats.RELATIVE)
+    return format_relative(expiry)
diff --git a/tests/bot/exts/info/test_information.py b/tests/bot/exts/info/test_information.py
index 30e5258f..d896b765 100644
--- a/tests/bot/exts/info/test_information.py
+++ b/tests/bot/exts/info/test_information.py
@@ -1,6 +1,7 @@
 import textwrap
 import unittest
 import unittest.mock
+from datetime import datetime
 
 import discord
 
@@ -288,6 +289,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
         user.nick = None
         user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock")
         user.colour = 0
+        user.created_at = user.joined_at = datetime.utcnow()
 
         embed = await self.cog.create_user_embed(ctx, user, False)
 
@@ -309,6 +311,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
         user.nick = "Cat lover"
         user.__str__ = unittest.mock.Mock(return_value="Mr. Hemlock")
         user.colour = 0
+        user.created_at = user.joined_at = datetime.utcnow()
 
         embed = await self.cog.create_user_embed(ctx, user, False)
 
@@ -329,6 +332,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
 
         # A `MockMember` has the @Everyone role by default; we add the Admins to that.
         user = helpers.MockMember(roles=[admins_role], colour=100)
+        user.created_at = user.joined_at = datetime.utcnow()
 
         embed = await self.cog.create_user_embed(ctx, user, False)
 
@@ -355,6 +359,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
         nomination_counts.return_value = ("Nominations", "nomination info")
 
         user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)
+        user.created_at = user.joined_at = datetime.utcfromtimestamp(1)
         embed = await self.cog.create_user_embed(ctx, user, False)
 
         infraction_counts.assert_called_once_with(user)
@@ -394,6 +399,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
         user_messages.return_value = ("Messages", "user message counts")
 
         user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)
+        user.created_at = user.joined_at = datetime.utcfromtimestamp(1)
         embed = await self.cog.create_user_embed(ctx, user, False)
 
         infraction_counts.assert_called_once_with(user)
@@ -440,6 +446,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
         moderators_role = helpers.MockRole(name='Moderators')
 
         user = helpers.MockMember(id=314, roles=[moderators_role], colour=100)
+        user.created_at = user.joined_at = datetime.utcnow()
         embed = await self.cog.create_user_embed(ctx, user, False)
 
         self.assertEqual(embed.colour, discord.Colour(100))
@@ -457,6 +464,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
         ctx = helpers.MockContext()
 
         user = helpers.MockMember(id=217, colour=discord.Colour.default())
+        user.created_at = user.joined_at = datetime.utcnow()
         embed = await self.cog.create_user_embed(ctx, user, False)
 
         self.assertEqual(embed.colour, discord.Colour.og_blurple())
@@ -474,6 +482,7 @@ class UserEmbedTests(unittest.IsolatedAsyncioTestCase):
         ctx = helpers.MockContext()
 
         user = helpers.MockMember(id=217, colour=0)
+        user.created_at = user.joined_at = datetime.utcnow()
         user.display_avatar.url = "avatar url"
         embed = await self.cog.create_user_embed(ctx, user, False)
 
diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py
index a3dcbfc0..120d6517 100644
--- a/tests/bot/utils/test_time.py
+++ b/tests/bot/utils/test_time.py
@@ -13,13 +13,15 @@ class TimeTests(unittest.TestCase):
         """humanize_delta should be able to handle unknown units, and will not abort."""
         # Does not abort for unknown units, as the unit name is checked
         # against the attribute of the relativedelta instance.
-        self.assertEqual(time.humanize_delta(relativedelta(days=2, hours=2), 'elephants', 2), '2 days and 2 hours')
+        actual = time.humanize_delta(relativedelta(days=2, hours=2), precision='elephants', max_units=2)
+        self.assertEqual(actual, '2 days and 2 hours')
 
     def test_humanize_delta_handle_high_units(self):
         """humanize_delta should be able to handle very high units."""
         # Very high maximum units, but it only ever iterates over
         # each value the relativedelta might have.
-        self.assertEqual(time.humanize_delta(relativedelta(days=2, hours=2), 'hours', 20), '2 days and 2 hours')
+        actual = time.humanize_delta(relativedelta(days=2, hours=2), precision='hours', max_units=20)
+        self.assertEqual(actual, '2 days and 2 hours')
 
     def test_humanize_delta_should_normal_usage(self):
         """Testing humanize delta."""
@@ -32,7 +34,8 @@ class TimeTests(unittest.TestCase):
 
         for delta, precision, max_units, expected in test_cases:
             with self.subTest(delta=delta, precision=precision, max_units=max_units, expected=expected):
-                self.assertEqual(time.humanize_delta(delta, precision, max_units), expected)
+                actual = time.humanize_delta(delta, precision=precision, max_units=max_units)
+                self.assertEqual(actual, expected)
 
     def test_humanize_delta_raises_for_invalid_max_units(self):
         """humanize_delta should raises ValueError('max_units must be positive') for invalid max_units."""
@@ -40,22 +43,11 @@ class TimeTests(unittest.TestCase):
 
         for max_units in test_cases:
             with self.subTest(max_units=max_units), self.assertRaises(ValueError) as error:
-                time.humanize_delta(relativedelta(days=2, hours=2), 'hours', max_units)
-            self.assertEqual(str(error.exception), 'max_units must be positive')
-
-    def test_parse_rfc1123(self):
-        """Testing parse_rfc1123."""
-        self.assertEqual(
-            time.parse_rfc1123('Sun, 15 Sep 2019 12:00:00 GMT'),
-            datetime(2019, 9, 15, 12, 0, 0, tzinfo=timezone.utc)
-        )
-
-    def test_format_infraction(self):
-        """Testing format_infraction."""
-        self.assertEqual(time.format_infraction('2019-12-12T00:01:00Z'), '<t:1576108860:f>')
+                time.humanize_delta(relativedelta(days=2, hours=2), precision='hours', max_units=max_units)
+            self.assertEqual(str(error.exception), 'max_units must be positive.')
 
-    def test_format_infraction_with_duration_none_expiry(self):
-        """format_infraction_with_duration should work for None expiry."""
+    def test_format_with_duration_none_expiry(self):
+        """format_with_duration should work for None expiry."""
         test_cases = (
             (None, None, None, None),
 
@@ -67,10 +59,10 @@ class TimeTests(unittest.TestCase):
 
         for expiry, date_from, max_units, expected in test_cases:
             with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected):
-                self.assertEqual(time.format_infraction_with_duration(expiry, date_from, max_units), expected)
+                self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected)
 
-    def test_format_infraction_with_duration_custom_units(self):
-        """format_infraction_with_duration should work for custom max_units."""
+    def test_format_with_duration_custom_units(self):
+        """format_with_duration should work for custom max_units."""
         test_cases = (
             ('3000-12-12T00:01:00Z', datetime(3000, 12, 11, 12, 5, 5, tzinfo=timezone.utc), 6,
              '<t:32533488060:f> (11 hours, 55 minutes and 55 seconds)'),
@@ -80,10 +72,10 @@ class TimeTests(unittest.TestCase):
 
         for expiry, date_from, max_units, expected in test_cases:
             with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected):
-                self.assertEqual(time.format_infraction_with_duration(expiry, date_from, max_units), expected)
+                self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected)
 
-    def test_format_infraction_with_duration_normal_usage(self):
-        """format_infraction_with_duration should work for normal usage, across various durations."""
+    def test_format_with_duration_normal_usage(self):
+        """format_with_duration should work for normal usage, across various durations."""
         utc = timezone.utc
         test_cases = (
             ('2019-12-12T00:01:00Z', datetime(2019, 12, 11, 12, 0, 5, tzinfo=utc), 2,
@@ -105,11 +97,11 @@ class TimeTests(unittest.TestCase):
 
         for expiry, date_from, max_units, expected in test_cases:
             with self.subTest(expiry=expiry, date_from=date_from, max_units=max_units, expected=expected):
-                self.assertEqual(time.format_infraction_with_duration(expiry, date_from, max_units), expected)
+                self.assertEqual(time.format_with_duration(expiry, date_from, max_units), expected)
 
     def test_until_expiration_with_duration_none_expiry(self):
-        """until_expiration should work for None expiry."""
-        self.assertEqual(time.until_expiration(None), None)
+        """until_expiration should return "Permanent" is expiry is None."""
+        self.assertEqual(time.until_expiration(None), "Permanent")
 
     def test_until_expiration_with_duration_custom_units(self):
         """until_expiration should work for custom max_units."""
@@ -130,7 +122,6 @@ class TimeTests(unittest.TestCase):
             ('3000-12-12T00:00:00Z', '<t:32533488000:R>'),
             ('3000-11-23T20:09:00Z', '<t:32531918940:R>'),
             ('3000-11-23T20:09:00Z', '<t:32531918940:R>'),
-            (None, None),
         )
 
         for expiry, expected in test_cases: