Browse Source

Merge branch 'master' into feat/community-invite

pull/10386/head
Soheab 4 months ago
parent
commit
8e802f7622
  1. 39
      discord/app_commands/transformers.py
  2. 44
      discord/client.py
  3. 24
      discord/ext/commands/converter.py
  4. 19
      discord/ext/commands/errors.py
  5. 6
      discord/gateway.py
  6. 16
      discord/integrations.py
  7. 27
      discord/message.py
  8. 13
      discord/ui/view.py
  9. 1
      discord/utils.py
  10. 5
      docs/ext/commands/api.rst
  11. 8
      docs/interactions/api.rst

39
discord/app_commands/transformers.py

@ -23,6 +23,7 @@ DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
import datetime
import inspect import inspect
from dataclasses import dataclass from dataclasses import dataclass
@ -52,7 +53,7 @@ from ..channel import StageChannel, VoiceChannel, TextChannel, CategoryChannel,
from ..abc import GuildChannel from ..abc import GuildChannel
from ..threads import Thread from ..threads import Thread
from ..enums import Enum as InternalEnum, AppCommandOptionType, ChannelType, Locale 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 ..user import User
from ..role import Role from ..role import Role
from ..member import Member from ..member import Member
@ -62,6 +63,7 @@ from .._types import ClientT
__all__ = ( __all__ = (
'Transformer', 'Transformer',
'Transform', 'Transform',
'Timestamp',
'Range', 'Range',
) )
@ -681,6 +683,41 @@ class UnionChannelTransformer(BaseChannelTransformer[ClientT]):
return resolved 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]] = { CHANNEL_TO_TYPES: Dict[Any, List[ChannelType]] = {
AppCommandChannel: [ AppCommandChannel: [
ChannelType.stage_voice, ChannelType.stage_voice,

44
discord/client.py

@ -88,7 +88,7 @@ if TYPE_CHECKING:
from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime
from .app_commands import Command, ContextMenu from .app_commands import Command, ContextMenu
from .automod import AutoModAction, AutoModRule from .automod import AutoModAction, AutoModRule
from .channel import DMChannel, GroupChannel from .channel import DMChannel, GroupChannel, VoiceChannelEffect
from .ext.commands import AutoShardedBot, Bot, Context, CommandError from .ext.commands import AutoShardedBot, Bot, Context, CommandError
from .guild import GuildChannel from .guild import GuildChannel
from .integrations import Integration from .integrations import Integration
@ -1753,6 +1753,38 @@ class Client:
timeout: Optional[float] = ..., timeout: Optional[float] = ...,
) -> Tuple[ScheduledEvent, User]: ... ) -> Tuple[ScheduledEvent, User]: ...
@overload
async def wait_for(
self,
event: Literal['scheduled_event_update'],
/,
*,
check: Optional[Callable[[ScheduledEvent, ScheduledEvent], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[ScheduledEvent, ScheduledEvent]: ...
# Soundboard
@overload
async def wait_for(
self,
event: Literal['soundboard_sound_create', 'soundboard_sound_delete'],
/,
*,
check: Optional[Callable[[SoundboardSound], bool]] = ...,
timeout: Optional[float] = ...,
) -> SoundboardSound: ...
@overload
async def wait_for(
self,
event: Literal['soundboard_sound_update'],
/,
*,
check: Optional[Callable[[SoundboardSound, SoundboardSound], bool]] = ...,
timeout: Optional[float] = ...,
) -> Tuple[SoundboardSound, SoundboardSound]: ...
# Stages # Stages
@overload @overload
@ -1859,6 +1891,16 @@ class Client:
timeout: Optional[float] = ..., timeout: Optional[float] = ...,
) -> Tuple[Member, VoiceState, VoiceState]: ... ) -> Tuple[Member, VoiceState, VoiceState]: ...
@overload
async def wait_for(
self,
event: Literal['voice_channel_effect'],
/,
*,
check: Optional[Callable[[VoiceChannelEffect], bool]] = ...,
timeout: Optional[float] = ...,
) -> VoiceChannelEffect: ...
# Polls # Polls
@overload @overload

24
discord/ext/commands/converter.py

@ -24,6 +24,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
import datetime
import inspect import inspect
import re import re
from typing import ( from typing import (
@ -86,6 +87,7 @@ __all__ = (
'clean_content', 'clean_content',
'Greedy', 'Greedy',
'Range', 'Range',
'Timestamp',
'run_converters', 'run_converters',
) )
@ -893,6 +895,28 @@ class GuildStickerConverter(IDConverter[discord.GuildSticker]):
return result 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]): class ScheduledEventConverter(IDConverter[discord.ScheduledEvent]):
"""Converts to a :class:`~discord.ScheduledEvent`. """Converts to a :class:`~discord.ScheduledEvent`.

19
discord/ext/commands/errors.py

@ -79,6 +79,7 @@ __all__ = (
'SoundboardSoundNotFound', 'SoundboardSoundNotFound',
'PartialEmojiConversionFailure', 'PartialEmojiConversionFailure',
'BadBoolArgument', 'BadBoolArgument',
'BadTimestampArgument',
'MissingRole', 'MissingRole',
'BotMissingRole', 'BotMissingRole',
'MissingAnyRole', 'MissingAnyRole',
@ -602,6 +603,24 @@ class BadBoolArgument(BadArgument):
super().__init__(f'{argument} is not a recognised boolean option') 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): class RangeError(BadArgument):
"""Exception raised when an argument is out of range. """Exception raised when an argument is out of range.

6
discord/gateway.py

@ -210,6 +210,10 @@ class KeepAliveHandler(threading.Thread):
def tick(self) -> None: def tick(self) -> None:
self._last_recv = time.perf_counter() self._last_recv = time.perf_counter()
def beat(self) -> Dict[str, Any]:
self._last_send = time.perf_counter()
return self.get_payload()
def ack(self) -> None: def ack(self) -> None:
ack_time = time.perf_counter() ack_time = time.perf_counter()
self._last_ack = ack_time self._last_ack = ack_time
@ -541,7 +545,7 @@ class DiscordWebSocket:
if op == self.HEARTBEAT: if op == self.HEARTBEAT:
if self._keep_alive: if self._keep_alive:
beat = self._keep_alive.get_payload() beat = self._keep_alive.beat()
await self.send_as_json(beat) await self.send_as_json(beat)
return return

16
discord/integrations.py

@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE.
from __future__ import annotations from __future__ import annotations
import datetime import datetime
from typing import Any, Dict, Optional, TYPE_CHECKING, Type, Tuple from typing import Any, Dict, List, Optional, TYPE_CHECKING, Type, Tuple
from .utils import _get_as_snowflake, parse_time, MISSING from .utils import _get_as_snowflake, parse_time, MISSING
from .user import User from .user import User
from .enums import try_enum, ExpireBehaviour from .enums import try_enum, ExpireBehaviour
@ -98,6 +98,10 @@ class Integration:
The account linked to this integration. The account linked to this integration.
user: :class:`User` user: :class:`User`
The user that added this integration. The user that added this integration.
scopes: List[:class:`str`]
The OAuth2 scopes the application has been authorized for.
.. versionadded:: 2.7
""" """
__slots__ = ( __slots__ = (
@ -109,6 +113,7 @@ class Integration:
'account', 'account',
'user', 'user',
'enabled', 'enabled',
'scopes',
) )
def __init__(self, *, data: IntegrationPayload, guild: Guild) -> None: def __init__(self, *, data: IntegrationPayload, guild: Guild) -> None:
@ -128,6 +133,7 @@ class Integration:
user = data.get('user') user = data.get('user')
self.user: Optional[User] = User(state=self._state, data=user) if user else None self.user: Optional[User] = User(state=self._state, data=user) if user else None
self.enabled: bool = data['enabled'] self.enabled: bool = data['enabled']
self.scopes: List[str] = data.get('scopes', [])
async def delete(self, *, reason: Optional[str] = None) -> None: async def delete(self, *, reason: Optional[str] = None) -> None:
"""|coro| """|coro|
@ -184,6 +190,10 @@ class StreamIntegration(Integration):
The integration account information. The integration account information.
synced_at: :class:`datetime.datetime` synced_at: :class:`datetime.datetime`
An aware UTC datetime representing when the integration was last synced. An aware UTC datetime representing when the integration was last synced.
scopes: List[:class:`str`]
The OAuth2 scopes the application has been authorized for.
.. versionadded:: 2.7
""" """
__slots__ = ( __slots__ = (
@ -352,6 +362,10 @@ class BotIntegration(Integration):
The integration account information. The integration account information.
application: :class:`IntegrationApplication` application: :class:`IntegrationApplication`
The application tied to this integration. The application tied to this integration.
scopes: List[:class:`str`]
The OAuth2 scopes the application has been authorized for.
.. versionadded:: 2.7
""" """
__slots__ = ('application',) __slots__ = ('application',)

27
discord/message.py

@ -3051,3 +3051,30 @@ class Message(PartialMessage, Hashable):
The newly edited message. The newly edited message.
""" """
return await self.edit(attachments=[a for a in self.attachments if a not in attachments]) return await self.edit(attachments=[a for a in self.attachments if a not in attachments])
def is_forwardable(self) -> bool:
""":class:`bool`: Whether the message can be forwarded using :meth:`Message.forward`.
A message is forwardable only if it is a basic message type and does not
contain a poll, call, or activity, and is not a system message.
.. versionadded:: 2.7
"""
if self.type not in (
MessageType.default,
MessageType.reply,
MessageType.chat_input_command,
MessageType.context_menu_command,
):
return False
if self.poll is not None:
return False
if self.call is not None:
return False
if self.activity is not None:
return False
return True

13
discord/ui/view.py

@ -899,7 +899,7 @@ class ViewStore:
self._modals[view.custom_id] = view # type: ignore self._modals[view.custom_id] = view # type: ignore
return return
dispatch_info = self._views.setdefault(message_id, {}) dispatch_info = self._views.get(message_id, {})
is_fully_dynamic = True is_fully_dynamic = True
for item in view.walk_children(): for item in view.walk_children():
if isinstance(item, DynamicItem): if isinstance(item, DynamicItem):
@ -910,25 +910,28 @@ class ViewStore:
is_fully_dynamic = False is_fully_dynamic = False
view._cache_key = message_id view._cache_key = message_id
if dispatch_info:
self._views[message_id] = dispatch_info
if message_id is not None and not is_fully_dynamic: if message_id is not None and not is_fully_dynamic:
self._synced_message_views[message_id] = view self._synced_message_views[message_id] = view
def remove_view(self, view: View) -> None: def remove_view(self, view: BaseView) -> None:
if view.__discord_ui_modal__: if view.__discord_ui_modal__:
self._modals.pop(view.custom_id, None) # type: ignore self._modals.pop(view.custom_id, None) # type: ignore
return return
dispatch_info = self._views.get(view._cache_key) dispatch_info = self._views.get(view._cache_key)
if dispatch_info: if dispatch_info:
for item in view._children: for item in view.walk_children():
if isinstance(item, DynamicItem): if isinstance(item, DynamicItem):
pattern = item.__discord_ui_compiled_template__ pattern = item.__discord_ui_compiled_template__
self._dynamic_items.pop(pattern, None) self._dynamic_items.pop(pattern, None)
elif item.is_dispatchable(): elif item.is_dispatchable():
dispatch_info.pop((item.type.value, item.custom_id), None) # type: ignore dispatch_info.pop((item.type.value, item.custom_id), None) # type: ignore
if len(dispatch_info) == 0: if dispatch_info is not None and len(dispatch_info) == 0:
self._views.pop(view._cache_key, None) self._views.pop(view._cache_key, None)
self._synced_message_views.pop(view._cache_key, None) # type: ignore self._synced_message_views.pop(view._cache_key, None) # type: ignore

1
discord/utils.py

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

5
docs/ext/commands/api.rst

@ -536,6 +536,11 @@ Converters
.. autoclass:: discord.ext.commands.SoundboardSoundConverter .. autoclass:: discord.ext.commands.SoundboardSoundConverter
:members: :members:
.. attributetable:: discord.ext.commands.Timestamp
.. autoclass:: discord.ext.commands.Timestamp
:members:
.. attributetable:: discord.ext.commands.clean_content .. attributetable:: discord.ext.commands.clean_content
.. autoclass:: discord.ext.commands.clean_content .. autoclass:: discord.ext.commands.clean_content

8
docs/interactions/api.rst

@ -1169,6 +1169,14 @@ Range
.. autoclass:: discord.app_commands.Range .. autoclass:: discord.app_commands.Range
:members: :members:
Timestamp
++++++++++
.. attributetable:: discord.app_commands.Timestamp
.. autoclass:: discord.app_commands.Timestamp
:members:
Translations Translations
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

Loading…
Cancel
Save