aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/exts/moderation/infraction/_scheduler.py7
-rw-r--r--bot/exts/moderation/infraction/management.py4
-rw-r--r--bot/exts/moderation/infraction/superstarify.py4
-rw-r--r--bot/exts/moderation/stream.py2
-rw-r--r--bot/exts/moderation/watchchannels/_watchchannel.py4
-rw-r--r--bot/exts/recruitment/talentpool/_cog.py8
-rw-r--r--bot/exts/recruitment/talentpool/_review.py4
-rw-r--r--bot/exts/utils/reminders.py5
-rw-r--r--bot/utils/time.py94
-rw-r--r--tests/bot/utils/test_time.py4
10 files changed, 63 insertions, 73 deletions
diff --git a/bot/exts/moderation/infraction/_scheduler.py b/bot/exts/moderation/infraction/_scheduler.py
index 9d4d58e2e..47b639421 100644
--- a/bot/exts/moderation/infraction/_scheduler.py
+++ b/bot/exts/moderation/infraction/_scheduler.py
@@ -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_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 dd994a2d2..23c6e8b92 100644
--- a/bot/exts/moderation/infraction/management.py
+++ b/bot/exts/moderation/infraction/management.py
@@ -150,7 +150,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_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")
@@ -351,7 +351,7 @@ class ModManagement(commands.Cog):
active = infraction["active"]
user = infraction["user"]
expires_at = infraction["expires_at"]
- created = time.format_infraction(infraction["inserted_at"])
+ created = time.discord_timestamp(infraction["inserted_at"])
dm_sent = infraction["dm_sent"]
# Format the user string.
diff --git a/bot/exts/moderation/infraction/superstarify.py b/bot/exts/moderation/infraction/superstarify.py
index 2e272dbb0..a037ca1be 100644
--- a/bot/exts/moderation/infraction/superstarify.py
+++ b/bot/exts/moderation/infraction/superstarify.py
@@ -73,7 +73,7 @@ class Superstarify(InfractionScheduler, Cog):
notified = await _utils.notify_infraction(
user=after,
infr_type="Superstarify",
- expires_at=time.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 = time.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/stream.py b/bot/exts/moderation/stream.py
index bc9d35714..4dccc8a7e 100644
--- a/bot/exts/moderation/stream.py
+++ b/bot/exts/moderation/stream.py
@@ -133,7 +133,7 @@ class Stream(commands.Cog):
await ctx.send(f"{Emojis.check_mark} {member.mention} can now stream until {time.discord_timestamp(duration)}.")
# Convert here for nicer logging
- revoke_time = time.format_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 106483527..ee9b6ba45 100644
--- a/bot/exts/moderation/watchchannels/_watchchannel.py
+++ b/bot/exts/moderation/watchchannels/_watchchannel.py
@@ -285,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 = time.get_time_delta(inserted_at)
+ time_delta = time.format_relative(inserted_at)
reason = self.watched_users[user_id]['reason']
@@ -359,7 +359,7 @@ class WatchChannel(metaclass=CogABCMeta):
if member:
line += f" ({member.name}#{member.discriminator})"
inserted_at = user_data['inserted_at']
- line += f", added {time.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 80274eaea..bbc135454 100644
--- a/bot/exts/recruitment/talentpool/_cog.py
+++ b/bot/exts/recruitment/talentpool/_cog.py
@@ -180,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 {time.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']:
@@ -561,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}"
)
@@ -570,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"""
@@ -584,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 474f669c6..b4d177622 100644
--- a/bot/exts/recruitment/talentpool/_review.py
+++ b/bot/exts/recruitment/talentpool/_review.py
@@ -321,7 +321,7 @@ class Reviewer:
infractions += ", with the last infraction issued "
# Infractions were ordered by time since insertion descending.
- infractions += time.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.format_relative(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 bfa294809..289d00356 100644
--- a/bot/exts/utils/reminders.py
+++ b/bot/exts/utils/reminders.py
@@ -168,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:
@@ -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)
- expiry = time.format_relative(remind_datetime)
+ expiry = time.format_relative(remind_at)
mentions = ", ".join([
# Both Role and User objects have the `name` attribute
diff --git a/bot/utils/time.py b/bot/utils/time.py
index 13dfc6fb7..e927a5e63 100644
--- a/bot/utils/time.py
+++ b/bot/utils/time.py
@@ -1,10 +1,10 @@
import datetime
import re
from enum import Enum
+from time import struct_time
from typing import Optional, Union
import arrow
-import dateutil.parser
from dateutil.relativedelta import relativedelta
DISCORD_TIMESTAMP_REGEX = re.compile(r"<t:(\d+):f>")
@@ -19,8 +19,18 @@ _DURATION_REGEX = re.compile(
r"((?P<seconds>\d+?) ?(seconds|second|S|s))?"
)
-
-ValidTimestamp = Union[int, datetime.datetime, datetime.date]
+# 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
+]
class TimestampFormats(Enum):
@@ -60,15 +70,14 @@ 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."""
- # 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()
+def discord_timestamp(timestamp: Timestamp, format: TimestampFormats = TimestampFormats.DATE_TIME) -> str:
+ """
+ Format a timestamp as a Discord-flavored Markdown timestamp.
- return f"<t:{int(timestamp)}:{format.value}>"
+ `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}>"
def humanize_delta(delta: relativedelta, precision: str = "seconds", max_units: int = 6) -> str:
@@ -115,14 +124,6 @@ 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 = format_relative(date_time)
-
- return time_delta
-
-
def parse_duration_string(duration: str) -> Optional[relativedelta]:
"""
Converts a `duration` string to a relativedelta object.
@@ -154,64 +155,63 @@ def relativedelta_to_timedelta(delta: relativedelta) -> datetime.timedelta:
return utcnow + delta - utcnow
-def format_relative(timestamp: ValidTimestamp) -> str:
+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. See `time.discord_timestamp`.
+ remains until `timestamp` is reached.
+
+ `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(timestamp: str) -> str:
- """Format an infraction timestamp to a discord timestamp."""
- return discord_timestamp(dateutil.parser.isoparse(timestamp))
-
-
def format_with_duration(
- timestamp: Optional[str],
- other_timestamp: Optional[datetime.datetime] = None,
+ timestamp: Optional[Timestamp],
+ other_timestamp: Optional[Timestamp] = None,
max_units: int = 2,
) -> Optional[str]:
"""
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 falsy 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.
+
+ Return None if `timestamp` is falsy.
"""
if not timestamp:
return None
- date_to_formatted = format_infraction(timestamp)
-
- other_timestamp = other_timestamp or datetime.datetime.now(datetime.timezone.utc)
- timestamp = dateutil.parser.isoparse(timestamp).replace(microsecond=0)
+ timestamp = arrow.get(timestamp)
+ if not other_timestamp:
+ other_timestamp = arrow.utcnow()
+ else:
+ other_timestamp = arrow.get(other_timestamp)
- delta = abs(relativedelta(timestamp, other_timestamp))
+ formatted_timestamp = discord_timestamp(timestamp)
+ delta = abs(relativedelta(timestamp.datetime, other_timestamp.datetime))
duration = humanize_delta(delta, max_units=max_units)
- duration_formatted = f" ({duration})" if duration else ""
- return f"{date_to_formatted}{duration_formatted}"
+ return f"{formatted_timestamp} ({duration})"
-def until_expiration(
- expiry: Optional[str]
-) -> Optional[str]:
+def until_expiration(expiry: Optional[Timestamp]) -> Optional[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 format_relative, except that this function doesn't error on a null input
- and return null if the expiry is in the paste
+ `expiry` can be any type supported by the single-arg `arrow.get()`, except for a `tzinfo`.
+
+ Return None if `expiry` is falsy or is in the past.
"""
if not expiry:
return None
- now = arrow.utcnow()
- since = dateutil.parser.isoparse(expiry).replace(microsecond=0)
-
- if since < now:
+ expiry = arrow.get(expiry)
+ if expiry < arrow.utcnow():
return None
- return format_relative(since)
+ return format_relative(expiry)
diff --git a/tests/bot/utils/test_time.py b/tests/bot/utils/test_time.py
index 02b5f8c17..027e2052e 100644
--- a/tests/bot/utils/test_time.py
+++ b/tests/bot/utils/test_time.py
@@ -43,10 +43,6 @@ class TimeTests(unittest.TestCase):
time.humanize_delta(relativedelta(days=2, hours=2), 'hours', max_units)
self.assertEqual(str(error.exception), 'max_units must be positive')
- def test_format_infraction(self):
- """Testing format_infraction."""
- self.assertEqual(time.format_infraction('2019-12-12T00:01:00Z'), '<t:1576108860:f>')
-
def test_format_with_duration_none_expiry(self):
"""format_with_duration should work for None expiry."""
test_cases = (