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
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,

44
discord/client.py

@ -88,7 +88,7 @@ if TYPE_CHECKING:
from .abc import Messageable, PrivateChannel, Snowflake, SnowflakeTime
from .app_commands import Command, ContextMenu
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 .guild import GuildChannel
from .integrations import Integration
@ -1753,6 +1753,38 @@ class Client:
timeout: Optional[float] = ...,
) -> 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
@overload
@ -1859,6 +1891,16 @@ class Client:
timeout: Optional[float] = ...,
) -> 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
@overload

24
discord/ext/commands/converter.py

@ -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`.

19
discord/ext/commands/errors.py

@ -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.

6
discord/gateway.py

@ -210,6 +210,10 @@ class KeepAliveHandler(threading.Thread):
def tick(self) -> None:
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:
ack_time = time.perf_counter()
self._last_ack = ack_time
@ -541,7 +545,7 @@ class DiscordWebSocket:
if op == self.HEARTBEAT:
if self._keep_alive:
beat = self._keep_alive.get_payload()
beat = self._keep_alive.beat()
await self.send_as_json(beat)
return

16
discord/integrations.py

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

27
discord/message.py

@ -3051,3 +3051,30 @@ class Message(PartialMessage, Hashable):
The newly edited message.
"""
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
return
dispatch_info = self._views.setdefault(message_id, {})
dispatch_info = self._views.get(message_id, {})
is_fully_dynamic = True
for item in view.walk_children():
if isinstance(item, DynamicItem):
@ -910,25 +910,28 @@ class ViewStore:
is_fully_dynamic = False
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:
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__:
self._modals.pop(view.custom_id, None) # type: ignore
return
dispatch_info = self._views.get(view._cache_key)
if dispatch_info:
for item in view._children:
for item in view.walk_children():
if isinstance(item, DynamicItem):
pattern = item.__discord_ui_compiled_template__
self._dynamic_items.pop(pattern, None)
elif item.is_dispatchable():
dispatch_info.pop((item.type.value, item.custom_id), None) # type: ignore
if len(dispatch_info) == 0:
self._views.pop(view._cache_key, None)
if dispatch_info is not None and len(dispatch_info) == 0:
self._views.pop(view._cache_key, None)
self._synced_message_views.pop(view._cache_key, None) # type: ignore

1
discord/utils.py

@ -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:

5
docs/ext/commands/api.rst

@ -536,6 +536,11 @@ Converters
.. autoclass:: discord.ext.commands.SoundboardSoundConverter
:members:
.. attributetable:: discord.ext.commands.Timestamp
.. autoclass:: discord.ext.commands.Timestamp
:members:
.. attributetable:: 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
:members:
Timestamp
++++++++++
.. attributetable:: discord.app_commands.Timestamp
.. autoclass:: discord.app_commands.Timestamp
:members:
Translations
~~~~~~~~~~~~~

Loading…
Cancel
Save