1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
import contextlib
import arrow
import discord
from dateutil import parser
from discord.ext import commands
from bot.bot import Bot
# https://discord.com/developers/docs/reference#message-formatting-timestamp-styles
STYLES = {
"Epoch": ("",),
"Short Time": ("t", "h:mm A",),
"Long Time": ("T", "h:mm:ss A"),
"Short Date": ("d", "MM/DD/YYYY"),
"Long Date": ("D", "MMMM D, YYYY"),
"Short Date/Time": ("f", "MMMM D, YYYY h:mm A"),
"Long Date/Time": ("F", "dddd, MMMM D, YYYY h:mm A"),
"Relative Time": ("R",)
}
DROPDOWN_TIMEOUT = 60
class DateString(commands.Converter):
"""Convert a relative or absolute date/time string to an arrow.Arrow object."""
async def convert(self, ctx: commands.Context, argument: str) -> arrow.Arrow | tuple | None:
"""
Convert a relative or absolute date/time string to an arrow.Arrow object.
Try to interpret the date string as a relative time. If conversion fails, try to interpret it as an absolute
time. Tokens that are not recognised are returned along with the part of the string that was successfully
converted to an arrow object. If the date string cannot be parsed, BadArgument is raised.
"""
try:
return arrow.utcnow().dehumanize(argument)
except (ValueError, OverflowError):
try:
dt, ignored_tokens = parser.parse(argument, fuzzy_with_tokens=True)
except parser.ParserError:
raise commands.BadArgument(f"`{argument}` Could not be parsed to a relative or absolute date.")
except OverflowError:
raise commands.BadArgument(f"`{argument}` Results in a date outside of the supported range.")
return arrow.get(dt), ignored_tokens
class Epoch(commands.Cog):
"""Convert an entered time and date to a unix timestamp."""
def __init__(self, bot: Bot) -> None:
self.bot = bot
@commands.command(name="epoch")
async def epoch(self, ctx: commands.Context, *, date_time: DateString = None) -> None:
"""
Convert an entered date/time string to the equivalent epoch.
**Relative time**
Must begin with `in...` or end with `...ago`.
Accepted units: "seconds", "minutes", "hours", "days", "weeks", "months", "years".
eg `.epoch in a month 4 days and 2 hours`
**Absolute time**
eg `.epoch 2022/6/15 16:43 -04:00`
Absolute times must be entered in descending orders of magnitude.
If AM or PM is left unspecified, the 24-hour clock is assumed.
Timezones are optional, and will default to UTC. The following timezone formats are accepted:
Z (UTC)
±HH:MM
±HHMM
±HH
Times in the dropdown are shown in UTC
"""
if not date_time:
await self.bot.invoke_help_command(ctx)
return
if isinstance(date_time, tuple):
# Remove empty strings. Strip extra whitespace from the remaining items
ignored_tokens = list(map(str.strip, filter(str.strip, date_time[1])))
date_time = date_time[0]
if ignored_tokens:
await ctx.send(f"Could not parse the following token(s): `{', '.join(ignored_tokens)}`")
await ctx.send(f"Date and time parsed as: `{date_time.format(arrow.FORMAT_RSS)}`")
epoch = int(date_time.timestamp())
view = TimestampMenuView(ctx, self._format_dates(date_time), epoch)
original = await ctx.send(f"`{epoch}`", view=view)
await view.wait() # wait until expiration before removing the dropdown
with contextlib.suppress(discord.NotFound):
await original.edit(view=None)
@staticmethod
def _format_dates(date: arrow.Arrow) -> list[str]:
"""
Return a list of date strings formatted according to the discord timestamp styles.
These are used in the description of each style in the dropdown
"""
date = date.to("utc")
formatted = [str(int(date.timestamp()))]
formatted += [date.format(format[1]) for format in list(STYLES.values())[1:7]]
formatted.append(date.humanize())
return formatted
class TimestampMenuView(discord.ui.View):
"""View for the epoch command which contains a single `discord.ui.Select` dropdown component."""
def __init__(self, ctx: commands.Context, formatted_times: list[str], epoch: int):
super().__init__(timeout=DROPDOWN_TIMEOUT)
self.ctx = ctx
self.epoch = epoch
self.dropdown: discord.ui.Select = self.children[0]
for label, date_time in zip(STYLES.keys(), formatted_times, strict=True):
self.dropdown.add_option(label=label, description=date_time)
@discord.ui.select(placeholder="Select the format of your timestamp")
async def select_format(self, interaction: discord.Interaction, _: discord.ui.Select) -> discord.Message:
"""Drop down menu which contains a list of formats which discord timestamps can take."""
selected = interaction.data["values"][0]
if selected == "Epoch":
return await interaction.response.edit_message(content=f"`{self.epoch}`")
return await interaction.response.edit_message(content=f"`<t:{self.epoch}:{STYLES[selected][0]}>`")
async def interaction_check(self, interaction: discord.Interaction) -> bool:
"""Check to ensure that the interacting user is the user who invoked the command."""
if interaction.user != self.ctx.author:
embed = discord.Embed(description="Sorry, but this dropdown menu can only be used by the original author.")
await interaction.response.send_message(embed=embed, ephemeral=True)
return False
return True
async def setup(bot: Bot) -> None:
"""Load the Epoch cog."""
await bot.add_cog(Epoch(bot))
|