Add Discord timestamp converter and transformer

This commit is contained in:
Alex Nørgaard
2026-02-23 03:13:10 +00:00
committed by GitHub
parent fd5a218d7c
commit 8bad09e1d8
6 changed files with 95 additions and 1 deletions

View File

@@ -23,6 +23,7 @@ DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import datetime
import inspect
from dataclasses import dataclass
@@ -52,7 +53,7 @@ from ..channel import StageChannel, VoiceChannel, TextChannel, CategoryChannel,
from ..abc import GuildChannel
from ..threads import Thread
from ..enums import Enum as InternalEnum, AppCommandOptionType, ChannelType, Locale
from ..utils import MISSING, maybe_coroutine, _human_join
from ..utils import MISSING, maybe_coroutine, _human_join, TIMESTAMP_PATTERN
from ..user import User
from ..role import Role
from ..member import Member
@@ -62,6 +63,7 @@ from .._types import ClientT
__all__ = (
'Transformer',
'Transform',
'Timestamp',
'Range',
)
@@ -681,6 +683,41 @@ class UnionChannelTransformer(BaseChannelTransformer[ClientT]):
return resolved
if TYPE_CHECKING:
Timestamp = datetime.datetime
else:
class Timestamp(Transformer[ClientT]):
"""A type annotation that can be applied to a parameter for transforming a :ddocs:`Discord style timestamp <reference#message-formatting>` input to a
:class:`datetime.datetime`.
.. versionadded:: 2.7
.. warning::
Due to a Discord limitation, no timezone is provided with the input. The UTC timezone has been supplanted instead.
Examples
---------
.. code-block:: python3
@app_commands.command()
async def datetime(interaction: discord.Interaction, value: app_commands.Timestamp):
await interaction.response.send_message(value.isoformat())
"""
@property
def type(self) -> AppCommandOptionType:
return AppCommandOptionType.string
async def transform(self, interaction: Interaction[ClientT], value: Any, /):
match = TIMESTAMP_PATTERN.match(value)
if not match:
raise TransformerError(value, AppCommandOptionType.string, self)
return datetime.datetime.fromtimestamp(int(match[1]), tz=datetime.timezone.utc)
CHANNEL_TO_TYPES: Dict[Any, List[ChannelType]] = {
AppCommandChannel: [
ChannelType.stage_voice,

View File

@@ -24,6 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations
import datetime
import inspect
import re
from typing import (
@@ -86,6 +87,7 @@ __all__ = (
'clean_content',
'Greedy',
'Range',
'Timestamp',
'run_converters',
)
@@ -893,6 +895,28 @@ class GuildStickerConverter(IDConverter[discord.GuildSticker]):
return result
if TYPE_CHECKING:
Timestamp = datetime.datetime
else:
class Timestamp(Converter[str]):
"""Converts to a :class:`datetime.datetime`.
Conversion is attempted based on the :ddocs:`Discord style timestamp <reference#message-formatting>` input format.
.. versionadded:: 2.7
.. warning::
Due to a Discord limitation, no timezone is provided with the input. The UTC timezone has been supplanted instead.
"""
async def convert(self, ctx: Context[BotT], argument: str) -> datetime.datetime:
match = discord.utils.TIMESTAMP_PATTERN.match(argument)
if not match:
raise BadTimestampArgument(argument)
return datetime.datetime.fromtimestamp(int(match[1]), tz=datetime.timezone.utc)
class ScheduledEventConverter(IDConverter[discord.ScheduledEvent]):
"""Converts to a :class:`~discord.ScheduledEvent`.

View File

@@ -79,6 +79,7 @@ __all__ = (
'SoundboardSoundNotFound',
'PartialEmojiConversionFailure',
'BadBoolArgument',
'BadTimestampArgument',
'MissingRole',
'BotMissingRole',
'MissingAnyRole',
@@ -602,6 +603,24 @@ class BadBoolArgument(BadArgument):
super().__init__(f'{argument} is not a recognised boolean option')
class BadTimestampArgument(BadArgument):
"""Exception raised when a timestamp argument was not convertable.
This inherits from :exc:`BadArgument`
.. versionadded:: 2.7
Attributes
-----------
argument: :class:`str`
The datetime/timestamp argument supplied by the caller that was not a valid timestamp format.
"""
def __init__(self, argument: str) -> None:
self.argument: str = argument
super().__init__(f'{argument} is not a recognised datetime or timestamp option')
class RangeError(BadArgument):
"""Exception raised when an argument is out of range.

View File

@@ -118,6 +118,7 @@ __all__ = (
DISCORD_EPOCH = 1420070400000
DEFAULT_FILE_SIZE_LIMIT_BYTES = 10485760
TIMESTAMP_PATTERN: re.Pattern[str] = re.compile(r'<t:(-?\d+)(?::[tTdDfFsSR])?>')
class _MissingSentinel: